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 Character
contoh saya yang dimiliki is_the_chosen_one == True
dan 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)!
through
tabelManyToManyField
yang membutuhkanunique_together
batasan.Jawaban:
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)
sumber
save(self)
kesave(self, *args, **kwargs)
tapi mengedit itu ditolak. Dapatkah peninjau mana pun meluangkan waktu untuk menjelaskan mengapa - karena ini tampaknya konsisten dengan praktik terbaik Django.get()
menggunakan objek Karakter dan kemudiansave()
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)
transaction.atomic
yang penting di sini. Ini juga lebih efisien menggunakan satu kueri.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.
sumber
save
menjadi@transaction.atomic
transaksi. Karena bisa saja Anda menghapus semua bendera, tetapi kemudian penyimpanan gagal dan Anda berakhir dengan semua karakter tidak dipilih.@transaction.atomic
juga melindungi dari kondisi balapan.with transaction.atomic:
di dalam pernyataan if bersama dengan menyimpan di dalam jika. Kemudian tambahkan blok else dan juga simpan di blok else.Alih-alih menggunakan pembersihan / penyimpanan model kustom, saya membuat kolom kustom yang menggantikan
pre_save
metode tersebutdjango.db.models.BooleanField
. Alih-alih memunculkan kesalahan jika ada bidang lainTrue
, saya membuat semua bidang lainFalse
jika adaTrue
. Juga alih-alih memunculkan kesalahan jika bidang ituFalse
dan tidak ada bidang lainTrue
, 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)
sumber
Return True
kesetattr(model_instance, self.attname, True)
true
olah Anda menghapus satu-satunyatrue
baris.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.
sumber
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 bisa
mungkindigunakan di dalamthrough
tabel aManyToManyField
dalam suatuunique_together
situasi.(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
clean
metode 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
False
atau meningkatkan aValidationError
. 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".
sumber
save()
operasi gagal!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)
sumber
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
sumber
Lebih mudah menambahkan batasan ini ke model Anda setelah Django versi 2.2. Anda bisa langsung menggunakan
UniqueConstraint.condition
. Django DocsGanti saja model Anda
class Meta
seperti ini:class Meta: constraints = [ UniqueConstraint(fields=['is_the_chosen_one'], condition=Q(is_the_chosen_one=True), name='unique_is_the_chosen_one') ]
sumber
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)
sumber
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
ValidationError
ketika mencoba untuk menyimpan record lain dengan nilai True.Juga, saya telah menambahkan
unique_for
argumen 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)
sumber
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()
sumber
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)
sumber
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.
sumber