Django 惟一连接(带外键)

我有一个情况,我想使用 unique_together的 Meta 选项来执行某个规则,下面是中间模型:

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

这是 UserProfile:

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

谢谢。

60301 次浏览

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

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)

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',
)

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'),
)
]

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"))
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.

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)