Nilai BooleanField unik di Django?


90

Misalkan models.py saya seperti ini:

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

Saya ingin hanya satu Charactercontoh saya yang dimiliki is_the_chosen_one == Truedan yang lainnya dimiliki is_the_chosen_one == False. Bagaimana cara terbaik untuk memastikan batasan keunikan ini dipatuhi?

Nilai tertinggi untuk jawaban yang memperhitungkan pentingnya menghormati batasan pada tingkat formulir database, model dan (admin)!


4
Pertanyaan bagus. Saya juga penasaran apakah mungkin membuat batasan seperti itu. Saya tahu bahwa jika Anda membuatnya menjadi kendala unik, Anda hanya akan mendapatkan dua kemungkinan baris dalam database Anda ;-)
Andre Miller

Belum tentu: jika Anda menggunakan NullBooleanField, maka Anda harus dapat memiliki: (True, A False, sejumlah NULL).
Matthew Schinckel

Menurut penelitian saya , jawaban @semente , memperhitungkan pentingnya menghormati batasan pada level database, model, dan form (admin) sementara itu memberikan solusi yang bagus bahkan untuk throughtabel ManyToManyFieldyang membutuhkan unique_togetherbatasan.
raratiru

Jawaban:


66

Setiap kali saya perlu menyelesaikan tugas ini, yang telah saya lakukan adalah mengganti metode penyimpanan untuk model dan memeriksanya apakah ada model lain yang benderanya sudah disetel (dan matikan).

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
Saya baru saja mengubah 'def save (self):' menjadi: 'def save (self, * args, ** kwargs):'
Marek

8
Saya mencoba untuk menyunting ini untuk perubahan save(self)ke save(self, *args, **kwargs)tapi mengedit itu ditolak. Dapatkah peninjau mana pun meluangkan waktu untuk menjelaskan mengapa - karena ini tampaknya konsisten dengan praktik terbaik Django.
scytale

14
Saya mencoba mengedit untuk menghilangkan kebutuhan untuk mencoba / kecuali dan untuk membuat proses lebih efisien tetapi ditolak .. Daripada get()menggunakan objek Karakter dan kemudian save()menggunakannya lagi, Anda hanya perlu memfilter dan memperbarui, yang hanya menghasilkan satu kueri SQL dan membantu menjaga konsistensi 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
Saya tidak dapat menyarankan metode yang lebih baik untuk menyelesaikan tugas itu tetapi saya ingin mengatakan bahwa, jangan pernah mempercayai metode simpan atau bersih jika Anda menjalankan aplikasi web yang mungkin Anda ambil beberapa permintaan ke titik akhir pada saat yang sama. Anda masih harus menerapkan cara yang lebih aman mungkin di tingkat database.
u.unver34

1
Ada jawaban yang lebih baik di bawah ini. Penggunaan jawaban Ellis Percival transaction.atomicyang penting di sini. Ini juga lebih efisien menggunakan satu kueri.
alexbhandari

36

Saya akan mengganti metode penyimpanan model dan jika Anda telah menyetel boolean ke True, pastikan semua yang lain disetel ke 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)

Saya mencoba mengedit jawaban serupa oleh Adam, tetapi ditolak karena terlalu banyak mengubah jawaban aslinya. Cara ini lebih ringkas dan efisien karena pemeriksaan entri lain dilakukan dalam satu kueri.


8
Saya rasa ini adalah jawaban terbaik, tapi saya akan menyarankan pembungkus savemenjadi @transaction.atomictransaksi. Karena bisa saja Anda menghapus semua bendera, tetapi kemudian penyimpanan gagal dan Anda berakhir dengan semua karakter tidak dipilih.
Mitar

Terima kasih sudah mengatakannya. Anda benar sekali dan saya akan memperbarui jawabannya.
Ellis Percival

@Mitar @transaction.atomicjuga melindungi dari kondisi balapan.
Pawel Furmaniak

