Django'da benzersiz BooleanField değeri?


90

Modeller.py'nin böyle olduğunu varsayalım:

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

CharacterÖrneklerimden yalnızca birinin sahip olmasını is_the_chosen_one == Trueve diğerlerinin sahip olmasını istiyorum is_the_chosen_one == False. Bu benzersizlik kısıtlamasına uyulduğundan en iyi nasıl emin olabilirim?

Veritabanı, model ve (yönetici) form düzeylerinde kısıtlamaya uymanın önemini dikkate alan yanıtlar için en iyi notlar!


4
İyi soru. Böyle bir kısıtlama kurmanın mümkün olup olmadığını da merak ediyorum. Bunu basitçe benzersiz bir kısıtlama yaparsanız, veritabanınızda yalnızca iki olası satırla karşılaşacağınızı biliyorum ;-)
Andre Miller

Mutlaka: Bir NullBooleanField kullanıyorsanız, şunlara sahip olmanız gerekir: (a True, a False, herhangi bir sayıda NULL).
Matthew Schinckel

Göre Araştırmalarım , @semente cevap, dikkate bile bir için mükemmel bir çözüm sağlarken veritabanı, model ve (yönetici) formu seviyelerinde kısıtlamayı saygı göstermenin önemini alır throughtablosunda ManyToManyFieldbir ihtiyacı olduğunu unique_togetherkısıtlamasını.
raratiru

Yanıtlar:


66

Bu görevi yerine getirmem gerektiğinde, yaptığım şey model için kaydetme yöntemini geçersiz kılmak ve başka bir modelde bayrağın önceden ayarlanmış olup olmadığını kontrol ettirmek (ve kapatmak).

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
Ben sadece 'def save (self):' ifadesini: 'def save (self, * args, ** kwargs):' olarak değiştirirdim
Marek

8
Olarak değiştirmek save(self)için bunu düzenlemeyi denedim, save(self, *args, **kwargs)ancak düzenleme reddedildi. Gözden geçirenlerden herhangi biri nedenini açıklamak için zaman ayırabilir mi - çünkü bu, Django'nun en iyi uygulamasıyla tutarlı görünebilir.
scytale

14
Deneme / hariç tutma ihtiyacını ortadan kaldırmak ve işlemi daha verimli hale getirmek için düzenlemeyi denedim, ancak reddedildi .. get()Character nesnesini alıp save()tekrar kullanmak yerine, yalnızca bir SQL sorgusu üreten filtreleme ve güncelleme yapmanız yeterlidir. ve DB'nin tutarlı kalmasına yardımcı olur: 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
Bu görevi yerine getirmek için daha iyi bir yöntem öneremem ama şunu söylemek istiyorum, aynı anda bir uç noktaya birkaç istek alabileceğiniz bir web uygulaması çalıştırıyorsanız, kaydetme veya temizleme yöntemlerine asla güvenmeyin. Yine de veritabanı düzeyinde daha güvenli bir yol uygulamalısınız.
u.unver34

1
Aşağıda daha iyi bir cevap var. Ellis Percival'in cevabı transaction.atomicburada önemli olanı kullanır . Ayrıca tek bir sorgu kullanarak daha verimlidir.
alexbhandari

36

Modelin kaydetme yöntemini geçersiz kılardım ve boole değerini True olarak ayarladıysanız, diğerlerinin False olarak ayarlandığından emin olun.

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)

Benzer cevabı Adam tarafından düzenlemeyi denedim, ancak orijinal cevabı çok fazla değiştirdiği için reddedildi. Bu yöntem, diğer girişlerin kontrolü tek bir sorguda yapıldığından daha kısa ve verimli olur.


8
Bunun en iyi cevap olduğunu düşünüyorum, ancak savebir @transaction.atomicişlemi tamamlamanızı öneririm . Çünkü tüm bayrakları kaldırırsınız, ancak daha sonra kaydetme başarısız olur ve sonuçta tüm karakterler seçilmemiştir.
Mitar

Bunu söylediğin için teşekkürler. Kesinlikle haklısın ve cevabı güncelleyeceğim.
Ellis Percival

