Giá trị BooleanField duy nhất trong Django?


87

Giả sử models.py của tôi giống như vậy:

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

Tôi muốn chỉ một trong các phiên bản của tôi Characteris_the_chosen_one == Truevà tất cả các phiên bản khác có is_the_chosen_one == False. Làm cách nào để tôi có thể đảm bảo tốt nhất sự hạn chế về tính duy nhất này được tôn trọng?

Điểm cao nhất cho các câu trả lời có tính đến tầm quan trọng của việc tôn trọng các ràng buộc ở cấp cơ sở dữ liệu, mô hình và (quản trị) biểu mẫu!


4
Câu hỏi hay. Tôi cũng tò mò nếu có thể thiết lập một ràng buộc như vậy. Tôi biết rằng nếu bạn chỉ đơn giản đặt nó trở thành một ràng buộc duy nhất, bạn sẽ chỉ có hai hàng có thể có trong cơ sở dữ liệu của mình ;-)
Andre Miller

Không nhất thiết: nếu bạn sử dụng NullBooleanField, thì bạn sẽ có thể có: (Đúng, Sai, bất kỳ số NULL nào).
Matthew Schinckel

Theo nghiên cứu của tôi , câu trả lời @semente có tính đến tầm quan trọng của việc tôn trọng ràng buộc ở cấp cơ sở dữ liệu, mô hình và (quản trị viên) trong khi nó cung cấp một giải pháp tuyệt vời ngay cả đối với một throughbảng ManyToManyFieldcần có unique_togetherràng buộc.
raratiru

Câu trả lời:


66

Bất cứ khi nào tôi cần hoàn thành nhiệm vụ này, những gì tôi đã làm là ghi đè phương thức lưu cho mô hình và kiểm tra xem có mô hình nào khác đã đặt cờ hay không (và tắt nó đi).

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
Tôi chỉ muốn thay đổi 'def tiết kiệm (tự):' to: 'def tiết kiệm (bản thân, * args, kwargs **):'
Marek

8
Tôi đã cố chỉnh sửa điều này để thay đổi save(self)thành save(self, *args, **kwargs)nhưng chỉnh sửa đã bị từ chối. Có thể bất kỳ người đánh giá nào dành thời gian để giải thích lý do tại sao không - vì điều này có vẻ phù hợp với phương pháp hay nhất của Django.
scytale 22/10/12

14
Tôi đã cố chỉnh sửa để loại bỏ nhu cầu thử / ngoại trừ và để làm cho quá trình hiệu quả hơn nhưng nó đã bị từ chối .. Thay vì nhập get()đối tượng Ký tự rồi nhập save()lại, bạn chỉ cần lọc và cập nhật, điều này chỉ tạo ra một truy vấn SQL và giúp giữ cho DB nhất quán: 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
Tôi không thể đề xuất bất kỳ phương pháp nào tốt hơn để hoàn thành nhiệm vụ đó nhưng tôi muốn nói rằng, đừng bao giờ tin tưởng các phương pháp lưu hoặc sạch nếu bạn đang chạy một ứng dụng web mà bạn có thể đưa một số yêu cầu đến một điểm cuối cùng một lúc. Bạn vẫn phải thực hiện một cách an toàn hơn có thể ở cấp độ cơ sở dữ liệu.
u.unver 34

1
Có một câu trả lời tốt hơn bên dưới. Câu trả lời của Ellis Percival sử dụng transaction.atomicđiều quan trọng ở đây. Nó cũng hiệu quả hơn khi sử dụng một truy vấn duy nhất.
alexbhandari

33

Tôi sẽ ghi đè phương thức lưu của mô hình và nếu bạn đã đặt boolean thành True, hãy đảm bảo tất cả các phương thức khác được đặt thành 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)

Tôi đã thử chỉnh sửa câu trả lời tương tự của Adam, nhưng nó đã bị từ chối vì thay đổi quá nhiều câu trả lời ban đầu. Cách này ngắn gọn và hiệu quả hơn vì việc kiểm tra các mục nhập khác được thực hiện trong một truy vấn duy nhất.


7
Tôi nghĩ đây là câu trả lời tốt nhất, nhưng tôi khuyên bạn nên kết hợp savethành một @transaction.atomicgiao dịch. Bởi vì có thể xảy ra trường hợp bạn xóa tất cả cờ, nhưng sau đó lưu không thành công và bạn kết thúc với tất cả các ký tự không được chọn.
Mitar

