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 . 이 고유성 제약이 존중되는지 어떻게 가장 잘 확인할 수 있습니까?

데이터베이스, 모델 및 (관리자) 양식 수준에서 제약 조건 준수의 중요성을 고려한 답변에 대한 최고 점수!


4
좋은 질문. 그러한 제약을 설정할 수 있는지도 궁금합니다. 단순히 고유 한 제약 조건으로 만들면 데이터베이스에 가능한 행이 두
Andre Miller

반드시 그런 것은 아닙니다. NullBooleanField를 사용하는 경우 다음을 가질 수 있어야합니다 (True, False, 임의의 수의 NULL).
Matthew Schinckel

에 따르면 내 연구 , @semente 대답은, 계정으로 심지어위한 훌륭한 솔루션을 제공하면서 데이터베이스, 모델과 (관리자) 폼 수준에서 제약 조건을 존중의 중요성 소요 through의 테이블 ManyToManyField필요가있는 unique_together제약 조건을.
raratiru 16.07.07

답변:


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
try / except의 필요성을 제거하고 프로세스를보다 효율적으로 만들기 위해 편집을 시도했지만 거부되었습니다. get()Character 개체를 save()ing 한 다음 다시 ing 하는 대신 필터링하고 업데이트하면 하나의 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 2014 년

2
이 작업을 수행하는 더 좋은 방법을 제안 할 수는 없지만 동시에 끝점에 몇 가지 요청을받을 수있는 웹 응용 프로그램을 실행하는 경우 저장 또는 정리 방법을 신뢰하지 마십시오. 여전히 데이터베이스 수준에서 더 안전한 방법을 구현해야합니다.
u.unver34

1
아래에 더 나은 답변이 있습니다. Ellis Percival의 대답은 transaction.atomic여기서 중요한 사용 입니다. 또한 단일 쿼리를 사용하는 것이 더 효율적입니다.
alexbhandari

33

모델의 저장 방법을 재정의하고 부울을 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)

Adam의 유사한 답변을 편집하려고 시도했지만 원래 답변을 너무 많이 변경하여 거부되었습니다. 이 방법은 다른 항목의 검사가 단일 쿼리로 수행되므로보다 간결하고 효율적입니다.


7
이것이 최선의 대답이라고 생각하지만 트랜잭션으로 포장 save하는 것이 좋습니다 @transaction.atomic. 모든 플래그를 제거했지만 저장이 실패하고 모든 문자가 선택되지 않은 상태로 끝날 수 있기 때문입니다.
Mitar

그렇게 말해 주셔서 감사합니다. 당신이 절대적으로 옳으며 대답을 업데이트하겠습니다.
Ellis Percival 2016 년

@Mitar @transaction.atomic는 또한 경쟁 조건으로부터 보호합니다.
Pawel Furmaniak

1
무엇보다도 최고의 솔루션!
Arturo

1
transaction.atomic과 관련하여 데코레이터 대신 컨텍스트 관리자를 사용했습니다. 부울 필드가 참인 경우에만 중요하기 때문에 모든 모델 저장에서 원자 트랜잭션을 사용할 이유가 없습니다. with transaction.atomic:if 문 내부를 사용하고 if 내부에 저장하는 것이 좋습니다 . 그런 다음 else 블록을 추가하고 else 블록에도 저장합니다.
alexbhandari

29

사용자 지정 모델 정리 / 저장을 사용하는 대신 .NET에서 메서드를 재정의 하는 사용자 지정 필드를 만들었습니다 . 대신 다른 필드가 있다면 오류를 제기의 , 나는 다른 모든 필드를 만든 이 있다면 . 또한 필드가 있고 다른 필드가 없으면 오류를 발생시키는 대신 필드를 다음 과 같이 저장했습니다.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
다른 방법보다이 외모 훨씬 더 깨끗한
pistache

2
모델 UniqueBoolean이 True 인 경우 objects.update가 다른 모든 개체를 False로 설정하는 것이 잠재적으로 위험한 것처럼 보이지만이 솔루션도 마음에 듭니다. UniqueBooleanField가 다른 객체를 False로 설정해야하는지 또는 오류가 발생해야하는지 (다른 합리적인 대안)을 나타내는 선택적 인수를 취하면 더 좋을 것입니다. 또한,이 속성을 true로 설정할 ELIF, 귀하의 의견 주어, 나는 당신이 변화해야한다고 생각 Return Truesetattr(model_instance, self.attname, True)
앤드류 체이스

2
UniqueBooleanField는 원하는만큼 많은 False 값을 가질 수 있으므로 실제로 고유하지 않습니다. 더 나은 이름이 무엇인지 잘 모르겠습니다 ... OneTrueBooleanField? 내가 정말로 원하는 것은 외래 키와 조합하여 범위를 지정하여 관계 당 한 번만 True가되도록 허용 된 BooleanField를 가질 수 있도록하는 것입니다 (예 : CreditCard에는 "primary"필드와 FK to User 및 사용자 / 기본 조합은 사용 당 한 번 True입니다). 이 경우 저장을 무시하는 Adam의 대답이 더 간단 할 것이라고 생각합니다.
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 2013 년

10

여기에있는 답으로 끝을 맺으려고 노력하면서 그중 일부는 동일한 문제를 성공적으로 해결하고 각각 다른 상황에 적합하다는 것을 발견했습니다.

나는 선택할 것이다 :

  • @semente : 가능한 최소한의 Django ORM을 재정의하는 동안 데이터베이스, 모델 및 관리자 양식 수준의 제약 조건을 존중합니다. 또한 그것은 할 수 있습니다아마돌며 사용되는 through(A)의 테이블 ManyToManyFieldA의unique_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)  
    

내 경우에 적합하지 않지만 실행 가능한 다른 솔루션 :

@nemocorpclean유효성 검사를 수행하는 메서드를 재정의합니다 . 그러나 어떤 모델이 "하나"인지는보고하지 않으며 사용자 친화적이지 않습니다. 그럼에도 불구하고 특히 누군가 @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.condition. 장고 문서

다음 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

이 구현은 ValidationErrorTrue 값으로 다른 레코드를 저장하려고 할 때를 발생시킵니다.

또한 다음 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.