@Mitar @transaction.atomicayrıca yarış koşullarından da korur.
Pawel Furmaniak

2
Hepsi arasında en iyi çözüm!
Arturo

1
Transaction.atomic ile ilgili olarak bir dekoratör yerine bağlam yöneticisini kullandım. Her modelde atomik işlem kullanmak için bir neden görmüyorum, çünkü bu yalnızca boole alanı doğruysa önemlidir. with transaction.atomic:If ifadesinin içinde, if içinde kaydetmeyi öneririm . Daha sonra başka bir blok eklemek ve ayrıca başka bloğa kaydetmek.
alexbhandari

29

Özel model temizleme / kaydetme kullanmak yerine , yöntemi geçersiz kılarak özel bir alan oluşturdum . Bunun yerine başka bir alan ise bir hata yükseltilmesi , tüm diğer alanları yapılmış imiş . Ayrıca alan varsa ve başka hiçbir alan olmadıysa bir hata oluşturmak yerine, alanı şu şekilde kaydettim: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
Bu, diğer yöntemlerden çok daha temiz görünüyor
pistache

2
UniqueBoolean'ın True olduğu durumda, objects.update'in diğer tüm nesneleri False olarak ayarlaması potansiyel olarak tehlikeli görünse de, bu çözümü de seviyorum. UniqueBooleanField, diğer nesnelerin False olarak ayarlanıp ayarlanmayacağını veya bir hatanın ortaya çıkması gerekip gerekmediğini (diğer mantıklı alternatif) belirtmek için isteğe bağlı bir bağımsız değişken alsaydı daha da iyi olurdu. Ayrıca, true özelliğini ayarlamak istediğiniz elif, sizin Yorum göz önüne alındığında, ben değiştirmek gerektiğini düşünüyorum Return Trueiçinsetattr(model_instance, self.attname, True)
Andrew Chase

2
UniqueBooleanField, istediğiniz kadar False değerine sahip olabileceğiniz için gerçekten benzersiz değildir. Daha iyi bir adın ne olacağından emin değil ... OneTrueBooleanField? Gerçekten istediğim, bunu bir yabancı anahtarla kombinasyon halinde kapsayabilmek, böylece ilişki başına yalnızca bir kez True olmasına izin verilen bir BooleanField'e sahip olabilmektir (örneğin, bir CreditCard'ın bir "birincil" alanı ve Kullanıcı için bir FK'si vardır ve Kullanıcı / Birincil kombinasyonu kullanım başına bir kez Doğru'dur). Bu durumda Adam'ın kurtarmayı geçersiz kılan cevabının benim için daha kolay olacağını düşünüyorum.
Andrew Chase

1
Bu yöntemin true, tek truesatırı siliyormuşsunuz gibi hiçbir satır ayarlanmamış bir duruma geçmenize izin verdiğine dikkat edilmelidir .
rblk

11

Aşağıdaki çözüm biraz çirkin ama işe yarayabilir:

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)

İs_the_chosen_one öğesini False veya None olarak ayarlarsanız, her zaman NULL olacaktır. NULL'a istediğiniz kadar sahip olabilirsiniz, ancak yalnızca bir True'ya sahip olabilirsiniz.


1
Ayrıca düşündüğüm ilk çözüm. NULL her zaman benzersizdir, böylece her zaman birden fazla NULL içeren bir sütuna sahip olabilirsiniz.
kaleissin

10

Buradaki cevaplarla sonları buluşturmaya çalışırken, bazılarının aynı konuyu başarıyla ele aldığını ve her birinin farklı durumlarda uygun olduğunu görüyorum:

Seçmek isterdim:

  • @semente : Mümkün olan en az Django ORM'yi geçersiz kılarken , veritabanı, model ve yönetici formu seviyelerindeki kısıtlamaya saygı duyar . Üstelik yapabilirmuhtemelenbir durumda bir throughtablo içinde kullanılabilir .ManyToManyFieldunique_together(Kontrol edip rapor edeceğim)

    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 : Veritabanına yalnızca bir kez daha vurur ve geçerli girişi seçilen giriş olarak kabul eder. Temiz ve zarif.

    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)  
    

