Django Unique Together (with foreign keys)

Question:

I have a situation where I want to use the Meta options of unique_together to enforce a certain rule, here’s the intermediary model:

class UserProfileExtension(models.Model):
    extension = models.ForeignKey(Extension, unique=False)
    userprofile = models.ForeignKey(UserProfile, unique=False)
    user = models.ForeignKey(User, unique=False)  

    class Meta:
        unique_together = (("userprofile", "extension"),
                           ("user", "extension"),
                           # How can I enforce UserProfile's Client 
                           # and Extension to be unique? This obviously
                           # doesn't work, but is this idea possible without
                           # creating another FK in my intermediary model 
                           ("userprofile__client", "extension"))

and here’s UserProfile:

class UserProfile(models.Model):
    user = models.ForeignKey(User, unique=True)
    client = models.ForeignKey(Client)

Thanks.

Asked By: chiurox

||

Answers:

You can’t.

The unique_together clause is directly translated to the SQL unique index. And you can only set those on columns of a single table, not a combination of several tables.

You can add validation for it yourself though, simply overwrite the validate_unique method and add this validation to it.

Docs: http://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.validate_unique

Answered By: Wolph

My solution was to use Django’s get_or_create. By using get_or_create, a useless get will occur if the row already exists in the database, and the row will be created if it does not exist.

Example:

 
extension = Extension.objects.get(pk=someExtensionPK)
userProfile = UserProfile.objects.get(pk=someUserProfilePK)
UserProfileExtension.objects.get_or_create(extension=extension, userprofile=userProfile)
Answered By: Jason Pudzianowski

My 2 cents, complementing the accepted response from @Wolph

You can add validation for it yourself though, simply overwrite the validate_unique method and add this validation to it.

This is a working example code someone could find usefull.

from django.core.exceptions import ValidationError


class MyModel(models.Model):

    fk = models.ForeignKey(AnotherModel, on_delete=models.CASCADE)

    my_field = models.CharField(...)  # whatever

    def validate_unique(self, *args, **kwargs):
        super().validate_unique(*args, **kwargs)
        if self.__class__.objects.
                filter(fk=self.fk, my_field=self.my_field).
                exists():
            raise ValidationError(
                message='MyModel with this (fk, my_field) already exists.',
                code='unique_together',
            )
Answered By: Manu Artero

From django 2.2+ versions, it is suggested to use constraint & Index as model class meta option:

https://docs.djangoproject.com/en/3.2/ref/models/options/#django.db.models.Options.unique_together

https://docs.djangoproject.com/en/3.2/ref/models/options/#django.db.models.Options.constraints

class UniqueConstraintModel(models.Model):
    race_name = models.CharField(max_length=100)
    position = models.IntegerField()
    global_id = models.IntegerField()
    fancy_conditions = models.IntegerField(null=True)

    class Meta:
        constraints = [
            models.UniqueConstraint(
                name="unique_constraint_model_global_id_uniq",
                fields=('global_id',),
            ),
            models.UniqueConstraint(
                name="unique_constraint_model_fancy_1_uniq",
                fields=('fancy_conditions',),
                condition=models.Q(global_id__lte=1)
            ),
            models.UniqueConstraint(
                name="unique_constraint_model_fancy_3_uniq",
                fields=('fancy_conditions',),
                condition=models.Q(global_id__gte=3)
            ),
            models.UniqueConstraint(
                name="unique_constraint_model_together_uniq",
                fields=('race_name', 'position'),
                condition=models.Q(race_name='example'),
            )
        ]
Answered By: auvipy

You need to call Models.full_clean() method to call validate_unique for foreignKey. You can override save() to call this

class UserProfileExtension(models.Model):
    extension = models.ForeignKey(Extension, unique=False)
    userprofile = models.ForeignKey(UserProfile, unique=False)
    user = models.ForeignKey(User, unique=False)  


    def save(self, *args, **kwargs):
        self.full_clean()
        super().save(*args, **kwargs)

    class Meta:
        unique_together = (("userprofile", "extension"),
                       ("user", "extension"),
                       # How can I enforce UserProfile's Client 
                       # and Extension to be unique? This obviously
                       # doesn't work, but is this idea possible without
                       # creating another FK in my intermediary model 
                       ("userprofile__client", "extension"))
Answered By: Harun-Ur-Rashid
from django.core.exceptions import ValidationError

.....

class UserProfileExtension(models.Model):
    extension = models.ForeignKey(Extension, unique=False)
    userprofile = models.ForeignKey(UserProfile, unique=False)
    user = models.ForeignKey(User, unique=False)  

    def validate_unique(self, *args, **kwargs):
        super(UserProfileExtension, self).validate_unique(*args, **kwargs)
        query = UserProfileExtension.objects.filter(extension=self.extension)
        if query.filter(userprofile__client=self.userprofile.client).exists():
            raise ValidationError({'extension':['Extension already exits for userprofile__client',]})

The first query is to filter all records in UserProfileExtension model which has the same extension we are putting in the current record.

Then we filter the query returned to find if it already contains userprofile__client which we are passing in the current record.

Answered By: isAif

Another possible solution is to add this on your save method from your Model:

def save(self, *args, **kwargs):
    unique = self.__class__.objects.filter( extension =self.extension, userprofile=self.userprofile )
    if unique.exists():
        self.id = unique[0].id
    super(self.__class__, self).save(*args, **kwargs)
Answered By: Karl Zillner