Tworzenie modelu z dwoma opcjonalnymi, ale jednym obowiązkowym kluczem obcym


9

Mój problem polega na tym, że mam model, który może wziąć jeden z dwóch kluczy obcych, aby powiedzieć, jaki to model. Chcę, aby zajęło to co najmniej jedno, ale nie jedno i drugie. Czy mogę nadal mieć ten sam model, czy powinienem podzielić go na dwa typy? Oto kod:

class Inspection(models.Model):
    InspectionID = models.AutoField(primary_key=True, unique=True)
    GroupID = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
    SiteID = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)

    @classmethod
    def create(cls, groupid, siteid):
        inspection = cls(GroupID = groupid, SiteID = siteid)
        return inspection

    def __str__(self):
        return str(self.InspectionID)

class InspectionReport(models.Model):
    ReportID = models.AutoField(primary_key=True, unique=True)
    InspectionID = models.ForeignKey('Inspection', on_delete=models.CASCADE, null=True)
    Date = models.DateField(auto_now=False, auto_now_add=False, null=True)
    Comment = models.CharField(max_length=255, blank=True)
    Signature = models.CharField(max_length=255, blank=True)

Problemem jest Inspectionmodel. Powinno to być powiązane z grupą lub witryną, ale nie z obydwoma. Obecnie przy tej konfiguracji potrzebuje obu.

Wolałbym nie trzeba podzielić to na dwie prawie identycznych modeli GroupInspectioni SiteInspectiontak każde rozwiązanie, które utrzymuje ją jako jeden model byłby idealny.


Być może lepiej jest tutaj zastosować podklasę. Można zrobić Inspectionklasę, a następnie do podklasy SiteInspectioni GroupInspectiondla niezarejestrowanych części -common.
Willem Van Onsem

Możliwe, że nie są ze sobą powiązane, ale unique=Trueczęść w polach FK oznacza, że Inspectionmoże istnieć tylko jedna instancja dla jednej danej GroupIDlub SiteIDinstancji - IOW, to relacja jeden do jednego, a nie jeden do wielu. Czy tego naprawdę chcesz?
bruno desthuilliers

„Obecnie przy takiej konfiguracji potrzebuje obu.” => technicznie nie robi - na poziomie bazy danych można ustawić oba klucze, jeden lub żaden z tych kluczy (z zastrzeżeniem wspomnianym powyżej). Tylko w przypadku korzystania z ModelForm (bezpośrednio lub przez administratora django) pola te zostaną oznaczone jako wymagane, a to dlatego, że nie przekazałeś argumentu „blank = True”.
bruno desthuilliers

@brunodesthuilliers Tak. Pomysł polega na tym, aby Inspectionbyć łącznikiem między Groupi Sitea InspectionID, a następnie mogę mieć wiele „inspekcji” w postaci InspectionReporttego jednego związku. Zrobiono to, aby łatwiej było sortować według Datewszystkich rekordów związanych z jednym Grouplub Site. Mam nadzieję, że to ma sens
CalMac

@ Cm0295 Obawiam się, że nie widzę sensu tego poziomu pośredniego - umieszczenie FK grupy / witryny bezpośrednio w InspectionReport daje dokładnie taką samą usługę AFAICT - filtruj swoje InspectionReports według odpowiedniego klucza (lub po prostu postępuj zgodnie z odwrotnym deskryptorem z witryny lub Grupuj), posortuj je według daty i gotowe.
bruno desthuilliers

Odpowiedzi:


5

Sugerowałbym, abyś dokonał takiej weryfikacji na sposób Django

przez przesłonięcie cleanmetody Django Model

class Inspection(models.Model):
    ...

    def clean(self):
        if <<<your condition>>>:
            raise ValidationError({
                    '<<<field_name>>>': _('Reason for validation error...etc'),
                })
        ...
    ...

Należy jednak pamiętać, że podobnie jak Model.full_clean (), metoda clean () modelu nie jest wywoływana po wywołaniu metody save () modelu. musi zostać wywołany ręcznie, aby sprawdzić poprawność danych modelu, lub można zastąpić metodę zapisu modelu, aby zawsze wywoływała metodę clean () przed uruchomieniem Modelmetody zapisu klasy


