Erstellen eines Modells mit zwei optionalen, aber einem obligatorischen Fremdschlüssel

9

Mein Problem ist, dass ich ein Modell habe, das einen von zwei Fremdschlüsseln verwenden kann, um zu sagen, um welche Art von Modell es sich handelt. Ich möchte, dass es mindestens einen braucht, aber nicht beide. Kann ich dieses Modell noch als ein Modell haben oder sollte ich es in zwei Typen aufteilen? Hier ist der Code:

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)

Das Problem ist das InspectionModell. Dies sollte entweder mit einer Gruppe oder einer Site verknüpft sein, jedoch nicht mit beiden. Derzeit benötigt dieses Setup beides.

Ich hätte lieber nicht aufgeteilt in zwei fast identische Modelle GroupInspectionund SiteInspection, so dass jede Lösung , die es als ein Modell hält wäre ideal.

CalMac
quelle
Vielleicht ist es hier besser, Unterklassen zu verwenden. Sie können eine InspectionKlasse erstellen und dann eine Unterklasse in SiteInspectionund GroupInspectionfür die nicht üblichen Teile erstellen.
Willem Van Onsem
Möglicherweise nicht verwandt, aber der unique=TrueTeil in Ihren FK-Feldern bedeutet, dass nur eine InspectionInstanz für eine bestimmte GroupIDoder SiteIDInstanz existieren kann - IOW, es ist eine Eins-zu-Eins-Beziehung, keine Eins-zu-Viele-Beziehung. Willst du das wirklich?
Bruno Desthuilliers
"Derzeit benötigt es mit diesem Setup beides." => Technisch gesehen nicht - auf Datenbankebene können Sie entweder beide oder einen dieser Schlüssel festlegen (mit der oben genannten Einschränkung). Nur wenn Sie ein ModelForm verwenden (direkt oder über django admin), werden diese Felder als erforderlich markiert, und das liegt daran, dass Sie das Argument 'blank = True' nicht übergeben haben.
Bruno Desthuilliers
@brunodesthuilliers Ja, die Idee ist, Inspectioneine Verbindung zwischen dem Groupoder Siteund einem zu haben InspectionID, dann kann ich mehrere "Inspektionen" in Form von InspectionReportfür diese eine Beziehung durchführen. Dies wurde gemacht, damit ich leichter Datenach allen Datensätzen sortieren kann, die sich auf einen Groupoder mehrere beziehen Site. Hoffe das macht Sinn
CalMac
@ Cm0295 Ich fürchte, ich verstehe den Sinn dieser Indirektionsebene nicht - wenn Sie die Gruppen- / Site-FKs direkt in InspectionReport einfügen, erhalten Sie genau den gleichen Service AFAICT - filtern Sie Ihre InspectionReports nach dem entsprechenden Schlüssel (oder folgen Sie einfach dem umgekehrten Deskriptor von Site oder Gruppe), sortiere sie nach Datum und du bist fertig.
Bruno Desthuilliers

Antworten:

5

Ich würde vorschlagen, dass Sie eine solche Validierung nach Django-Art durchführen

durch Überschreiben der cleanMethode des Django-Modells

class Inspection(models.Model):
    ...

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

Beachten Sie jedoch, dass wie bei Model.full_clean () die clean () -Methode eines Modells nicht aufgerufen wird, wenn Sie die save () -Methode Ihres Modells aufrufen. Es muss manuell aufgerufen werden, um die Modelldaten zu validieren, oder Sie können die Speichermethode des Modells überschreiben, damit es immer die clean () -Methode aufruft, bevor die ModelKlassensparmethode ausgelöst wird


Eine andere Lösung , die hilfreich sein könnte, ist die Verwendung von GenericRelations , um ein polymorphes Feld bereitzustellen, das sich auf mehr als eine Tabelle bezieht. Dies kann jedoch der Fall sein, wenn diese Tabellen / Objekte im Systemdesign von Anfang an austauschbar verwendet werden können.

Radwan Abu-Odeh
quelle
2

Wie in den Kommentaren erwähnt, besteht der Grund dafür, dass "bei dieser Einrichtung beides benötigt wird" darin, dass Sie vergessen haben, das blank=Truezu Ihren FK-Feldern hinzuzufügen , sodass Ihr ModelForm(entweder benutzerdefiniertes oder vom Administrator generiertes Standardfeld) das Formularfeld erforderlich macht . Auf der Ebene des Datenbankschemas können Sie beide füllen, entweder eine oder keine dieser FKs. Dies wäre in Ordnung, da Sie diese Datenbankfelder nullbar gemacht haben (mit dem null=TrueArgument).