Benim durumum için uygun olmayan ancak geçerli olan diğer çözümler:

@nemocorp , cleandoğrulama gerçekleştirme yöntemini geçersiz kılıyor . Bununla birlikte, hangi modelin "bir" olduğunu geri bildirmez ve bu kullanıcı dostu değildir. Buna rağmen, özellikle birisi @Flyte kadar agresif olma niyetinde değilse çok güzel bir yaklaşım.

@ saul.shanabrook ve @Thierry J. , başka herhangi bir "is_the_one" girişini değiştirecek Falseveya a ValidationError. Tamamen gerekli olmadıkça Django kurulumuma yeni özellikler eklemek konusunda isteksizim.

@daigorocub : Django sinyallerini kullanır. Bunu benzersiz bir yaklaşım buluyorum ve Django Sinyallerinin nasıl kullanılacağına dair bir ipucu veriyor . Bununla birlikte, bu prosedürü bir "ayrıştırılmış uygulamanın" bir parçası olarak değerlendiremediğim için, bunun -kısmen konuşmak gerekirse- "doğru" bir sinyal kullanımı olup olmadığından emin değilim.


İnceleme için teşekkürler! Kodunuzu burada da güncellemek istemeniz durumunda, yorumlardan birine dayanarak cevabımı biraz güncelledim.
Ellis Percival

@EllisPercival İpucu için teşekkürler! Kodu buna göre güncelledim. Model.Model.save () işlevinin bir şey döndürmediğini aklınızda bulundurun .
raratiru

Bu iyi. Çoğunlukla kendi çizgisinde ilk dönüşü elde etmek için. Atomik işlemde .save () 'yi içermediğinden, sürümünüz aslında yanlış. Artı, bunun yerine 'işlem.atomic () ile:' olmalıdır.
Ellis Percival

1
@EllisPercival Tamam, teşekkürler! Gerçekten de, save()işlem başarısız olursa her şeyin geri alınmasına ihtiyacımız var !
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

Yukarıdaki formu yönetici için de kullanabilirsiniz, sadece şunu kullanın:

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

Bunu yapmak, doğrulamayı temel yönetici formunda kullanılabilir hale getirdi


4

Django 2.2 sürümünden sonra modelinize bu tür bir kısıtlama eklemek daha kolaydır. Doğrudan kullanabilirsiniz UniqueConstraint.condition. Django Belgeleri

Modellerinizi class Metaşu şekilde geçersiz kılmanız yeterlidir :

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

2

Ve hepsi bu.

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

2

Saul ile benzer bir yaklaşım, ancak biraz farklı bir amaç kullanmak:

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

Bu uygulama ValidationError, True değerine sahip başka bir kaydı kaydetmeye çalışırken bir yükseltir .

Ayrıca, unique_foryalnızca aynı değere sahip kayıtlar için gerçek benzersizliği kontrol etmek için modeldeki başka herhangi bir alana ayarlanabilen bağımsız değişkeni ekledim , örneğin:

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

1

Soruma cevap verdiğim için puan alır mıyım?

sorun, kendini döngüde bulmasıydı, şu şekilde düzeltildi:

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

Hayır, kendi sorunuzu yanıtlayıp bu yanıtı kabul ettiğiniz için puan yok. Ancak, birisi cevabınızı olumlu oylarsa dikkat edilmesi gereken noktalar var. :)
dandan78

Onun yerine burada kendi sorunuzu cevaplamak istemediğinizden emin misiniz ? Temelde siz ve @sampablokuper aynı soruyu sordu
j_syk

1

Bu çözümlerden bazılarını denedim ve sadece kod kısalığı uğruna başka bir çözüm buldum (formları geçersiz kılmak veya yöntemi kaydetmek zorunda değilsiniz). Bunun işe yaraması için, alan tanımında benzersiz olamaz, ancak sinyal bunun olmasını sağlar.

# 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

Yeni başlayanlar için işleri daha az karmaşık hale getirmek için 2020 güncellemesi:

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

Elbette, benzersiz boolean'ın False olmasını istiyorsanız, her True ile False'ı değiştirirsiniz ve bunun tersi de geçerlidir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.