ค่า BooleanField ที่ไม่ซ้ำกันใน Django?


90

สมมติว่า 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ทั้งหมดที่จะมี ฉันจะแน่ใจได้อย่างไรว่าข้อ จำกัด ด้านความเป็นเอกลักษณ์นี้ได้รับการเคารพ

คะแนนสูงสุดสำหรับคำตอบที่คำนึงถึงความสำคัญของการเคารพข้อ จำกัด ในระดับฐานข้อมูลโมเดลและ (ผู้ดูแลระบบ)!


4
คำถามที่ดี. ฉันยังอยากรู้ว่ามันเป็นไปได้ไหมที่จะตั้งค่าข้อ จำกัด ดังกล่าว ฉันรู้ว่าถ้าคุณทำให้มันเป็นข้อ จำกัด ที่ไม่เหมือนใครคุณจะได้แถวที่เป็นไปได้เพียงสองแถวในฐานข้อมูลของคุณ ;-)
Andre Miller

ไม่จำเป็น: หากคุณใช้ NullBooleanField คุณควรจะมี: (จริงเท็จจำนวน NULL เท่าไหร่ก็ได้)
Matthew Schinckel

ตามการวิจัยของฉัน , @sementeคำตอบคำนึงถึงความสำคัญของการเคารพข้อ จำกัด ในฐานข้อมูลและรูปแบบ (admin) ระดับรูปแบบในขณะที่มันยังมีทางออกที่ดีแม้สำหรับที่throughตารางManyToManyFieldที่ต้องการunique_togetherจำกัด
raratiru

คำตอบ:


66

เมื่อใดก็ตามที่ฉันต้องการทำงานนี้ให้สำเร็จสิ่งที่ฉันได้ทำคือลบล้างวิธีการบันทึกสำหรับโมเดลและให้ตรวจสอบว่าโมเดลอื่น ๆ มีการตั้งค่าสถานะไว้แล้วหรือไม่ (และปิด)

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
scytale

14
ฉันพยายามแก้ไขเพื่อลบความจำเป็นในการลอง / ยกเว้นและเพื่อให้กระบวนการมีประสิทธิภาพมากขึ้น แต่ถูกปฏิเสธ .. แทนที่จะติดget()ตั้งวัตถุอักขระแล้วsave()กลับเข้าไปใหม่คุณเพียงแค่ต้องกรองและอัปเดตซึ่งสร้างแบบสอบถาม SQL เพียงรายการเดียว และช่วยให้ 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

2
ฉันไม่สามารถแนะนำวิธีที่ดีกว่าในการทำงานนั้นให้สำเร็จได้ แต่ฉันอยากจะบอกว่าอย่าไว้ใจวิธีการบันทึกหรือล้างข้อมูลหากคุณใช้งานเว็บแอปพลิเคชันซึ่งคุณอาจต้องใช้คำขอสองสามข้อไปยังจุดสิ้นสุดในเวลาเดียวกัน คุณยังคงต้องใช้วิธีที่ปลอดภัยกว่าในระดับฐานข้อมูล
u.unver34

1
มีคำตอบที่ดีกว่าด้านล่าง คำตอบของ Ellis Percival ใช้transaction.atomicซึ่งมีความสำคัญที่นี่ นอกจากนี้ยังมีประสิทธิภาพมากขึ้นโดยใช้แบบสอบถามเดียว
alexbhandari

36

ฉันจะแทนที่วิธีการบันทึกของโมเดลและหากคุณตั้งค่าบูลีนเป็น 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)

ฉันพยายามแก้ไขคำตอบที่คล้ายกันของอดัม แต่ถูกปฏิเสธเนื่องจากเปลี่ยนคำตอบเดิมมากเกินไป วิธีนี้มีความกระชับและมีประสิทธิภาพมากกว่าเนื่องจากการตรวจสอบรายการอื่นทำได้ในแบบสอบถามเดียว


8
ฉันคิดว่านี่เป็นคำตอบที่ดีที่สุด แต่ฉันขอแนะนำให้รวมsaveเข้ากับ@transaction.atomicธุรกรรม เนื่องจากอาจเกิดขึ้นได้ที่คุณลบแฟล็กทั้งหมด แต่การบันทึกล้มเหลวและคุณจะไม่ได้เลือกอักขระทั้งหมด
Mitar

