Dlaczego model.save () nie wywołuje funkcji full_clean ()?

150

Jestem po prostu ciekawy, czy ktoś wie, czy jest dobry powód, dla którego orm django nie wywołuje „full_clean” na modelu, chyba że jest on zapisywany jako część formularza modelu.

Zauważ, że full_clean () nie zostanie wywołana automatycznie po wywołaniu metody save () twojego modelu. Będziesz musiał wywołać to ręcznie, jeśli chcesz przeprowadzić jednoetapową weryfikację modelu dla własnych ręcznie utworzonych modeli. Pełny, czysty dokument django

(UWAGA: aktualizacja cytatu dla Django 1.6 ... poprzednie dokumenty django zawierały również zastrzeżenie dotyczące ModelForms.)

Czy istnieją dobre powody, dla których ludzie nie chcieliby takiego zachowania? Pomyślałem, że gdybyś poświęcił trochę czasu na dodanie walidacji do modelu, chciałbyś, aby weryfikacja była uruchamiana za każdym razem, gdy model jest zapisywany.

Wiem, jak sprawić, by wszystko działało poprawnie, szukam tylko wyjaśnienia.

Aaron
źródło
11
Bardzo dziękuję za to pytanie, powstrzymało mnie to od uderzenia głową o ścianę znacznie dłużej. Stworzyłem mixin, który może pomóc innym. Sprawdź sedno: gist.github.com/glarrain/5448253
glarrain
I w końcu używam sygnału, aby złapać pre_savehaczyk i zrobić full_cleanna wszystkich złapanych modelach.
Alfred Huang,

Odpowiedzi:

59

AFAIK, jest to spowodowane wsteczną kompatybilnością. Występują również problemy z ModelForms z wykluczonymi polami, modelami z wartościami domyślnymi, sygnałami pre_save () itp.

Źródła, którymi możesz być zainteresowany:

lqc
źródło
3
Najbardziej pomocny fragment (IMHO) z drugiego źródła: „Opracowanie„ automatycznej ”opcji walidacji, która jest zarówno wystarczająco prosta, aby faktycznie była użyteczna, jak i wystarczająco solidna, aby obsłużyć wszystkie skrajne przypadki, to - jeśli to w ogóle możliwe - znacznie więcej niż można osiągnąć w ramach czasowych 1.2. Dlatego na razie Django nie ma nic takiego i nie będzie tego miało w 1.2. Jeśli myślisz, że możesz sprawić, by działało w 1.3, najlepiej jest opracować propozycja, zawierająca przynajmniej przykładowy kod, wraz z wyjaśnieniem, jak zachować prostotę i niezawodność ”.
Josh
30

Ze względu na zgodność, automatyczne czyszczenie przy zapisywaniu nie jest włączone w jądrze django.

Jeśli rozpoczynamy nowy projekt i chcemy, aby domyślna savemetoda w modelu mogła wyczyścić się automatycznie, możemy użyć następującego sygnału, aby wyczyścić przed zapisaniem każdego modelu.

from django.dispatch import receiver
from django.db.models.signals import pre_save, post_save

@receiver(pre_save)
def pre_save_handler(sender, instance, *args, **kwargs):
    instance.full_clean()