Außerdem (siehe meine anderen Kommentare) möchten Sie möglicherweise überprüfen, ob Ihre FKs wirklich einzigartig sein sollen. Dies macht Ihre Eins-zu-Viele-Beziehung technisch zu einer Eins-zu-Eins-Beziehung. Sie dürfen nur einen einzigen "Inspektions" -Datensatz für eine bestimmte GroupID oder SiteId verwenden (Sie können nicht zwei oder mehr "Inspektionen" für eine GroupId oder SiteId durchführen.) . Wenn dies WIRKLICH das ist, was Sie wollen, können Sie stattdessen ein explizites OneToOneField verwenden (das Datenbankschema ist das gleiche, aber das Modell ist expliziter und der zugehörige Deskriptor ist für diesen Anwendungsfall viel benutzerfreundlicher).

Als Randnotiz: In einem Django-Modell wird ein ForeignKey-Feld als verwandte Modellinstanz und nicht als unformatierte ID angezeigt. IOW, vorausgesetzt:

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)

dann bar.foowird sich auflösen foo, nicht auf foo.id. Sie möchten also auf jeden Fall Ihre InspectionIDund SiteIDFelder in richtig inspectionund umbenennen site. Übrigens lautet die Namenskonvention in Python 'all_lower_with_underscores' für alles andere als Klassennamen und Pseudokonstanten.

Nun zu Ihrer Kernfrage: Es gibt keine spezielle Standard-SQL-Methode zum Erzwingen einer "die eine oder andere" Einschränkung auf Datenbankebene. Daher wird normalerweise eine CHECK-Einschränkung verwendet , die in einem Django-Modell mit den Meta-Einschränkungen des Modells durchgeführt wird. Option .

Wie Einschränkungen auf DB-Ebene tatsächlich unterstützt und durchgesetzt werden, hängt jedoch von Ihrem DB-Anbieter ab (MySQL <8.0.16 ignoriert sie beispielsweise einfach ), und die Art der Einschränkungen, die Sie hier benötigen, wird im Formular nicht durchgesetzt oder Validierung auf Modellebene , nur wenn Sie versuchen, das Modell zu speichern. Sie möchten also auch eine Validierung entweder auf Modellebene hinzufügen (vorzugsweise) oder Formularebene Validierung, in beiden Fällen in dem (Ltg.) Modell oder clean()Formularmethode.

Um es kurz zu machen:

  • Überprüfen Sie zunächst, ob Sie dies wirklich möchten unique=True Einschränkung Wenn ja, ersetzen Sie Ihr FK-Feld durch ein OneToOneField.

  • füge hinzu ein blank=True arg sowohl Ihre FK (oder OneToOne) Felder

  • füge das richtige hinzu Check - Einschränkung in Meta Ihrem Modell - die Doc succint ist aber immer noch deutlich genug , wenn Sie wissen , komplexe Abfragen mit dem ORM zu tun (und wenn Sie ihm Zeit , dass du nicht lernen ;-))
  • Fügen Sie clean()Ihrem Modell eine Methode hinzu, die überprüft, ob Sie das eine oder das andere Feld haben, und ansonsten einen Validierungsfehler auslöst

und Sie sollten in Ordnung sein, vorausgesetzt, Ihr RDBMS berücksichtigt natürlich die Überprüfungsbeschränkungen.

Beachten Sie nur, dass Ihr InspectionModell bei diesem Design eine völlig nutzlose (und dennoch kostspielige!) Indirektion ist. Sie erhalten genau dieselben Funktionen zu geringeren Kosten, wenn Sie die FKs (und Einschränkungen, Validierung usw.) direkt in verschiebenInspectionReport .

Jetzt gibt es möglicherweise eine andere Lösung: Behalten Sie das Inspektionsmodell bei, aber platzieren Sie die FK als OneToOneField am anderen Ende der Beziehung (in Site und Gruppe):

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)

Und dann können Sie alle Berichte für eine bestimmte Site oder Gruppe mit abrufen yoursite.inspection.inspectionreport_set.all() .

Dadurch wird vermieden, dass eine bestimmte Einschränkung oder Validierung hinzugefügt werden muss, jedoch auf Kosten einer zusätzlichen Indirektionsebene (SQL) join Klausel usw.).

Welche dieser Lösungen "die beste" ist, hängt wirklich vom Kontext ab. Sie müssen also die Auswirkungen beider verstehen und überprüfen, wie Sie Ihre Modelle normalerweise verwenden, um herauszufinden, welche für Ihre eigenen Anforderungen besser geeignet ist. Soweit es mich betrifft und ohne mehr Kontext (oder im Zweifel), würde ich die Lösung lieber mit den weniger indirekten Ebenen verwenden, aber mit YMMV.

