【问题标题】:Unique BooleanField value in Django?Django中唯一的BooleanField值?
【发布时间】:2010-11-30 02:52:30
【问题描述】:

假设我的 models.py 是这样的:

class Character(models.Model):
    name = models.CharField(max_length=255)
    is_the_chosen_one = models.BooleanField()

我只希望我的一个 Character 实例拥有 is_the_chosen_one == True 并且所有其他实例拥有 is_the_chosen_one == False 。我怎样才能最好地确保遵守这个唯一性约束?

考虑到尊重数据库、模型和(管理员)表单级别约束的重要性的答案的最高分!

【问题讨论】:

  • 好问题。我也很好奇是否可以设置这样的约束。我知道,如果您只是将其设置为唯一约束,您最终将在数据库中只有两个可能的行;-)
  • 不一定:如果您使用 NullBooleanField,那么您应该能够拥有:(一个 True,一个 False,任意数量的 NULL)。
  • 根据my research@semente 的回答,考虑到尊重数据库、模型和(管理员)表单级别约束的重要性,同时它甚至为@987654327 提供了一个很好的解决方案@ 需要 unique_together 约束的 ManyToManyField 表。

标签: database django django-models django-admin django-forms


【解决方案1】:

在实现覆盖 model.save()* 的解决方案时,我遇到了 Django Admin 在点击 model.save() 之前引发错误的问题。原因似乎是在调用 model.save() 之前,管理员调用了 model.clean()(或者可能是 model.full_clean(),我没有仔细调查)。 model.clean() 依次调用 model.validate_unique() 在我的自定义保存方法可以处理唯一违规之前引发 ValidationError。为了解决这个问题,我重写了 model.validate_unique() 如下:

    def validate_unique(self, exclude=None):
        try:
            super().validate_unique(exclude=exclude)
        except ValidationError as e:
            validation_errors = e.error_dict
            try:
                list_validation_errors = validation_errors["is_the_chosen_one"]
                for validation_error in list_validation_errors:
                    if validation_error.code == "unique":
                        list_validation_errors.remove(validation_error)
                if not list_validation_errors:
                    validation_errors.pop(key)
            except KeyError:
                continue
            if e.error_dict:
                raise e

* 对于使用 pre_save 的信号解决方案也是如此,因为在调用 .validate_unique 之前也不发送 pre_save