ขอบคุณที่พูดเช่นนั้น คุณพูดถูกแล้วเราจะอัปเดตคำตอบ
Ellis Percival

@ มิทาร์@transaction.atomicยังปกป้องจากสภาพการแข่งขัน
Pawel Furmaniak

2
ทางออกที่ดีที่สุดในบรรดาทั้งหมด!
Arturo

1
เกี่ยวกับ transaction.atomic ฉันใช้ตัวจัดการบริบทแทนมัณฑนากร ฉันไม่เห็นเหตุผลที่จะใช้การทำธุรกรรมปรมาณูในทุกรุ่นที่บันทึกไว้เพราะสิ่งนี้สำคัญก็ต่อเมื่อฟิลด์บูลีนเป็นจริง ฉันขอแนะนำให้ใช้with transaction.atomic:ภายในคำสั่ง if ควบคู่ไปกับการบันทึกภายใน if จากนั้นเพิ่มบล็อกอื่นและบันทึกในบล็อกอื่น
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
วิธีนี้ดูสะอาดกว่าวิธีอื่นมาก
pistache

2
ฉันชอบโซลูชันนี้เช่นกันแม้ว่าจะดูเหมือนว่าวัตถุอาจเป็นอันตรายได้อัปเดตตั้งค่าวัตถุอื่น ๆ ทั้งหมดเป็น False ในกรณีที่โมเดล UniqueBoolean เป็น True จะดียิ่งขึ้นหาก UniqueBooleanField ใช้อาร์กิวเมนต์ที่เป็นทางเลือกเพื่อระบุว่าควรตั้งค่าวัตถุอื่นเป็น False หรือไม่หรือควรเพิ่มข้อผิดพลาด (ทางเลือกอื่นที่สมเหตุสมผล) นอกจากนี้ให้ความคิดเห็นของคุณใน elif ซึ่งคุณต้องการตั้งค่าแอตทริบิวต์เป็น true ฉันคิดว่าคุณควรเปลี่ยน Return Trueเป็นsetattr(model_instance, self.attname, True)
Andrew Chase

2
UniqueBooleanField ไม่ซ้ำใครจริงๆเนื่องจากคุณสามารถมีค่า False ได้มากเท่าที่คุณต้องการ ไม่แน่ใจว่าชื่อที่ดีกว่าจะเป็นอย่างไร ... OneTrueBooleanField? สิ่งที่ฉันต้องการจริงๆคือสามารถกำหนดขอบเขตนี้ร่วมกับคีย์ต่างประเทศเพื่อที่ฉันจะได้มี BooleanField ที่ได้รับอนุญาตให้เป็น True เพียงครั้งเดียวต่อความสัมพันธ์ (เช่น CreditCard มีฟิลด์ "หลัก" และ FK ถึงผู้ใช้และ ชุดค่าผสมผู้ใช้ / หลักเป็น True หนึ่งครั้งต่อการใช้งาน) สำหรับกรณีนั้นฉันคิดว่าคำตอบของอดัมที่ลบล้างบันทึกจะตรงกว่าสำหรับฉัน
Andrew Chase

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 ให้น้อยที่สุด นอกจากนี้ยังสามารถอาจใช้ภายใน throughตารางของManyToManyFieldaunique_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 อย่างไรก็ตามฉันไม่แน่ใจว่านี่เป็นการใช้สัญญาณ - พูดอย่างเคร่งครัด - "เหมาะสม" หรือไม่เนื่องจากฉันไม่สามารถพิจารณาขั้นตอนนี้เป็นส่วนหนึ่งของ "แอปพลิเคชันแยกส่วน" ได้


ขอบคุณสำหรับรีวิว! ฉันได้อัปเดตคำตอบของฉันเล็กน้อยตามหนึ่งในความคิดเห็นในกรณีที่คุณต้องการอัปเดตรหัสของคุณที่นี่ด้วย
Ellis Percival

@EllisPercival ขอบคุณสำหรับคำใบ้! ฉันอัปเดตรหัสตามนั้น จำไว้ว่าโมเดลนั้น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.condition. Django 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

ใช้แนวทางที่คล้ายกันกับซาอูล แต่มีจุดประสงค์ที่แตกต่างกันเล็กน้อย:

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.