Cảm ơn bạn đã nói như vậy. Bạn hoàn toàn đúng và tôi sẽ cập nhật câu trả lời.
Ellis Percival

@Mitar @transaction.atomiccũng bảo vệ khỏi tình trạng chủng tộc.
Pawel Furmaniak

1
Giải pháp tốt nhất trong số tất cả!
Arturo

1
Về giao dịch.atomic, tôi đã sử dụng trình quản lý ngữ cảnh thay vì trình trang trí. Tôi thấy không có lý do gì để sử dụng giao dịch nguyên tử trên mọi lưu mô hình vì điều này chỉ quan trọng nếu trường boolean là đúng. Tôi đề nghị sử dụng with transaction.atomic:bên trong câu lệnh if cùng với việc lưu bên trong if. Sau đó, thêm một khối khác và cũng lưu trong khối khác.
alexbhandari

29

Thay vì sử dụng làm sạch / lưu mô hình tùy chỉnh, tôi đã tạo một trường tùy chỉnh ghi đè pre_savephương thức trên django.db.models.BooleanField. Thay vì đưa ra lỗi nếu có trường khác True, tôi thực hiện tất cả các trường khác Falsenếu có True. Ngoài ra, thay vì đưa ra lỗi nếu trường này Falsevà không có trường nào khác True, tôi đã lưu trường đó dưới dạngTrue

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
Vẻ này xa hơn sạch hơn so với các phương pháp khác
pistache

2
Tôi cũng thích giải pháp này, mặc dù có vẻ nguy hiểm khi để object.update đặt tất cả các đối tượng khác thành False trong trường hợp mô hình UniqueBoolean là True. Sẽ tốt hơn nữa nếu UniqueBooleanField lấy một đối số tùy chọn để cho biết liệu các đối tượng khác có nên được đặt thành False hay không hoặc nếu một lỗi sẽ được nâng lên (giải pháp thay thế hợp lý khác). Ngoài ra, được đưa ra bình luận của bạn trong elif, nơi bạn muốn thiết lập các thuộc tính là true, tôi nghĩ bạn nên thay đổi Return Trueđểsetattr(model_instance, self.attname, True)
Andrew Chase

2
UniqueBooleanField không thực sự là duy nhất vì bạn có thể có nhiều giá trị Sai như bạn muốn. Bạn không chắc cái tên nào hay hơn sẽ là ... OneTrueBooleanField? Điều tôi thực sự muốn là có thể phạm vi điều này kết hợp với khóa ngoại để tôi có thể có BooleanField chỉ được phép là True một lần cho mỗi quan hệ (ví dụ: Thẻ tín dụng có trường "chính" và FK cho Người dùng và kết hợp Người dùng / Chính là Đúng một lần cho mỗi lần sử dụng). Đối với trường hợp đó, tôi nghĩ câu trả lời của Adam ghi đè lưu sẽ dễ hiểu hơn đối với tôi.
Andrew Chase

1
Cần lưu ý rằng phương pháp này cho phép bạn kết thúc ở trạng thái không có hàng nào được đặt như truethể bạn xóa truehàng duy nhất .
rblk

11

Giải pháp sau đây hơi xấu một chút nhưng có thể hoạt động:

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)

Nếu bạn đặt is_the_chosen_one thành Sai hoặc Không thì nó sẽ luôn là NULL. Bạn có thể có NULL bao nhiêu tùy thích, nhưng bạn chỉ có thể có một True.


1
Giải pháp đầu tiên tôi cũng nghĩ ra. NULL luôn là duy nhất nên bạn luôn có thể có một cột có nhiều hơn một NULL.
kaleissin

10

Cố gắng hoàn thành các câu trả lời ở đây, tôi thấy rằng một số trong số chúng giải quyết thành công cùng một vấn đề và mỗi câu trả lời phù hợp trong các tình huống khác nhau:

Tôi muốn chọn:

  • @semente : Tôn trọng hạn chế ở cấp cơ sở dữ liệu, mô hình và biểu mẫu quản trị trong khi nó ghi đè Django ORM ít nhất có thể. Hơn nữa nó có thểcó lẽđược sử dụng bên trong một throughbảng ManyToManyFieldtrong một unique_togethertình huống.(Tôi sẽ kiểm tra nó và báo cáo)

    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 : Chỉ truy cập cơ sở dữ liệu một lần nữa và chấp nhận mục nhập hiện tại là mục đã chọn. Sạch sẽ và thanh lịch.

    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)  
    

Các giải pháp khác không phù hợp với trường hợp của tôi nhưng khả thi:

