Daniel
Daniel

Reputation: 3527

Django - Unique Collection of Foreign Keys Constraint

I have two models like so:

class Group(...):
    pass

class Identifier(...):
    value = models.CharField(...)
    group = models.ForeignKey('Group', ..., related_named = 'identifiers')

How can I:

  1. restrict a Group to only have at most 4 Identifiers?
  2. Ensure that any combination of up to 4 Identifiers (the value of the identifier) is unique across all Groups?

For part 2, here is an example of the flattened Groups table:

row | id__0__val | id__1__val | id__2__val | id__3__val 
--- | ---------- | ---------- | ---------- | ----------
  0 |       abc  |        123 |        xyz |        456
  1 |       abc  |        123 |        xyz |          -   <-- valid (nulls are okay)
  2 |       123  |        abc |        xyz |        456   <-- invalid (same combo as row 0)   

Previously I have tried (something like) this, but it seems messy, has limited functionality, and I'm not sure it will even work:

class Group(...):
    id__0 = models.OneToOneField('Identifier', blank = True, null = True, ...)
    id__1 = models.OneToOneField('Identifier', blank = True, null = True, ...)
    id__2 = models.OneToOneField('Identifier', blank = True, null = True, ...)
    id__3 = models.OneToOneField('Identifier', blank = True, null = True, ...)

    class Meta: 
        unique_together = ('id__0__value', 'id__1__value', 'id__2__value', 'id__3__value')

What is a better way to handle this constraint?

Upvotes: 4

Views: 734

Answers (2)

Brian Destura
Brian Destura

Reputation: 12078

My take on this, but doing it through a clean method:

class Group(models.Model):
    MAX_IDENTIFIERS_PER_GROUP = 4

class Identifier(models.Model):
    value = models.CharField()
    group = models.ForeignKey('Group', related_named='identifiers')

    def clean(self):
        identifiers = self.group.identifiers.values_list('value', flat=True)

        # If a new identifier is being created, but the current group already has identifiers == MAX_IDENTIFIERS_PER_GROUP
        if not self.pk and len(identifiers) == Group.MAX_IDENTIFIERS_PER_GROUP:
            raise ValidationError(f'Cannot have more than {Group.MAX_IDENTIFIERS_PER_GROUP} identifiers for group {self.group.pk}')

        # If there is another group with the same values in identifiers as the current identifier's group
        conflicting_identifier = Identifier.objects
            .filter(value__in=set(identifiers + [self.value, ]))
            .exclude(group=self.group)
            .values('group')
            .annotate(value_count=Count('value'))
            .filter(value_count__gte=Group.MAX_IDENTIFIERS_PER_GROUP).first()

        if conflicting_identifier:
            raise ValidationError(f'Current identifier combination is already present in group {conflicting_identifier.group.pk}')

Note that the clean does not get run automatically in save so this needs to be manually used. (Haven't tested this but the gist should be clear ;) )

Upvotes: 1

Adelina
Adelina

Reputation: 11931

Could do it via validate_unique method:

class Group(models.Model):
    ...

    def validate_unique(self, exclude=None):
        qs = Identifier.objects.filter(group_id=self.id)
        # restrict a Group to only have at most 4 Identifiers
        if qs.count() >= 4:
            raise ValidationError("group already has 4 Identifiers")
        # Ensure that any combination of up to 4 Identifiers is unique across all Groups
        values = []
        for group in Group.objects.all():
            values.append([i.value for i in group.identifiers])
        current_values = [i.value for i in self.identifiers]
        # https://stackoverflow.com/questions/22483730/python-check-if-list-of-lists-of-lists-contains-a-specific-list
        if current_values in values:
            raise ValidationError("group with these identifiers already exists")

        

This is pseudo code so might not work - but you get the idea :)

Upvotes: 0

Related Questions