【讨论】:

    【解决方案2】:

    这里的答案试图入不敷出,我发现其中一些成功地解决了相同的问题,并且每个都适用于不同的情况:

    我会选择:

    • @semente:尊重数据库、模型和管理表单级别的约束,同时尽可能地覆盖 Django ORM。此外,它可以在ManyToManyFieldunique_together 情况下的through 表中使用。

        class MyModel(models.Model):
            is_the_chosen_one = models.BooleanField(null=True, default=None, unique=True)
      
            def save(self, *args, **kwargs):
                if self.is_the_chosen_one is False:
                    self.is_the_chosen_one = None
                super(MyModel, self).save(*args, **kwargs)
      

      更新NullBooleanField 将由Django-4.0 改成deprecated,对于BooleanField(null=True)

    • @Ellis Percival: 只多打一次数据库,并接受当前条目作为选择的条目。干净优雅。

        from django.db import transaction
      
        class Character(models.Model):
            name = models.CharField(max_length=255)
            is_the_chosen_one = models.BooleanField()
      
        def save(self, *args, **kwargs):
            if not self.is_the_chosen_one:
                # The use of return is explained in the comments
                return super(Character, self).save(*args, **kwargs)  
            with transaction.atomic():
                Character.objects.filter(
                    is_the_chosen_one=True).update(is_the_chosen_one=False)
                # The use of return is explained in the comments
                return super(Character, self).save(*args, **kwargs)  
      

    其他不适合我的情况但可行的解决方案:

    @nemocorp 覆盖clean 方法来执行验证。但是,它不会报告哪个模型是“那个”,这对用户不友好。尽管如此,这是一种非常好的方法,特别是如果有人不打算像@Flyte 那样激进。

    @saul.shanabrook@Thierry J. 将创建一个自定义字段,它将任何其他“is_the_one”条目更改为False 或引发ValidationError。我只是不愿意在我的 Django 安装中加入新功能,除非绝对必要。

    @daigorocub:使用 Django 信号。我发现它是一种独特的方法,并提示了如何使用Django Signals。但是我不确定这是否是 - 严格来说 - “正确”使用信号,因为我不能将此过程视为“解耦应用程序”的一部分。

    【讨论】:

    • 感谢您的评论!我已经根据其中一个 cmets 稍微更新了我的答案,以防您也想在这里更新您的代码。
    • @EllisPercival 感谢您的提示!我相应地更新了代码。请记住,models.Model.save() 不会返回任何内容。
    • 没关系。这主要是为了节省在自己的线路上获得第一次回报。您的版本实际上不正确,因为它不包括 .save() 在原子事务中。另外,它应该是'with transaction.atomic():'。
    • @EllisPercival 好的,谢谢!事实上,如果save() 操作失败,我们需要回滚所有内容!
    【解决方案3】:

    我会覆盖模型的保存方法,如果您将布尔值设置为 True,请确保所有其他值都设置为 False。

    from django.db import transaction
    
    class Character(models.Model):
        name = models.CharField(max_length=255)
        is_the_chosen_one = models.BooleanField()
    
        def save(self, *args, **kwargs):
            if not self.is_the_chosen_one:
                return super(Character, self).save(*args, **kwargs)
            with transaction.atomic():
                Character.objects.filter(
                    is_the_chosen_one=True).update(is_the_chosen_one=False)
                return super(Character, self).save(*args, **kwargs)
    

    我尝试编辑亚当的类似答案,但由于更改了太多原始答案而被拒绝。这种方式更加简洁高效,因为其他条目的检查是在单个查询中完成的。

    【讨论】:

    • 我认为这是最好的答案,但我建议将 save 包装到 @transaction.atomic 事务中。因为您可能会删除所有标志,但随后保存失败并且您最终会得到所有未选择的字符。
    • 谢谢你这么说。你是绝对正确的,我会更新答案。
    • @Mitar @transaction.atomic 还可以防止竞争条件。
    • 最好的解决方案!
    • 关于 transaction.atomic 我使用了上下文管理器而不是装饰器。我认为没有理由在每个模型上都使用原子事务,因为这仅在布尔字段为真时才重要。我建议在 if 语句中使用 with transaction.atomic: 并在 if 中保存。然后添加一个 else 块并保存在 else 块中。
    【解决方案4】:

    将这种约束添加到模型中会更简单 在 Django 2.2 版之后。您可以直接使用UniqueConstraint.conditionDjango Docs

    只需像这样覆盖您的模型class Meta

    class Meta:
        constraints = [
            UniqueConstraint(fields=['is_the_chosen_one'], condition=Q(is_the_chosen_one=True), name='unique_is_the_chosen_one')
        ]
    

    【讨论】:

    • 这个简单明了。伟大的!谢谢。
    • 这是要走的路。
    【解决方案5】:

    2020 年更新让初学者的事情变得不那么复杂:

    class Character(models.Model):
        name = models.CharField(max_length=255)
        is_the_chosen_one = models.BooleanField(blank=False, null=False, default=False)
    
        def save(self):
             if self.is_the_chosen_one == True:
                  items = Character.objects.filter(is_the_chosen_one = True)
                  for x in items:
                       x.is_the_chosen_one = False
                       x.save()
             super().save()
    

    当然,如果您希望唯一的布尔值为 False,您只需将 True 的每个实例与 False 交换,反之亦然。

    【讨论】:

      【解决方案6】:

      使用与扫罗类似的方法,但目的略有不同:

      class TrueUniqueBooleanField(BooleanField):
      
          def __init__(self, unique_for=None, *args, **kwargs):
              self.unique_for = unique_for
              super(BooleanField, self).__init__(*args, **kwargs)
      
          def pre_save(self, model_instance, add):
              value = super(TrueUniqueBooleanField, self).pre_save(model_instance, add)
      
              objects = model_instance.__class__.objects
      
              if self.unique_for:
                  objects = objects.filter(**{self.unique_for: getattr(model_instance, self.unique_for)})
      
              if value and objects.exclude(id=model_instance.id).filter(**{self.attname: True}):
                  msg = 'Only one instance of {} can have its field {} set to True'.format(model_instance.__class__, self.attname)
                  if self.unique_for:
                      msg += ' for each different {}'.format(self.unique_for)
                  raise ValidationError(msg)
      
              return value
      

      当尝试保存另一个值为 True 的记录时,此实现将引发 ValidationError

      另外,我添加了unique_for 参数,该参数可以设置为模型中的任何其他字段,以仅检查具有相同值的记录的真实唯一性,例如:

      class Phone(models.Model):
          user = models.ForeignKey(User)
          main = TrueUniqueBooleanField(unique_for='user', default=False)
      

      【讨论】:

        【解决方案7】:

        我尝试了其中的一些解决方案,并最终选择了另一种,只是为了代码简洁(不必覆盖表单或保存方法)。 为此,该字段在其定义中不能是唯一的,但信号会确保发生这种情况。

        # making default_number True unique
        @receiver(post_save, sender=Character)
        def unique_is_the_chosen_one(sender, instance, **kwargs):
            if instance.is_the_chosen_one:
                Character.objects.all().exclude(pk=instance.pk).update(is_the_chosen_one=False)
        

        【讨论】:

          【解决方案8】:

          仅此而已。

          def save(self, *args, **kwargs):
              if self.default_dp:
                  DownloadPageOrder.objects.all().update(**{'default_dp': False})
              super(DownloadPageOrder, self).save(*args, **kwargs)
          

          【讨论】:

            【解决方案9】:

            每当我需要完成这项任务时,我所做的就是覆盖模型的保存方法并让它检查是否有任何其他模型已经设置了标志(并将其关闭)。

            class Character(models.Model):
                name = models.CharField(max_length=255)
                is_the_chosen_one = models.BooleanField()
            
                def save(self, *args, **kwargs):
                    if self.is_the_chosen_one:
                        try:
                            temp = Character.objects.get(is_the_chosen_one=True)
                            if self != temp:
                                temp.is_the_chosen_one = False
                                temp.save()
                        except Character.DoesNotExist:
                            pass
                    super(Character, self).save(*args, **kwargs)
            

            【讨论】:

            • 我只需将 'def save(self):' 更改为:'def save(self, *args, **kwargs):'
            • 我试图编辑它以将 save(self) 更改为 save(self, *args, **kwargs) 但编辑被拒绝。任何评论者都可以花时间解释原因吗?因为这似乎与 Django 最佳实践一致。
            • 我尝试编辑以消除对 try/except 的需要并提高过程效率,但它被拒绝了。而不是 get()ing Character 对象然后save()ing 它再次,你只需要过滤和更新,它只产生一个 SQL 查询并有助于保持数据库的一致性:if self.is_the_chosen_one: Character.objects.filter(is_the_chosen_one=True).update(is_the_chosen_one=False) super(Character, self).save(*args, **kwargs)
            • 我无法建议任何更好的方法来完成该任务,但我想说的是,如果您正在运行可能需要一些请求的 Web 应用程序,请不要相信 save 或 clean 方法同一时刻的端点。您仍然必须在数据库级别实现更安全的方法。
            • 下面有更好的答案。 Ellis Percival 的回答使用transaction.atomic,这在这里很重要。使用单个查询也更有效。
            【解决方案10】:

            回答我的问题可以获得积分吗?

            问题是它发现自己在循环中,通过以下方式修复:

                # is this the testimonial image, if so, unselect other images
                if self.testimonial_image is True:
                    others = Photograph.objects.filter(project=self.project).filter(testimonial_image=True)
                    pdb.set_trace()
                    for o in others:
                        if o != self: ### important line
                            o.testimonial_image = False
                            o.save()
            

            【讨论】:

            • 不,回答您自己的问题并接受该答案没有积分。但是,如果有人赞成您的回答,则需要指出一些要点。 :)
            • 你确定你不是想回答你自己的问题here instead?基本上你和@sampablokuper 有同样的问题
            【解决方案11】:

            我没有使用自定义模型清理/保存,而是创建了一个 custom field 覆盖 django.db.models.BooleanField 上的 pre_save 方法。如果另一个字段是True,我没有引发错误,而是将所有其他字段设置为False,如果它是True。另外,如果字段为False 而没有其他字段为True,我也没有引发错误,而是将其保存为True

            fields.py

            from django.db.models import BooleanField
            
            
            class UniqueBooleanField(BooleanField):
                def pre_save(self, model_instance, add):
                    objects = model_instance.__class__.objects
                    # If True then set all others as False
                    if getattr(model_instance, self.attname):
                        objects.update(**{self.attname: False})
                    # If no true object exists that isnt saved model, save as True
                    elif not objects.exclude(id=model_instance.id)\
                                    .filter(**{self.attname: True}):
                        return True
                    return getattr(model_instance, self.attname)
            
            # To use with South
            from south.modelsinspector import add_introspection_rules
            add_introspection_rules([], ["^project\.apps\.fields\.UniqueBooleanField"])
            

            models.py

            from django.db import models
            
            from project.apps.fields import UniqueBooleanField
            
            
            class UniqueBooleanModel(models.Model):
                unique_boolean = UniqueBooleanField()
            
                def __unicode__(self):
                    return str(self.unique_boolean)
            

            【讨论】:

            • 这看起来比其他方法干净得多
            • 我也喜欢这个解决方案,尽管在模型 UniqueBoolean 为 True 的情况下让 objects.update 将所有其他对象设置为 False 似乎有潜在危险。如果 UniqueBooleanField 采用可选参数来指示是否应将其他对象设置为 False 或是否应引发错误(其他明智的选择),那就更好了。另外,鉴于您在 elif 中的评论,您希望将属性设置为 true,我认为您应该将 Return True 更改为 setattr(model_instance, self.attname, True)
            • UniqueBooleanField 并不是真正唯一的,因为您可以拥有任意数量的 False 值。不确定什么是更好的名字... OneTrueBooleanField?我真正想要的是能够结合外键来确定这个范围,这样我就可以拥有一个 BooleanField ,每个关系只允许为 True 一次(例如,CreditCard 有一个“主”字段和一个对用户的 FK 和用户/主要组合每次使用一次为真)。对于那种情况,我认为 Adam 的回答覆盖 save 对我来说会更直接。
            • 需要注意的是,如果您删除唯一的true 行,此方法允许您最终处于没有行设置为true 的状态。
            【解决方案12】:

            以下解决方案有点难看,但可能有效:

            class MyModel(models.Model):
                is_the_chosen_one = models.NullBooleanField(default=None, unique=True)
            
                def save(self, *args, **kwargs):
                    if self.is_the_chosen_one is False:
                        self.is_the_chosen_one = None
                    super(MyModel, self).save(*args, **kwargs)
            

            如果您将 is_the_chosen_one 设置为 False 或 None,它将始终为 NULL。 NULL 可以随心所欲,但只能有一个 True。

            【讨论】:

            • 也是我想到的第一个解决方案。 NULL 始终是唯一的,因此您始终可以拥有一个包含多个 NULL 的列。
            【解决方案13】:
            class Character(models.Model):
                name = models.CharField(max_length=255)
                is_the_chosen_one = models.BooleanField()
            
                def clean(self):
                    from django.core.exceptions import ValidationError
                    c = Character.objects.filter(is_the_chosen_one__exact=True)  
                    if c and self.is_the_chosen:
                        raise ValidationError("The chosen one is already here! Too late")
            

            这样做可以在基本管理表单中进行验证

            【讨论】:

              【解决方案14】:
              class Character(models.Model):
                  name = models.CharField(max_length=255)
                  is_the_chosen_one = models.BooleanField()
              
                  def save(self, *args, **kwargs):
                      if self.is_the_chosen_one:
                          qs = Character.objects.filter(is_the_chosen_one=True)
                          if self.pk:
                              qs = qs.exclude(pk=self.pk)
                          if qs.count() != 0:
                              # choose ONE of the next two lines
                              self.is_the_chosen_one = False # keep the existing "chosen one"
                              #qs.update(is_the_chosen_one=False) # make this obj "the chosen one"
                      super(Character, self).save(*args, **kwargs)
              
              class CharacterForm(forms.ModelForm):
                  class Meta:
                      model = Character
              
                  # if you want to use the new obj as the chosen one and remove others, then
                  # be sure to use the second line in the model save() above and DO NOT USE
                  # the following clean method
                  def clean_is_the_chosen_one(self):
                      chosen = self.cleaned_data.get('is_the_chosen_one')
                      if chosen:
                          qs = Character.objects.filter(is_the_chosen_one=True)
                          if self.instance.pk:
                              qs = qs.exclude(pk=self.instance.pk)
                          if qs.count() != 0:
                              raise forms.ValidationError("A Chosen One already exists! You will pay for your insolence!")
                      return chosen
              

              您也可以使用上面的表格作为管理员,只需使用

              class CharacterAdmin(admin.ModelAdmin):
                  form = CharacterForm
              admin.site.register(Character, CharacterAdmin)
              

              【讨论】:

                猜你喜欢
                • 2015-05-05
                • 2020-05-01
                • 2021-06-07
                • 1970-01-01
                • 2010-12-05
                • 1970-01-01
                • 2011-12-24
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多