@nemocorp đang ghi đè cleanphương thức để thực hiện xác thực. Tuy nhiên, nó không báo cáo lại mô hình nào là "một" và điều này không thân thiện với người dùng. Mặc dù vậy, đó là một cách tiếp cận rất hay, đặc biệt nếu ai đó không có ý định gây hấn như @Flyte.

@ saul.shanabrook@Thierry J. sẽ tạo một trường tùy chỉnh sẽ thay đổi bất kỳ mục nhập "is_the_one" nào khác lên Falsehoặc tăng a ValidationError. Tôi chỉ miễn cưỡng áp dụng các tính năng mới vào bản cài đặt Django của mình trừ khi nó thật sự cần thiết.

@daigorocub : Sử dụng tín hiệu Django. Tôi thấy đây là một cách tiếp cận độc đáo và đưa ra gợi ý về cách sử dụng Tín hiệu Django . Tuy nhiên, tôi không chắc liệu đây có phải là cách sử dụng tín hiệu "theo cách nói hạn chế-" thích hợp "hay không vì tôi không thể coi quy trình này là một phần của" ứng dụng được tách rời ".


Cảm ơn đã xem xét! Tôi đã cập nhật câu trả lời của mình một chút, dựa trên một trong các nhận xét, trong trường hợp bạn cũng muốn cập nhật mã của mình ở đây.
Ellis Percival

@EllisPercival Cảm ơn bạn đã gợi ý! Tôi đã cập nhật mã cho phù hợp. Hãy ghi nhớ rằng các model.Model.save () không trả về thứ gì đó.
raratiru

Tốt rồi. Nó chủ yếu chỉ để tiết kiệm có lợi nhuận đầu tiên trên đường dây của riêng mình. Phiên bản của bạn thực sự không chính xác, vì nó không bao gồm .save () trong giao dịch nguyên tử. Ngoài ra, nó phải là 'with transaction.atomic ():' để thay thế.
Ellis Percival

1
@EllisPercival OK, cảm ơn bạn! Thật vậy, chúng tôi cần mọi thứ quay trở lại, nếu save()hoạt động không thành công!
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

Bạn cũng có thể sử dụng biểu mẫu trên cho quản trị viên, chỉ cần sử dụng

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

Làm điều này làm cho xác thực có sẵn trong biểu mẫu quản trị cơ bản


4

Sẽ đơn giản hơn nếu bạn thêm loại ràng buộc này vào mô hình của bạn sau phiên bản Django 2.2. Bạn có thể trực tiếp sử dụng UniqueConstraint.condition. Django Docs

Chỉ cần ghi đè các mô hình của bạn class Metanhư thế này:

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

2

Và đó là tất cả.

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

2

Sử dụng cách tiếp cận tương tự như Sau-lơ, nhưng mục đích hơi khác:

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

Việc triển khai này sẽ tăng một ValidationErrorkhi cố gắng lưu một bản ghi khác với giá trị True.

Ngoài ra, tôi đã thêm unique_forđối số có thể được đặt thành bất kỳ trường nào khác trong mô hình, để chỉ kiểm tra tính duy nhất đúng cho các bản ghi có cùng giá trị, chẳng hạn như:

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

1

Tôi có nhận được điểm khi trả lời câu hỏi của mình không?

vấn đề là nó đã tìm thấy chính nó trong vòng lặp, được khắc phục bởi:

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

Không, không có điểm cho việc trả lời câu hỏi của chính bạn và chấp nhận câu trả lời đó. Tuy nhiên, có những điểm cần thực hiện nếu ai đó tán thành câu trả lời của bạn. :)
dandan78

Thay vào đó, bạn có chắc mình không cố ý trả lời câu hỏi của chính mình ở đây không? Về cơ bản, bạn và @sampablokuper có cùng câu hỏi
j_syk

1

Tôi đã thử một số giải pháp này và kết thúc với một giải pháp khác, chỉ vì lý do ngắn gọn của mã (không cần phải ghi đè các biểu mẫu hoặc phương thức lưu). Để điều này hoạt động, trường không thể là duy nhất trong định nghĩa của nó nhưng tín hiệu đảm bảo điều đó xảy ra.

# 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

Cập nhật năm 2020 để làm cho mọi thứ ít phức tạp hơn cho người mới bắt đầu:

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

Tất nhiên, nếu bạn muốn boolean duy nhất là False, bạn chỉ cần hoán đổi mọi trường hợp của True với False và ngược lại.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.