Alfred Huang
źródło
2
Dlaczego jest to lepsze (lub gorsze) niż przesłonięcie metody save w pewnym BaseModel (z którego wszystkie inne odziedziczą), aby najpierw wywołać full_clean, a następnie super ()?
J__
7
Widzę dwa problemy z tym podejściem 1) w przypadku ModelForm metoda full_clean () byłaby wywoływana dwukrotnie: przez formularz i przez sygnał 2) Gdyby formularz wykluczał niektóre pola, nadal byłyby one sprawdzane przez sygnał.
mehmet
1
@mehmet Może więc dodasz je if send == somemodel, then exclude some fieldsdopre_save_handler
Simin Jie
4
Dla tych, którzy używają lub rozważają użycie tego podejścia: pamiętaj, że to podejście nie jest oficjalnie obsługiwane przez Django i nie będzie obsługiwane w przewidywalnej przyszłości (zobacz ten komentarz w narzędziu do śledzenia błędów Django: code.djangoproject.com/ticket/ 29655 # comment: 3 ), więc prawdopodobnie natkniesz się na pewne niedoskonałości, takie jak zatrzymywanie uwierzytelniania ( code.djangoproject.com/ticket/29655 ), jeśli włączysz walidację dla wszystkich modeli. Z takimi problemami będziesz musiał sobie radzić sam. Jednak nie ma lepszego podejścia atm.
Evgeny A.
2
Począwszy od Django 2.2.3 powoduje to problem z podstawowym systemem uwierzytelniania. Otrzymasz ValidationError: Session with this Session key already exists. Aby tego uniknąć, musisz dodać instrukcję if dla, sender in list_of_model_classesaby zapobiec nadpisywaniu przez sygnał domyślnych modeli uwierzytelniania Django. Zdefiniuj jak list_of_model_classeschcesz
Addison Klinke
15

Najprostszym sposobem wywołania full_cleanmetody jest po prostu przesłonięcie savemetody w model:

def save(self, *args, **kwargs):
    self.full_clean()
    return super(YourModel, self).save(*args, **kwargs)
M. Void
źródło
Dlaczego jest to lepsze (lub gorsze) niż używanie sygnału?
J__
6
Widzę dwa problemy z tym podejściem 1) w przypadku ModelForm metoda full_clean () byłaby wywoływana dwukrotnie: przez formularz i przez zapis 2) Jeśli formularz wyklucza niektóre pola, nadal byłyby one sprawdzane przez save.
mehmet
3

Zamiast wstawiać fragment kodu, który deklaruje odbiornik, możemy użyć aplikacji jako INSTALLED_APPSsekcji wsettings.py

INSTALLED_APPS = [
    # ...
    'django_fullclean',
    # your apps here,
]

Wcześniej może być konieczna instalacja django-fullcleanza pomocą PyPI:

pip install django-fullclean
Alfred Huang
źródło
13
Dlaczego miałbyś pip installjakąś aplikację z 4 wierszami kodu (sprawdź kod źródłowy ) zamiast pisać te wiersze samodzielnie?
David D.
Inna biblioteka, której sam nie wypróbowałem: github.com/danielgatis/django-smart-save
Flimm
2

Jeśli masz model, który chcesz mieć pewność, że ma co najmniej jedną relację FK, a nie chcesz go używać, null=Falseponieważ wymaga to ustawienia domyślnego FK (które byłyby danymi śmieciowymi), najlepszym sposobem, jaki wymyśliłem, jest aby dodać własne .clean()i .save()metody. .clean()podnosi błąd walidacji i .save()wywołuje czysty. W ten sposób integralność jest wymuszana zarówno z formularzy, jak i z innego kodu wywołującego, wiersza poleceń i testów. Bez tego nie ma (AFAICT) sposobu na napisanie testu, który zapewni, że model ma relację FK z konkretnie wybranym (nie domyślnym) innym modelem.

class Payer(models.Model):

    name = models.CharField(blank=True, max_length=100)
    # Nullable, but will enforce FK in clean/save:
    payer_group = models.ForeignKey(PayerGroup, null=True, blank=True,)

    def clean(self):
        # Ensure every Payer is in a PayerGroup (but only via forms)
        if not self.payer_group:
            raise ValidationError(
                {'payer_group': 'Each Payer must belong to a PayerGroup.'})

    def save(self, *args, **kwargs):
        self.full_clean()
        return super().save(*args, **kwargs)

    def __str__(self):
        return self.name
shacker
źródło
1

Komentując odpowiedź @Alfred Huang i komentuje ją. Można zablokować podpięcie pre_save do aplikacji, definiując listę klas w bieżącym module (models.py) i sprawdzając ją w zaczepie pre_save:

CUSTOM_CLASSES = [obj for name, obj in
        inspect.getmembers(sys.modules[__name__])
        if inspect.isclass(obj)]

@receiver(pre_save)
def pre_save_handler(sender, instance, **kwargs):
    if type(instance) in CUSTOM_CLASSES:
        instance.full_clean()
Peter Shannon
źródło