Djangoの一意のBooleanField値?


87

私のmodels.pyが次のようなものだとします。

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

私は自分のCharacterインスタンスの1つだけを持ちis_the_chosen_one == True、他のすべてのインスタンスは持っていたいですis_the_chosen_one == False。この一意性の制約が尊重されるようにするにはどうすればよいですか?

データベース、モデル、および(管理)フォームレベルでの制約を尊重することの重要性を考慮した回答のトップマーク!


4
良い質問。また、そのような制約を設定できるかどうかも知りたいです。単純にそれを一意の制約にした場合、データベース内の可能な行は2つだけになることを私は知っています;-)
Andre Miller

必ずしもそうとは限りません。NullBooleanFieldを使用する場合は、次のことができるはずです:(True、False、任意の数のNULL)。
Matthew Schinckel 2009年

私の調査によると、@ sementeの回答では、データベース、モデル、および(管理)フォームレベルで制約を尊重することの重要性を考慮に入れていますが、制約が必要なthroughテーブルに対しても優れたソリューションを提供ManyToManyFieldunique_togetherます。
raratiru 2016

回答:


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

8
これを編集してに変更しようとしsave(self)ましたsave(self, *args, **kwargs)が、編集が拒否されました。レビューアの誰かが理由を説明するのに時間がかかるでしょうか-これはDjangoのベストプラクティスと一致しているように見えるからです。
スキュタレー2012年

14
try / exceptionの必要性を取り除き、プロセスをより効率的にするために編集を試みましたが、拒否されましget()た。Characterオブジェクトをsave()実行してから再度実行する代わりに、フィルターと更新を行うだけで、SQLクエリが1つだけ生成されます。 DBの一貫性を維持するのに役立ち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アプリケーションを実行している場合は、saveまたはcleanメソッドを信頼しないでください。それでも、おそらくデータベースレベルでより安全な方法を実装する必要があります。
u.unver34 2018

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)

アダムの同様の回答を編集してみましたが、元の回答を変更しすぎたため却下されました。この方法は、他のエントリのチェックが1つのクエリで実行されるため、より簡潔で効率的です。


7
私は、これが最良の答えだと思うが、私はラッピングをお勧めしますsave@transaction.atomicトランザクション。すべてのフラグを削除しても、保存に失敗し、すべての文字が選択されなくなる可能性があるためです。
mitar 2016年

そう言ってくれてありがとう。あなたは絶対に正しいです、そして私は答えを更新します。
Ellis Percival

@Mitar@transaction.atomicは、競合状態からも保護します。
Pawel Furmaniak 2018

1
すべての中で最良の解決策!
アルトゥーロ

1
transaction.atomicに関しては、デコレータの代わりにコンテキストマネージャを使用しました。ブールフィールドがtrueの場合にのみ問題になるため、すべてのモデルの保存でアトミックトランザクションを使用する理由はわかりません。with transaction.atomic:if内に保存するとともにifステートメント内で使用することをお勧めします。次に、elseブロックを追加し、elseブロックに保存します。
alexbhandari

29

カスタムモデルのクリーニング/保存を使用する代わりに、のメソッドをオーバーライドするカスタムフィールドを作成しました。別のフィールドがの場合にエラーを発生させる代わりに、他のすべてのフィールドがの場合に作成しました。また、フィールドがあり、他のフィールドがない場合にエラーを発生させる代わりに、フィールドを次のように保存しましたpre_savedjango.db.models.BooleanFieldTrueFalseTrueFalseTrueTrue

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
これは他の方法よりもはるかにきれいに見えます
ピスタチオ2013年

2
モデルUniqueBooleanがTrueの場合、objects.updateが他のすべてのオブジェクトをFalseに設定することは潜在的に危険であるように思われますが、私もこのソリューションが好きです。UniqueBooleanFieldがオプションの引数を取り、他のオブジェクトをFalseに設定する必要があるかどうか、またはエラーを発生させる必要があるかどうかを示すと、さらに良いでしょう(他の賢明な代替手段)。また、属性をtrueに設定するelifでのコメントを考えると、次のように変更Return Trueする必要があると思いますsetattr(model_instance, self.attname, True)
Andrew Chase

2
UniqueBooleanFieldは、必要な数のFalse値を持つことができるため、実際には一意ではありません。より良い名前が何であるかわからない... OneTrueBooleanField?私が本当に望んでいるのは、これを外部キーと組み合わせてスコープできるようにすることです。これにより、リレーションシップごとに1回だけTrueが許可されるBooleanFieldを使用できます(たとえば、CreditCardには「プライマリ」フィールドとユーザーへのFKがあります。ユーザー/プライマリの組み合わせは、使用ごとに1回Trueです)。その場合、saveをオーバーライドするAdamの答えは私にとってより簡単になると思います。
アンドリューチェイス

1
この方法では、行trueのみを削除するかのように、行が設定されていない状態になる可能性があることに注意してくださいtrue
rblk 2017

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つしか持つことができません。


1
私も考えた最初の解決策。NULLは常に一意であるため、複数のNULLを持つ列を常に持つことができます。
kaleissin 2013年

10

ここでの答えを達成しようとすると、それらのいくつかは同じ問題にうまく対処し、それぞれが異なる状況に適していることがわかりました。

私が選ぶだろう:

  • @semente:データベース、モデル、および管理フォームレベルで制約を尊重し、DjangoORMを可能な限りオーバーライドしません。さらにそれはできます多分ある状況での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:データベースに1回だけ追加でヒットし、現在のエントリを選択されたエントリとして受け入れます。清潔でエレガント。

    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)  
    

私の場合には適していないが実行可能な他の解決策:

@nemocorpclean、検証を実行するためにメソッドをオーバーライドしています。ただし、どのモデルが「1つ」であるかは報告されず、これはユーザーフレンドリーではありません。それにもかかわらず、特に誰かが@Flyteほど積極的になるつもりがない場合、これは非常に優れたアプローチです。

@ saul.shanabrook@ThierryJ。は、他の「is_the_one」エントリをに変更するFalseか、を上げるカスタムフィールドを作成しますValidationError。どうしても必要な場合を除いて、Djangoのインストールに新しい機能を実装するのは気が進まない。

@daigorocub:Djangoシグナルを使用します。私はそれをユニークなアプローチを見つけて使用する方法のヒント与えDjangoの信号を。ただし、この手順を「分離されたアプリケーション」の一部と見なすことができないため、これが「厳密に言えば」信号の「適切な」使用であるかどうかはわかりません。


レビューありがとうございます!ここでもコードを更新したい場合に備えて、コメントの1つに基づいて、回答を少し更新しました。
エリスパーシヴァル

@EllisPercivalヒントありがとうございます!それに応じてコードを更新しました。ただし、models.Model.save()は何かを返さないことに注意しください
raratiru

それはいいです。ほとんどの場合、最初のリターンを自分のラインに戻すことを節約するだけです。アトミックトランザクションに.save()が含まれていないため、実際にはバージョンが正しくありません。さらに、代わりに「transaction.atomic():」を使用する必要があります。
エリスパーシヴァル

1
@EllisPercival OK、ありがとう!実際、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 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')
    ]

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の値で別のレコードを保存しようとするとを発生させます。

また、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

代わりに、ここで自分の質問に答えるつもりはなかったのですか?基本的にあなたと@sampablokuperは同じ質問をしました
j_syk

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に、またはその逆に交換するだけです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.