2
Solusi terbaik di antara semuanya!
Arturo

1
Mengenai transaction.atomic saya menggunakan manajer konteks dan bukan dekorator. Saya tidak melihat alasan untuk menggunakan transaksi atom pada setiap model yang disimpan karena ini hanya penting jika bidang boolean benar. Saya sarankan menggunakan with transaction.atomic:di dalam pernyataan if bersama dengan menyimpan di dalam jika. Kemudian tambahkan blok else dan juga simpan di blok else.
alexbhandari

29

Alih-alih menggunakan pembersihan / penyimpanan model kustom, saya membuat kolom kustom yang menggantikan pre_savemetode tersebut django.db.models.BooleanField. Alih-alih memunculkan kesalahan jika ada bidang lain True, saya membuat semua bidang lain Falsejika ada True. Juga alih-alih memunculkan kesalahan jika bidang itu Falsedan tidak ada bidang lain True, saya menyimpannya sebagai bidangTrue

field.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
Ini terlihat jauh lebih bersih daripada metode lain
pistache

2
Saya menyukai solusi ini juga, meskipun tampaknya berpotensi berbahaya untuk memiliki objek.update mengatur semua objek lain ke False dalam kasus di mana model UniqueBoolean adalah True. Akan lebih baik jika UniqueBooleanField mengambil argumen opsional untuk menunjukkan apakah objek lain harus disetel ke False atau jika kesalahan harus dimunculkan (alternatif lain yang masuk akal). Juga, memberikan komentar Anda di elif, di mana Anda ingin menyetel atribut ke true, saya pikir Anda harus mengubah Return Truekesetattr(model_instance, self.attname, True)
Andrew Chase

2
UniqueBooleanField tidak benar-benar unik karena Anda dapat memiliki nilai False sebanyak yang Anda inginkan. Tidak yakin nama yang lebih baik untuk menjadi ... OneTrueBooleanField? Yang benar-benar saya inginkan adalah dapat mencakup ini dalam kombinasi dengan kunci asing sehingga saya dapat memiliki BooleanField yang hanya diizinkan menjadi True sekali per hubungan (misalnya, CreditCard memiliki bidang "utama" dan FK untuk Pengguna dan kombinasi Pengguna / Utama adalah Benar sekali per penggunaan). Untuk itu saya rasa jawaban Adam override save akan lebih lugas buat saya.
Andrew Chase

1
Perlu dicatat bahwa metode ini memungkinkan Anda berakhir dalam keadaan tanpa baris yang ditetapkan seolah- trueolah Anda menghapus satu-satunya truebaris.
rblk

11

Solusi berikut ini agak jelek tetapi mungkin berhasil:

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)

Jika Anda menyetel is_the_chosen_one ke False atau None, itu akan selalu NULL. Anda dapat memiliki NULL sebanyak yang Anda inginkan, tetapi Anda hanya dapat memiliki satu True.


1
Solusi pertama yang saya pikirkan juga. NULL selalu unik sehingga Anda selalu dapat memiliki kolom dengan lebih dari satu NULL.
kaleissin

10

Mencoba memenuhi kebutuhan dengan jawaban di sini, saya menemukan bahwa beberapa dari mereka berhasil mengatasi masalah yang sama dan masing-masing cocok dalam situasi yang berbeda:

Aku akan memilih:

  • @semente : Menghormati batasan pada tingkat bentuk basis data, model dan admin sementara itu menimpa Django ORM sekecil mungkin. Apalagi bisamungkindigunakan di dalam throughtabel a ManyToManyFielddalam suatu unique_togethersituasi.(Saya akan memeriksanya dan melaporkan)

    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 : Memukul database hanya satu kali ekstra dan menerima entri saat ini sebagai yang dipilih. Bersih dan elegan.

    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)  
    

Solusi lain tidak cocok untuk kasus saya tetapi dapat digunakan:

@ nemocorp mengganti cleanmetode untuk melakukan validasi. Namun, itu tidak melaporkan kembali model mana yang "satu" dan ini tidak ramah pengguna. Meskipun demikian, ini adalah pendekatan yang sangat bagus terutama jika seseorang tidak berniat untuk menjadi seagresif @Flyte.

@ saul.shanabrook dan @Thierry J. akan membuat bidang khusus yang akan mengubah entri "is_the_one" lainnya ke Falseatau meningkatkan a ValidationError. Saya hanya enggan untuk menerapkan fitur baru pada instalasi Django saya kecuali itu benar-benar diperlukan.

@daigorocub : Menggunakan sinyal Django. Saya menemukannya pendekatan unik dan memberi petunjuk tentang bagaimana menggunakan Sinyal Django . Namun saya tidak yakin apakah ini adalah penggunaan sinyal yang -tepatnya- "tepat" karena saya tidak dapat menganggap prosedur ini sebagai bagian dari "aplikasi terpisah".


Terima kasih atas reviewnya! Saya telah memperbarui sedikit jawaban saya, berdasarkan salah satu komentar, jika Anda ingin memperbarui kode Anda di sini juga.
Ellis Percival

@EllisPercival Terima kasih atas petunjuknya! Saya memperbarui kode yang sesuai. Ingatlah bahwa model.Model.save () tidak mengembalikan sesuatu.
raratiru

Tidak apa-apa. Ini sebagian besar hanya untuk menghemat pengembalian pertama di jalurnya sendiri. Versi Anda sebenarnya salah, karena tidak menyertakan .save () dalam transaksi atom. Plus, seharusnya 'with transaction.atomic ():' sebagai gantinya.
Ellis Percival

1
@EllisPercival Oke, terima kasih! Memang, kami membutuhkan semuanya dibatalkan, jika save()operasi gagal!
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

Anda juga dapat menggunakan formulir di atas untuk admin, cukup gunakan

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

Melakukan ini membuat validasi tersedia di formulir admin dasar


4

Lebih mudah menambahkan batasan ini ke model Anda setelah Django versi 2.2. Anda bisa langsung menggunakan UniqueConstraint.condition. Django Docs

Ganti saja model Anda class Metaseperti ini:

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

2

Dan itu saja.

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

2

Menggunakan pendekatan yang mirip dengan Saul, tetapi tujuan yang sedikit berbeda:

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

Implementasi ini akan memunculkan ValidationErrorketika mencoba untuk menyimpan record lain dengan nilai True.

Juga, saya telah menambahkan unique_forargumen yang dapat disetel ke bidang lain dalam model, untuk memeriksa keunikan-sebenarnya hanya untuk catatan dengan nilai yang sama, seperti:

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

1

Apakah saya mendapat poin untuk menjawab pertanyaan saya?

Masalahnya adalah ia menemukan dirinya sendiri dalam loop, diperbaiki oleh:

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

Tidak, tidak ada poin untuk menjawab pertanyaan Anda sendiri dan menerima jawaban itu. Namun, ada poin yang harus dibuat jika seseorang menyukai jawaban Anda. :)
dandan78

Apakah Anda yakin tidak bermaksud menjawab pertanyaan Anda sendiri di sini ? Pada dasarnya Anda dan @sampablokuper memiliki pertanyaan yang sama
j_syk

1

Saya mencoba beberapa solusi ini, dan berakhir dengan yang lain, hanya demi kode singkat (tidak perlu mengganti formulir atau menyimpan metode). Agar ini berfungsi, bidang tidak boleh unik dalam definisinya tetapi sinyal memastikan hal itu terjadi.

# 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

Pembaruan 2020 untuk membuat segalanya lebih mudah untuk pemula:

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

Tentu saja, jika Anda ingin boolean unik menjadi False, Anda hanya perlu menukar setiap instance True dengan False dan sebaliknya.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.