Creazione di un modello con due chiavi esterne opzionali, ma una obbligatoria


9

Il mio problema è che ho un modello che può prendere una delle due chiavi esterne per dire che tipo di modello è. Voglio che ne richieda almeno uno ma non entrambi. Posso avere questo ancora un modello o dovrei dividerlo in due tipi. Ecco il codice:

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)

Il problema è il Inspectionmodello. Questo dovrebbe essere collegato a un gruppo o un sito, ma non entrambi. Attualmente con questa configurazione ha bisogno di entrambi.

Preferirei non doverlo dividere in due modelli quasi identici GroupInspectione SiteInspection, quindi, qualsiasi soluzione che lo mantenga come un modello sarebbe l'ideale.


Forse usare la sottoclasse è meglio qui. È possibile creare una Inspectionclasse e quindi sottoclassare in SiteInspectione GroupInspectionper le parti non comuni.
Willem Van Onsem,

Forse non correlato, ma la unique=Trueparte nei campi FK significa che Inspectionpuò esistere solo un'istanza per una determinata GroupIDo SiteIDistanza - IOW, è una relazione uno a uno, non una a molte. E 'davvero ciò che vuoi ?
bruno desthuilliers,

"Attualmente con questa configurazione ha bisogno di entrambi." => tecnicamente, non lo è - a livello di database, è possibile impostare entrambe, o nessuna di quelle chiavi (con l'avvertenza menzionata sopra). È solo quando si utilizza un ModelForm (direttamente o tramite django admin) che quei campi saranno contrassegnati come richiesto, e questo perché non hai passato l'argomento 'blank = True'.
bruno desthuilliers

@brunodesthuilliers Sì, l'idea è quella di avere Inspectionun collegamento tra Groupo Sitee an InspectionID, quindi posso avere più "ispezioni" sotto forma di InspectionReportquella relazione. Questo è stato fatto in modo che io possa ordinare più facilmente Dateper tutti i record relativi a uno Groupo Site. Spero che abbia un senso
CalMac

@ Cm0295 Temo di non vedere il punto di questo livello di riferimento indiretto: inserendo gli FK di gruppo / sito direttamente in InspectionReport si ottiene esattamente lo stesso servizio AFAICT: filtra InspectorReports con la chiave appropriata (oppure segui semplicemente il descrittore inverso dal sito o Gruppo), ordinali per data e il gioco è fatto.
bruno desthuilliers

Risposte:


5

Suggerirei di fare tale convalida nel modo Django

sovrascrivendo il cleanmetodo di Django Model

class Inspection(models.Model):
    ...

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

Si noti, tuttavia, che come Model.full_clean (), il metodo clean () di un modello non viene invocato quando si chiama il metodo save () del modello. deve essere chiamato manualmente per convalidare i dati del modello, oppure è possibile sovrascrivere il metodo save del modello per farlo chiamare sempre il metodo clean () prima di attivare il Modelmetodo save della classe


Un'altra soluzione che potrebbe essere d'aiuto è l'uso di GenericRelations , al fine di fornire un campo polimorfico correlato a più di una tabella, ma può essere il caso se queste tabelle / oggetti possono essere utilizzate in modo intercambiabile nella progettazione del sistema sin dall'inizio.


2

Come accennato nei commenti, il motivo per cui "con questa configurazione ha bisogno di entrambi" è solo che hai dimenticato di aggiungere i blank=Truecampi FK, quindi il tuo ModelForm(uno personalizzato o quello predefinito generato dall'amministratore) renderà necessario il campo modulo . A livello di schema db, potresti riempire entrambi, uno o nessuno di questi FK, sarebbe ok dato che hai reso quei campi db nulli (con l' null=Trueargomento).

Inoltre, (vedi altri miei commenti), potresti voler verificare che tu voglia davvero che gli FK siano unici. Questo tecnicamente trasforma la tua relazione uno a più in una relazione uno a uno: ti è permesso solo un singolo record di 'ispezione' per un determinato GroupID o SiteId (non puoi avere due o più 'ispezioni' per un GroupId o SiteId) . Se è REALMENTE quello che vuoi, potresti voler usare un OneToOneField esplicito (lo schema db sarà lo stesso ma il modello sarà più esplicito e il descrittore correlato molto più utilizzabile per questo caso d'uso).

Come nota a margine: in un modello Django, un campo ForeignKey si materializza come un'istanza del modello correlata, non come un ID grezzo. IOW, dato questo:

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)

allora bar.foorisolverà a foo, non a foo.id. Quindi sicuramente vuoi rinominare il tuo InspectionIDe i SiteIDcampi in proprio inspectione site. A proposito, in Python, la convenzione di denominazione è "all_lower_with_underscores" per nient'altro che nomi di classe e pseudo-costanti.

Ora per la tua domanda principale: non esiste un modo SQL standard specifico per imporre un vincolo "l'uno o l'altro" a livello di database, quindi di solito viene fatto usando un vincolo CHECK , che viene eseguito in un modello Django con i meta "vincoli" del modello opzione .

Detto questo, il modo in cui i vincoli sono effettivamente supportati e applicati a livello di db dipende dal tuo fornitore di database (MySQL <8.0.16 semplicemente li ignora, per esempio), e il tipo di vincolo di cui avrai bisogno qui non sarà applicato nel modulo o livello modello di validazione , solo quando si cerca di salvare il modello, in modo da anche da aggiungere la convalida sia a livello di modello (preferibilmente) o di convalida dei livelli forma, in entrambi i casi nel modello (resp.) o di forma di clean()metodo.

Quindi, per farla breve:

  • prima ricontrolla che vuoi davvero questo unique=Truevincolo e, in caso affermativo, sostituisci il tuo campo FK con OneToOneField.

  • aggiungi un blank=Truearg a entrambi i campi FK (o OneToOne)

  • aggiungi il vincolo di controllo appropriato nella meta meta del tuo modello: il documento è succinto ma ancora abbastanza esplicito se sai di fare query complesse con l'ORM (e se non lo fai è tempo che impari ;-))
  • aggiungi un clean()metodo al tuo modello per verificare se hai l'uno o l'altro campo e genera un altro errore di convalida

e dovresti essere a posto, supponendo che il tuo RDBMS rispetti i vincoli del controllo ovviamente.

Basta notare che, con questo design, il tuo Inspectionmodello è un'inversione totalmente inutile (ma costosa!): Otterrai le stesse identiche funzionalità a un costo inferiore spostando direttamente gli FK (e vincoli, validazione ecc.) InspectionReport.

Ora potrebbe esserci un'altra soluzione: mantenere il modello Inspection, ma inserire l'FK come OneToOneField all'altra estremità della relazione (in Sito e Gruppo):

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)

