Załóżmy, że mój models.py jest taki:
class Character(models.Model):
name = models.CharField(max_length=255)
is_the_chosen_one = models.BooleanField()
Chcę, aby tylko jedna z moich Character
instancji miała, is_the_chosen_one == True
a wszystkie inne miały is_the_chosen_one == False
. Jak najlepiej zapewnić przestrzeganie tego ograniczenia wyjątkowości?
Najwyższe oceny za odpowiedzi, które uwzględniają konieczność przestrzegania ograniczeń na poziomie bazy danych, modelu i formularza (administratora)!
database
django
django-models
django-admin
django-forms
sampablokuper
źródło
źródło
through
tabeliManyToManyField
, która potrzebujeunique_together
ograniczenie.Odpowiedzi:
Zawsze, gdy potrzebowałem wykonać to zadanie, nadpisałem metodę zapisu modelu i kazałem mu sprawdzić, czy jakikolwiek inny model ma już ustawioną flagę (i wyłączyć ją).
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)
źródło
save(self)
na,save(self, *args, **kwargs)
ale edycja została odrzucona. Czy którykolwiek z recenzentów mógłby poświęcić trochę czasu, aby wyjaśnić dlaczego - ponieważ wydaje się to być zgodne z najlepszą praktyką Django.get()
obiekt Character, a następniesave()
ponownie, wystarczy filtrować i aktualizować, co daje tylko jedno zapytanie SQL i pomaga zachować spójność 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
to, co jest tutaj ważne. Jest to również bardziej wydajne przy użyciu pojedynczego zapytania.Zastąpiłbym metodę zapisu modelu i jeśli ustawiłeś wartość logiczną na True, upewnij się, że wszystkie inne są ustawione na 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)
Próbowałem zredagować podobną odpowiedź Adama, ale została ona odrzucona za zbyt dużą zmianę oryginalnej odpowiedzi. Ten sposób jest bardziej zwięzły i skuteczny, ponieważ sprawdzanie innych wpisów odbywa się w jednym zapytaniu.
źródło
save
do@transaction.atomic
transakcji. Ponieważ może się zdarzyć, że usuniesz wszystkie flagi, ale zapisanie się nie powiedzie i skończy się na tym, że wszystkie postacie nie zostaną wybrane.@transaction.atomic
chroni również przed stanem wyścigu.with transaction.atomic:
wewnątrz instrukcji if wraz z zapisaniem wewnątrz if. Następnie dodajemy kolejny blok i zapisujemy również w innym bloku.Zamiast korzystać z czyszczenia / zapisywania modelu niestandardowego, utworzyłem plik niestandardowe pole nadpisujące
pre_save
metodędjango.db.models.BooleanField
. Zamiast zgłaszać błąd, jeśli było inne poleTrue
, zrobiłem wszystkie inne pola,False
jeśli byłoTrue
. Zamiast zgłaszać błąd, jeśli pole byłoFalse
i żadne inne pole nie byłoTrue
, zapisałem je jako poleTrue
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)
źródło
Return True
nasetattr(model_instance, self.attname, True)
true
jakbyś usuwał jedynytrue
wiersz.Poniższe rozwiązanie jest trochę brzydkie, ale może działać:
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)
Jeśli ustawisz is_the_chosen_one na False lub None, zawsze będzie NULL. Możesz mieć NULL tyle, ile chcesz, ale możesz mieć tylko jedną True.
źródło
Próbując związać koniec z końcem z odpowiedziami tutaj, stwierdzam, że niektóre z nich odnoszą się z powodzeniem do tego samego problemu, a każdy jest odpowiedni w różnych sytuacjach:
Wybrałbym:
@semente : szanuje ograniczenie na poziomie bazy danych, modelu i formularza administratora, podczas gdy zastępuje Django ORM w najmniejszym możliwym stopniu. Co więcej, może
prawdopodobniebyć używane wewnątrzthrough
tabeliManyToManyField
wunique_together
sytuacji.(Sprawdzę to i zamelduję)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 : Uderza do bazy danych tylko jeden dodatkowy raz i akceptuje bieżący wpis jako wybrany. Czysty i elegancki.
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)
Inne rozwiązania nieodpowiednie dla mojego przypadku, ale wykonalne:
@nemocorp zastępuje
clean
metodę w celu przeprowadzenia walidacji. Jednak nie informuje, który model jest „tym”, a to nie jest przyjazne dla użytkownika. Mimo to jest to bardzo fajne podejście, zwłaszcza jeśli ktoś nie zamierza być tak agresywny jak @Flyte.@ saul.shanabrook i @Thierry J. utworzyliby niestandardowe pole, które zmieniłoby dowolny inny wpis „is_the_one” na
False
lub podniósłValidationError
. Jestem po prostu niechętny do wprowadzania nowych funkcji do mojej instalacji Django, chyba że jest to absolutnie konieczne.@daigorocub : używa sygnałów Django. Uważam, że jest to wyjątkowe podejście i podpowiada, jak używać sygnałów Django . Jednak nie jestem pewien, czy jest to - ściśle mówiąc - „właściwe” użycie sygnałów, ponieważ nie mogę traktować tej procedury jako części „aplikacji oddzielonej”.
źródło
save()
wypadek niepowodzenia operacji!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
Możesz użyć powyższego formularza również dla administratora, po prostu użyj
class CharacterAdmin(admin.ModelAdmin): form = CharacterForm admin.site.register(Character, CharacterAdmin)
źródło
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")
W ten sposób walidacja była dostępna w podstawowym formularzu administratora
źródło
Łatwiej jest dodać tego rodzaju ograniczenie do modelu po wersji 2.2 Django. Możesz bezpośrednio użyć
UniqueConstraint.condition
. Django DocsPo prostu zastąp swoje modele w
class Meta
ten sposób:class Meta: constraints = [ UniqueConstraint(fields=['is_the_chosen_one'], condition=Q(is_the_chosen_one=True), name='unique_is_the_chosen_one') ]
źródło
I to wszystko.
def save(self, *args, **kwargs): if self.default_dp: DownloadPageOrder.objects.all().update(**{'default_dp': False}) super(DownloadPageOrder, self).save(*args, **kwargs)
źródło
Stosując podobne podejście jak Saul, ale nieco inny cel:
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
Ta implementacja spowoduje podniesienie a
ValidationError
podczas próby zapisania innego rekordu z wartością True.Dodałem również
unique_for
argument, który można ustawić na dowolne inne pole w modelu, aby sprawdzić prawdziwą unikalność tylko dla rekordów o tej samej wartości, takich jak:class Phone(models.Model): user = models.ForeignKey(User) main = TrueUniqueBooleanField(unique_for='user', default=False)
źródło
Czy dostanę punkty za odpowiedź na moje pytanie?
problem polegał na tym, że znalazł się w pętli, naprawiony przez:
# 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()
źródło
Wypróbowałem niektóre z tych rozwiązań i skończyłem na innym, tylko ze względu na zwięzłość kodu (nie muszę nadpisywać formularzy ani zapisywać metody). Aby to zadziałało, pole nie może być unikalne w swojej definicji, ale sygnał zapewnia, że tak się stanie.
# 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)
źródło
Aktualizacja 2020, aby ułatwić początkującym:
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()
Oczywiście, jeśli chcesz, aby unikalna wartość logiczna miała wartość False, po prostu zamień każdą instancję True na False i odwrotnie.
źródło