Innym rozwiązaniem, które może pomóc, jest użycie GenericRelations w celu zapewnienia pola polimorficznego, które odnosi się do więcej niż jednej tabeli, ale może się zdarzyć, że te tabele / obiekty mogą być używane zamiennie w projekcie systemu od samego początku.


2

Jak wspomniano w komentarzach, powodem, dla którego „przy tej konfiguracji wymaga obu” jest to, że zapomniałeś dodać blank=Truepola FK, więc twoje ModelForm(niestandardowe lub domyślne wygenerowane przez administratora) spowoduje, że pole formularza będzie wymagane . Na poziomie schematu db można wypełnić oba, jeden lub żaden z tych FK, byłoby dobrze, ponieważ te pola db zostały dopuszczone do null (z null=Trueargumentem).

Ponadto (porównaj moje inne komentarze), możesz chcieć sprawdzić, czy naprawdę chcesz, aby FK były wyjątkowe. Technicznie zmienia to relację jeden do wielu w relację jeden do jednego - dozwolony jest tylko jeden rekord „inspekcji” dla danego GroupID lub SiteId (nie można mieć dwóch lub więcej „inspekcji” dla jednego GroupId lub SiteId) . Jeśli jest to NAPRAWDĘ to, czego chcesz, możesz zamiast tego użyć jawnego OneToOneField (schemat db będzie taki sam, ale model będzie bardziej wyraźny, a powiązany deskryptor znacznie bardziej użyteczny w tym przypadku użycia).

Na marginesie: w modelu Django pole ForeignKey materializuje się jako instancja modelu pokrewnego, a nie jako surowy identyfikator. IOW, biorąc pod uwagę to:

class Foo(models.Model):
    name = models.TextField()

class Bar(models.Model):
    foo = models.ForeignKey(Foo)


foo = Foo.objects.create(name="foo")
bar = Bar.objects.create(foo=foo)

wtedy bar.foozdecyduje się na foo, a nie na foo.id. Więc na pewno chcesz zmienić nazwę pola InspectionIDi SiteIDpola na właściwe inspectioni site. BTW, w Pythonie konwencja nazewnictwa to „all_lower_with_underscores” dla wszystkiego innego niż nazwy klas i pseudo-stałe.

Teraz najważniejsze pytanie: nie ma określonego standardowego sposobu SQL narzucenia ograniczenia „jedno lub drugie” na poziomie bazy danych, więc zwykle odbywa się to za pomocą ograniczenia CHECK , które jest wykonywane w modelu Django z meta „więzami” modelu opcja .

Biorąc to pod uwagę, sposób, w jaki ograniczenia są faktycznie obsługiwane i egzekwowane na poziomie db, zależy od twojego dostawcy DB (MySQL <8.0.16 po prostu zignoruj ​​je na przykład), a rodzaj ograniczenia, którego będziesz potrzebować tutaj , nie będzie egzekwowany w formularzu lub sprawdzanie poprawności na poziomie modelu , tylko przy próbie zapisania modelu, dlatego też chcesz dodać sprawdzanie poprawności na poziomie modelu (najlepiej) lub sprawdzanie poprawności na poziomie formularza, w obu przypadkach w (odpowiednio) modelu lub clean()metodzie formularza .

Krótko mówiąc:

  • najpierw dwukrotnie sprawdź, czy naprawdę chcesz tego unique=Trueograniczenia, a jeśli tak, zamień swoje pole FK na OneToOneField.

  • dodaj blank=Trueargument do obu pól FK (lub OneToOne)

  • dodaj odpowiednie ograniczenie sprawdzające do meta twojego modelu - dokument jest poprawny, ale nadal wystarczająco wyraźny, jeśli wiesz, że możesz wykonywać złożone zapytania z ORM (a jeśli nie, czas się uczyć ;-))
  • dodaj clean()metodę do swojego modelu, która sprawdza, czy masz jedno lub drugie pole i podnosi inny błąd sprawdzania poprawności