E quindi puoi ottenere tutti i rapporti per un determinato sito o gruppo con yoursite.inspection.inspectionreport_set.all().

Ciò evita di dover aggiungere vincoli o convalide specifici, ma a costo di un ulteriore livello di riferimento indiretto ( joinclausola SQL ecc.).

Quale di queste soluzioni sarebbe "la migliore" dipende dal contesto, quindi devi capire le implicazioni di entrambi e controllare come usi i tuoi modelli per scoprire quale è più appropriato per le tue esigenze. Per quanto mi riguarda e senza più contesto (o in dubbio) preferirei usare la soluzione con i livelli meno indiretti, ma YMMV.

NB per quanto riguarda le relazioni generiche: quelle possono essere utili quando si hanno davvero molti possibili modelli correlati e / o non si conosce in anticipo quali modelli si vorranno mettere in relazione con i propri. Ciò è particolarmente utile per le app riutilizzabili (pensa a funzionalità di "commenti" o "tag" ecc.) O estensibili (framework di gestione dei contenuti, ecc.). Il rovescio della medaglia è che rende le query molto più pesanti (e piuttosto poco pratiche quando si desidera fare query manuali sul proprio db). Per esperienza, possono rapidamente diventare un wrt / code e perf di bot PITA, quindi è meglio tenerli per quando non c'è soluzione migliore (e / o quando l'overhead di manutenzione e runtime non è un problema).

I miei 2 centesimi.


2

Django ha una nuova (dal 2.2) interfaccia per la creazione di vincoli DB: https://docs.djangoproject.com/en/3.0/ref/models/constraints/

È possibile utilizzare a CheckConstraintper applicare uno e solo uno è non nullo. Ne uso due per chiarezza:

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

Questo imporrà il vincolo solo a livello di DB. Dovrai convalidare manualmente gli input nei tuoi moduli o serializzatori.

Come nota a margine, probabilmente dovresti usare OneToOneFieldinvece di ForeignKey(unique=True). Lo vorrai anche tu blank=True.


0

Penso che tu stia parlando di relazioni generiche , documenti . La tua risposta è simile a questa .

Qualche tempo fa avevo bisogno di usare le relazioni generiche, ma ho letto in un libro e da qualche altra parte che l'uso dovrebbe essere evitato, penso che sia stato due scoop di Django.

Ho finito per creare un modello come questo:

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

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

Non sono sicuro che sia una buona soluzione e, come hai detto, preferiresti non usarlo, ma nel mio caso ha funzionato.


"Ho letto in un libro e da qualche altra parte" riguarda la peggiore ragione possibile per fare (o evitare di fare) qualcosa.
bruno desthuilliers

@brunodesthuilliers Pensavo che Two Scoops of Django fosse un buon libro.
Luis Silva,

Non posso dirlo, non l'ho letto. Ma ciò non ha alcuna relazione: il mio punto è che se non capisci perché il libro lo dice, allora non è conoscenza né esperienza, è credenza religiosa. Non mi dispiace il credo religioso quando si tratta di religione, ma non hanno posto in CS. O capisci quali sono i pro e i contro di alcune funzionalità e poi puoi giudicare se è appropriato in un determinato contesto , oppure non lo fai e quindi non dovresti pappagallo inconsapevolmente ciò che hai letto. Esistono casi d'uso molto validi per le relazioni generiche, il punto non è evitarli affatto, ma sapere quando evitarli.
bruno desthuilliers,

NB Capisco perfettamente che non si può sapere tutto su CS - ci sono domini in cui non ho altre opzioni che fidarmi di un libro. Ma probabilmente non risponderò alle domande su questo argomento ;-)
bruno desthuilliers

0

Potrebbe essere tardi per rispondere alla tua domanda, ma ho pensato che la mia soluzione potesse adattarsi al caso di un'altra persona.

Vorrei creare un nuovo modello, chiamiamolo Dependencye applicare la logica in quel modello.

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)

Quindi scriverei la logica per applicarla in modo molto esplicito.

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)

Ora devi solo crearne uno singolo ForeignKeyper il tuo Inspectionmodello.

Nelle tue viewfunzioni, devi creare un Dependencyoggetto e quindi assegnarlo al tuo Inspectionrecord. Assicurati di utilizzare create_dependency_objectnelle tue viewfunzioni.

Questo praticamente rende il tuo codice esplicito e a prova di bug. L'applicazione può essere ignorata troppo facilmente. Ma il punto è che ha bisogno di una conoscenza preliminare di questa esatta limitazione per essere aggirato.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.