Hinweis zu generischen Beziehungen: Diese können nützlich sein, wenn Sie wirklich viele mögliche verwandte Modelle haben und / oder vorher nicht wissen, welche Modelle Sie mit Ihren eigenen in Beziehung setzen möchten. Dies ist besonders nützlich für wiederverwendbare Apps (denken Sie an "Kommentare" oder "Tags" usw. Funktionen) oder erweiterbare Apps (Content Management Frameworks usw.). Der Nachteil ist, dass das Abfragen dadurch viel schwieriger wird (und eher unpraktisch ist, wenn Sie manuelle Abfragen für Ihre Datenbank durchführen möchten). Aus Erfahrung können sie schnell zu einem PITA-Bot für Code und Perfs werden. Bewahren Sie sie daher besser auf, wenn es keine bessere Lösung gibt (und / oder wenn der Wartungs- und Laufzeitaufwand kein Problem darstellt).

Meine 2 Cent.

bruno desthuilliers
quelle
2

Django verfügt über eine neue (seit 2.2) Schnittstelle zum Erstellen von DB-Einschränkungen: https://docs.djangoproject.com/de/3.0/ref/models/constraints/

Sie können a verwenden CheckConstraint, um zu erzwingen, dass one-and-only-one nicht null ist. Ich benutze zwei aus Gründen der Klarheit:

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'),
            ),
        ]

Dadurch wird die Einschränkung nur auf DB-Ebene erzwungen. Sie müssen Eingaben in Ihren Formularen oder Serialisierern manuell validieren.

Als Randnotiz sollten Sie wahrscheinlich OneToOneFieldanstelle von verwenden ForeignKey(unique=True). Du wirst auch wollen blank=True.

Jonathan Richards
quelle
0

Ich denke, Sie sprechen über generische Beziehungen , Dokumente . Ihre Antwort sieht ähnlich diesem .

Vor einiger Zeit musste ich generische Beziehungen verwenden, aber ich las in einem Buch und woanders, dass die Verwendung vermieden werden sollte. Ich denke, es waren zwei Kugeln Django.

Am Ende habe ich ein Modell wie dieses erstellt:

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

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

Ich bin mir nicht sicher, ob es eine gute Lösung ist, und wie Sie bereits erwähnt haben, möchten Sie sie lieber nicht verwenden, aber dies funktioniert in meinem Fall.

Luis Silva
quelle
"Ich lese in einem Buch und woanders" ist der schlimmste Grund, etwas zu tun (oder zu vermeiden).
Bruno Desthuilliers
@brunodesthuilliers Ich dachte, zwei Kugeln Django wären ein gutes Buch.
Luis Silva
Kann nicht sagen, ich habe es nicht gelesen. Aber das hat nichts damit zu tun: Mein Punkt ist, dass wenn Sie nicht verstehen, warum das Buch dies sagt, es weder Wissen noch Erfahrung ist, sondern religiöser Glaube. Ich habe nichts gegen religiösen Glauben, wenn es um Religion geht, aber sie haben keinen Platz in CS. Entweder verstehen Sie die Vor- und Nachteile einer Funktion, und dann können Sie beurteilen, ob sie in einem bestimmten Kontext angemessen ist , oder Sie tun dies nicht, und dann sollten Sie nicht gedankenlos nachlesen, was Sie gelesen haben. Es gibt sehr gültige Anwendungsfälle für generische Beziehungen. Es geht nicht darum, sie überhaupt zu vermeiden, sondern zu wissen, wann sie vermieden werden müssen.
Bruno Desthuilliers
NB Ich verstehe vollkommen, dass man nicht alles über CS wissen kann - es gibt Bereiche, in denen ich keine andere Wahl habe, als einem Buch zu vertrauen. Aber dann werde ich wahrscheinlich keine Fragen zu diesem Thema
beantworten ;-)
0

Es könnte spät sein, Ihre Frage zu beantworten, aber ich dachte, meine Lösung könnte zum Fall einer anderen Person passen.

Ich würde ein neues Modell erstellen, es nennen Dependencyund die Logik in diesem Modell anwenden.

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)

Dann würde ich die Logik so schreiben, dass sie sehr explizit anwendbar ist.

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)

Jetzt müssen Sie nur noch eine Single ForeignKeyfür Ihre erstellenInspection Modell .

In Ihren viewFunktionen müssen Sie ein DependencyObjekt erstellen und es dann Ihrem InspectionDatensatz zuweisen . Stellen Sie sicher, dass Sie create_dependency_objectin Ihrem verwendenview Funktionen verwenden.

Dies macht Ihren Code ziemlich explizit und fehlerfrei. Die Durchsetzung kann zu leicht umgangen werden. Der Punkt ist jedoch, dass Vorkenntnisse erforderlich sind, um diese genaue Einschränkung zu umgehen.

Nima
quelle