Sibirsky
Sibirsky

Reputation: 156

Django: unique_together for foreign field

For next two models:

class Foo(models.Model):
    parent = models.ForeignKey(Parent)
    name = models.CharField()


class Bar(models.Model):
    foreign = models.ForeignKey(Foo)
    value = models.CharField(max_length=20)

I need to have unique_together constraint for Bar model:

class Meta:
    unique_together = ('value', 'foreign__parent')

Which is impossible in Django.

But is it possible to achieve this on database level (Postgresql) with some contraint or model level validation to omit possible case (lock table?) when simultaneously same value may be saved to different Bar instances?

Django 2.2.4

Upvotes: 0

Views: 472

Answers (2)

Sibirsky
Sibirsky

Reputation: 156

Thanks to Anjaneyulu Batta, came to next solution:

@contextmanager
def lock_table(model):
    """
    Lock target table on commands UPDATE, DELETE and INSERT
    """
    with transaction.atomic(), transaction.get_connection().cursor() as cursor:
        cursor.execute(
            f'LOCK TABLE {model._meta.db_table} IN ROW EXCLUSIVE MODE;'
        )
        yield

And for model:

def validate_unique(self, exclude=None):
    super().validate_unique(exclude)
    queryset = type(self).objects.filter(
        value=self.value,
        foreign__parent=self.foreign.parent,
    )

    if self.pk:
        queryset = queryset.exclude(pk=self.pk)

    if queryset.exists():
        raise IntegrityError(_('Value must be unique for foreign field'))

def save(self, force_insert=False, force_update=False, using=None,
         update_fields=None):
    with lock_table(type(self)):
        self.validate_unique()
        super().save(force_insert, force_update, using, update_fields)

Should work fairly save.

Upvotes: 0

anjaneyulubatta505
anjaneyulubatta505

Reputation: 11665

You cannot achieve it with unique_together because it creates an index in the database level. But, You can add validation for it yourself though, simply overwrite the validate_unique method and add this validation to it.

from django.core.exceptions import ValidationError

class Bar(models.Model):
    foreign = models.ForeignKey(Foo)
    value = models.CharField(max_length=20)

    def validate_unique(self, *args, **kwargs):
        super(MyModel, self).validate_unique(*args, **kwargs)

        if self.__class__.objects.\
                filter(foreign__parent=self.foreign.parent, vaue=self.value).exists():
            raise ValidationError(
                message='record already exists with given values.',
                code='unique_together',
            )

Upvotes: 1

Related Questions