Django:如何在模型内验证unique_together

4 投票
3 回答
15833 浏览
提问于 2025-04-15 17:08

我有以下内容:

class AccountAdmin(models.Model):

    account = models.ForeignKey(Account)
    is_master = models.BooleanField()
    name = models.CharField(max_length=255)
    email = models.EmailField()

    class Meta:
        unique_together = (('Account', 'is_master'), ('Account', 'username'),)

如果我创建一个新的AccountAdmin,使用的用户名和同一个账户下的另一个用户名相同,那么系统不会给我一个错误提示来显示在页面上,而是直接出现一个完整的错误(IntegrityError),导致页面崩溃。我希望在我的视图中,我可以这样做:

if new_accountadmin_form.is_valid():
    new_accountadmin_form.save()

我该如何解决这个问题?有没有类似于is_valid()的方法,可以检查数据库中是否违反了unique_together = (('Account', 'is_master'), ('Account', 'username'),)这一部分的规则?

我希望在我的视图中不需要处理IntegrityError。这是业务逻辑和展示逻辑混在一起了。这违反了DRY原则,因为如果我在两个页面上显示同一个表单,我就得重复同样的代码块。如果我有两个表单用于同样的事情,我还得再次写相同的异常处理代码。

3 个回答

1

Model.Meta.unique_together 是用来在数据库中设置一个限制的,而 ModelForm.is_valid() 主要是检查数据类型是否正确。即使它检查了这些限制,仍然可能会出现竞争条件,这样在调用 save() 时可能会导致一个完整性错误(IntegrityError)。

你可能需要捕捉这个完整性错误:

if new_accountadmin_form.is_valid():
    try:
        newaccountadmin_form.save()
    except IntegrityError, error:
        # here's your error handling code
3

为了提供一种完全通用的方法。在模型中有以下两个辅助函数:

def getField(self,fieldName):
  # return the actual field (not the db representation of the field)
  try:
    return self._meta.get_field_by_name(fieldName)[0]
  except models.fields.FieldDoesNotExist:
    return None

还有

def getUniqueTogether(self):
  # returns the set of fields (their names) that must be unique_together
  # otherwise returns None
  unique_together = self._meta.unique_together
  for field_set in unique_together:
    return field_set
  return None

在表单中有以下函数:

def clean(self):
  cleaned_data = self.cleaned_data
  instance = self.instance

  # work out which fields are unique_together
  unique_filter = {}
  unique_fields = instance.getUniqueTogether()
  if unique_fields:
    for unique_field in unique_fields:
      field = instance.getField(unique_field)
      if field.editable: 
        # this field shows up in the form,
        # so get the value from the form
        unique_filter[unique_field] = cleaned_data[unique_field]
      else: 
        # this field is excluded from the form,
        # so get the value from the model
        unique_filter[unique_field] = getattr(instance,unique_field)

    # try to find if any models already exist in the db;
    # I find all models and then exlude those matching the current model.
    existing_instances = type(instance).objects.filter(**unique_filter).exclude(pk=instance.pk)

    if existing_instances:
      # if we've gotten to this point, 
      # then there is a pre-existing model matching the unique filter
      # so record the relevant errors
      for unique_field in unique_fields:
        self.errors[unique_field] = "This value must be unique."
8

这里有两个选择:

a) 使用一个尝试块(try block),在这里你可以保存你的模型,并捕捉到完整性错误(IntegrityError),然后处理这个错误。大概是这样的:

try:
    new_accountadmin_form.save()
except IntegrityError:
    new_accountadmin_form._errors["account"] = ["some message"]
    new_accountadmin_form._errors["is_master"] = ["some message"]

    del new_accountadmin_form.cleaned_data["account"]
    del new_accountadmin_form.cleaned_data["is_master"]

b) 在你的表单的 clean() 方法中,检查一下是否已经存在某一行数据,如果存在,就抛出一个 forms.ValidationError,并给出一个合适的提示信息。示例可以参考 这里


所以,选择 b) 吧……这就是我为什么 提到文档的原因;你需要的内容都在里面。

不过大概是这样的:

class YouForm(forms.Form):
    # Everything as before.
    ...

    def clean(self):
       """ This is the form's clean method, not a particular field's clean method """
       cleaned_data = self.cleaned_data

       account = cleaned_data.get("account")
       is_master = cleaned_data.get("is_master")
       username = cleaned_data.get("username")

       if AccountAdmin.objects.filter(account=account, is_master=is_master).count() > 0:
           del cleaned_data["account"]
           del cleaned_data["is_master"]
           raise forms.ValidationError("Account and is_master combination already exists.")

       if AccountAdmin.objects.filter(account=account, username=username).count() > 0:
           del cleaned_data["account"]
           del cleaned_data["username"]
           raise forms.ValidationError("Account and username combination already exists.")

    # Always return the full collection of cleaned data.
    return cleaned_data

顺便说一下,我刚意识到你上面提到的 unique_together 里有一个叫 username 的字段,但在模型中并没有这个字段。

上面的 clean 方法是在所有单独字段的 clean 方法都调用完之后才被调用的。

撰写回答