77

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.

auvipy
  • 769
  • 10
  • 22
chiurox
  • 1,549
  • 3
  • 18
  • 28

7 Answers7

90

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

Wolph
  • 78,177
  • 11
  • 137
  • 148
16

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',
            )
shapiromatron
  • 569
  • 6
  • 11
Manu Artero
  • 9,238
  • 6
  • 58
  • 73
  • This will not pass when editing a MyModel instance that exists already. – elsadek Jan 25 '22 at 20:40
  • should work for an already existing model, what's the error you're getting? – Manu Artero Jan 26 '22 at 11:23
  • 1
    Not an error, the queryset will return the loaded object also, therefor it will always raise the duplicate error of itself, you should exclude the object id from the queryset. – elsadek Jan 26 '22 at 15:34
12

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)
5

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'),
            )
        ]
auvipy
  • 769
  • 10
  • 22
3

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"))
Harun-Ur-Rashid
  • 3,338
  • 1
  • 15
  • 22
2
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.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
isAif
  • 2,126
  • 5
  • 22
  • 34
1

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)
Karl Zillner
  • 581
  • 7
  • 16