i powinieneś być w porządku, zakładając, że RDBMS oczywiście przestrzega ograniczeń sprawdzania.

Pamiętaj tylko, że dzięki tej konstrukcji Twój Inspectionmodel jest całkowicie bezużyteczną (ale kosztowną!) Pośrednią - uzyskasz dokładnie te same funkcje przy niższym koszcie, przenosząc FK (i ograniczenia, walidację itp.) Bezpośrednio do InspectionReport.

Teraz może istnieć inne rozwiązanie - zachowaj model Inspection, ale umieść FK jako OneToOneField na drugim końcu relacji (w witrynie i grupie):

class Inspection(models.Model):
    id = models.AutoField(primary_key=True) # a pk is always unique !

class InspectionReport(models.Model):
    # you actually don't need to manually specify a PK field,
    # Django will provide one for you if you don't
    # id = models.AutoField(primary_key=True)

    inspection = ForeignKey(Inspection, ...)
    date = models.DateField(null=True) # you should have a default then
    comment = models.CharField(max_length=255, blank=True default="")
    signature = models.CharField(max_length=255, blank=True, default="")


class Group(models.Model):
    inspection = models.OneToOneField(Inspection, null=True, blank=True)

class Site(models.Model):
    inspection = models.OneToOneField(Inspection, null=True, blank=True)

Następnie możesz uzyskać wszystkie raporty dla danej witryny lub grupy yoursite.inspection.inspectionreport_set.all().

Pozwala to uniknąć konieczności dodawania jakichkolwiek konkretnych ograniczeń lub sprawdzania poprawności, ale kosztem dodatkowego poziomu pośredniego ( joinklauzula SQL itp.).

To, które z tych rozwiązań będzie „najlepsze”, naprawdę zależy od kontekstu, więc musisz zrozumieć implikacje obu i sprawdzić, w jaki sposób zazwyczaj używasz swoich modeli, aby dowiedzieć się, które jest bardziej odpowiednie dla twoich potrzeb. Jeśli chodzi o mnie i bez większego kontekstu (lub wątpliwości) wolałbym używać rozwiązania z mniejszymi poziomami pośrednimi, ale YMMV.

Uwaga: ogólne relacje: mogą się przydać, gdy naprawdę masz wiele możliwych powiązanych modeli i / lub nie wiesz z góry, które modele chcesz odnosić do swoich. Jest to szczególnie przydatne w przypadku aplikacji wielokrotnego użytku (pomyśl „komentarze” lub „tagi” itp.) Lub rozszerzalnych (ramy zarządzania treścią itp.). Minusem jest to, że sprawia, że ​​zapytania są znacznie cięższe (i raczej niepraktyczne, gdy chcesz wykonywać zapytania ręczne na db). Z doświadczenia mogą szybko stać się kodem / kodem i perfem bota PITA, więc lepiej je zachować, gdy nie ma lepszego rozwiązania (i / lub gdy narzut związany z konserwacją i czasem wykonywania nie stanowi problemu).

Moje 2 centy.


2

Django ma nowy (od 2.2) interfejs do tworzenia ograniczeń DB: https://docs.djangoproject.com/en/3.0/ref/models/constraints/

Możesz użyć a, CheckConstraintaby wymusić, że jeden i jedyny jest inny niż null. Dla jasności używam dwóch:

class Inspection(models.Model):
    InspectionID = models.AutoField(primary_key=True, unique=True)
    GroupID = models.OneToOneField('PartGroup', on_delete=models.CASCADE, blank=True, null=True)
    SiteID = models.OneToOneField('Site', on_delete=models.CASCADE, blank=True, null=True)

    class Meta:
        constraints = [
            models.CheckConstraint(
                check=~Q(SiteID=None) | ~Q(GroupId=None),
                name='at_least_1_non_null'),
            ),
            models.CheckConstraint(
                check=Q(SiteID=None) | Q(GroupId=None),
                name='at_least_1_null'),
            ),
        ]

