Django中唯一的BooleanField值?


87

假设我的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。我怎样才能最好地确保遵守这种唯一性约束?

考虑到在数据库,模型和(admin)表单级别遵守约束的重要性的答案的最高分!


4
好问题。我也很好奇是否可以设置这样的约束。我知道,如果仅使它成为唯一约束,最终数据库中将只有两个可能的行;-)
Andre Miller 2009年

不一定:如果使用NullBooleanField,则应该能够:(True,False,任意数量的NULL)。
马修·申克尔

根据我的研究@ semente的答案考虑了在数据库,模型和(管理员)表单级别遵守约束的重要性,尽管它为需要约束的through表提供了一个很好的解决方案。ManyToManyFieldunique_together
raratiru's

Answers:


66

每当我需要完成此任务时,我要做的就是重写模型的save方法,并让它检查是否有其他模型已经设置了标志(并将其关闭)。

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)

3
我只是将'def save(self):'更改为:'def save(self,* args,** kwargs):'
Marek 2012年

8
我试图将其更改save(self)为,save(self, *args, **kwargs)但编辑被拒绝。任何评论者都可以花点时间解释原因-因为这似乎与Django最佳做法一致。
scytale 2012年

14
我尝试进行编辑以消除对try / except的需求,并使过程更高效,但它被拒绝了。.而不是先get()输入Character对象,然后save()再次将其选中,您只需要过滤和更新即可,它仅产生一个SQL查询并有助于保持数据库的一致性:if self.is_the_chosen_one:<newline> Character.objects.filter(is_the_chosen_one=True).update(is_the_chosen_one=False)<newline>super(Character, self).save(*args, **kwargs)
Ellis Percival 2014年

2
我无法建议任何更好的方法来完成该任务,但我想说的是,如果您正在运行Web应用程序,则不要信任保存或清除方法,因为这可能会在同一时间向端点发送一些请求。您仍然必须在数据库级别实施更安全的方法。
u.unver34 '18

1
下面有一个更好的答案。Ellis Percival的答案transaction.atomic在这里很重要。使用单个查询也更有效。
alexbhandari

33

我将覆盖模型的save方法,如果将布尔值设置为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)

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


7
我认为这是最好的答案,但我建议将其包装save@transaction.atomic交易。因为可能会删除所有标志,但是保存失败,最终所有字符都未被选中。
Mitar

谢谢你这么说。您绝对正确,我将更新答案。
Ellis Percival

@Mitar@transaction.atomic还可以防止种族状况。
Pawel Furmaniak '18年

1
最好的解决方案!
阿图罗

1
关于transaction.atomic,我使用了上下文管理器而不是装饰器。我认为没有理由在每个模型保存上都使用原子事务,因为这仅在布尔字段为true时才重要。我建议with transaction.atomic:在if语句内部使用,以及在if内部保存。然后添加一个else块,并保存在else块中。
alexbhandari

29

我没有使用自定义模型清理/保存,而是创建了一个自定义字段,字段覆盖上的pre_save方法django.db.models.BooleanField。如果不是其他字段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)

2
这看起来比其他方法干净得多
开心果

2
我也喜欢这种解决方案,尽管在模型UniqueBoolean为True的情况下,将objects.update设置为False似乎很危险。如果UniqueBooleanField接受一个可选参数来指示是否将其他对象设置为False或引发错误(另一个明智的选择),则效果会更好。另外,考虑到您在Elif中的注释(要将该属性设置为true),我认为您应该更改 Return Truesetattr(model_instance, self.attname, True)
Andrew Chase 2014年

2
UniqueBooleanField并不是唯一的,因为您可以根据需要拥有任意多个False值。不知道会有更好的名字吗... OneTrueBooleanField?我真正想要的是能够将其与外键结合使用,这样我就可以拥有一个BooleanField,每个关系仅允许一次为True(例如,CreditCard具有“ primary”字段,并且FK指向User和(每次使用一次,用户/主要组合为True)。对于这种情况,我认为亚当的答案优先于我。
Andrew Chase 2014年

1
应该注意的是,此方法使您最终处于未设置行的状态,就像true删除了唯一的true行一样。
rblk

11

以下解决方案有点难看,但可能会起作用:

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。


1
我想到的第一个解决方案也是。NULL始终是唯一的,因此您始终可以拥有一个包含多个NULL的列。
kaleissin

10

为了使收支平衡,我发现其中一些成功地解决了同一问题,并且每种都适用于不同的情况:

我会选:

  • @semente:在数据库,模型和管理表单级别遵守约束,同时尽可能最小化Django ORM。而且它可以大概在某种情况下在a的through表内使用 。ManyToManyFieldunique_together(我会检查并报告)

    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)
    
  • @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的提示。但是,我不确定这是否严格意义上来说是信号的“正确”使用,因为我不能将此过程视为“解耦应用程序”的一部分。


感谢您的评论!如果您也想在此处更新代码,则根据其中一项注释,我对答案做了一些更新。
Ellis Percival

@EllisPercival谢谢您的提示!我相应地更新了代码。请记住,尽管models.Model.save()不会返回任何内容。
raratiru

没关系。这主要是为了节省自己的第一笔收益。您的版本实际上是不正确的,因为它在原子事务中不包含.save()。另外,它应该是“ with transaction.atomic():”。
Ellis Percival

1
@EllisPercival好,谢谢!确实,如果save()操作失败,我们需要一切都回滚!
raratiru

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

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

这样做可以使验证在基本管理表单中可用


4

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

class Meta像这样覆盖您的模型:

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

2

就这样。

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

2

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

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

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

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

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

1

我回答问题会得到积分吗?

问题是它发现自己处于循环中,并通过以下方式解决:

    # 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()

不,回答您自己的问题并接受该回答没有任何意义。但是,如果有人支持您的回答,则有几点要注意。:)
dandan78 2011年

您确定不是要在这里回答自己的问题吗?基本上你和@sampablokuper有相同的问题
j_syk 2011年

1

我尝试了其中一些解决方案,最后又提出了一个解决方案,仅出于代码简短的目的(不必重写表单或保存方法)。为此,该字段在定义中不能唯一,但是信号可确保这种情况发生。

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

0

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交换,反之亦然。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.