Wymusi to tylko ograniczenie na poziomie bazy danych. Konieczne będzie ręczne sprawdzenie poprawności danych wejściowych w formularzach lub serializatorach.

Na marginesie, prawdopodobnie powinieneś użyć OneToOneFieldzamiast ForeignKey(unique=True). Będziesz także chciał blank=True.


0

Myślę, że mówisz o ogólnych stosunkach , dokumentach . Twoja odpowiedź wygląda podobnie do tej .

Jakiś czas temu musiałem używać relacji rodzajowych, ale czytałem w książce i gdzieś indziej, że należy tego unikać, myślę, że to były dwie miarki Django.

Ostatecznie stworzyłem taki model:

class GroupInspection(models.Model):
    InspectionID = models.ForeignKey..
    GroupID = models.ForeignKey..

class SiteInspection(models.Model):
    InspectionID = models.ForeignKey..
    SiteID = models.ForeignKey..

Nie jestem pewien, czy jest to dobre rozwiązanie i jak wspomniałeś, wolisz go nie używać, ale w moim przypadku to działa.


„Czytam w książce i gdzieś indziej” dotyczy najgorszego możliwego powodu (lub uniknięcia) zrobienia czegoś.
bruno desthuilliers

@brunodesthuilliers Myślałem, że Two Scoops of Django to dobra książka.
Luis Silva

Nie mogę powiedzieć, nie przeczytałem tego. Ale to nie ma związku: chodzi mi o to, że jeśli nie rozumiesz, dlaczego książka tak mówi, to nie jest to wiedza ani doświadczenie, tylko wiara religijna. Nie mam nic przeciwko wierze religijnej, jeśli chodzi o religię, ale nie ma dla nich miejsca w CS. Albo rozumiesz, jakie są zalety i wady niektórych funkcji, a następnie możesz ocenić, czy jest to właściwe w danym kontekście , czy nie, a wtedy nie powinieneś bezmyślnie papugować tego, co przeczytałeś. Istnieją bardzo uzasadnione przypadki użycia dla ogólnych relacji, nie chodzi o to, aby ich w ogóle unikać, ale wiedzieć, kiedy należy ich unikać.
bruno desthuilliers

NB Doskonale rozumiem, że nie można wiedzieć wszystkiego o CS - są domeny, w których nie mam innych opcji niż ufać jakiejś książce. Ale wtedy prawdopodobnie nie odpowiem na pytania na ten temat ;-)
bruno desthuilliers

0

Może być późno na odpowiedź na twoje pytanie, ale pomyślałem, że moje rozwiązanie może pasować do przypadku innej osoby.

Stworzyłbym nowy model, nazwijmy go Dependencyi zastosuję logikę w tym modelu.

class Dependency(models.Model):
    Group = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
    Site = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)

Następnie napisałbym logikę, aby można ją było zastosować bardzo wyraźnie.

class Dependency(models.Model):
    group = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
    site = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)

    _is_from_custom_logic = False

    @classmethod
    def create_dependency_object(cls, group=None, site=None):
        # you can apply any conditions here and prioritize the provided args
        cls._is_from_custom_logic = True
        if group:
            _new = cls.objects.create(group=group)
        elif site:
            _new = cls.objects.create(site=site)
        else:
            raise ValueError('')
        return _new

    def save(self, *args, **kwargs):
        if not self._is_from_custom_logic:
            raise Exception('')
        return super().save(*args, **kwargs)

Teraz wystarczy utworzyć singiel ForeignKeydo swojego Inspectionmodelu.

W swoich viewfunkcjach musisz utworzyć Dependencyobiekt, a następnie przypisać go do swojego Inspectionrekordu. Upewnij się, że używasz create_dependency_objectw swoich viewfunkcjach.

To sprawia, że ​​Twój kod jest wyraźny i odporny na błędy. Egzekucję można zbyt łatwo ominąć. Chodzi jednak o to, że trzeba ominąć tę dokładną wiedzę, aby ominąć to dokładne ograniczenie.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.