Création d'un modèle avec deux clés étrangères facultatives, mais une clé étrangère obligatoire

9

Mon problème est que j'ai un modèle qui peut prendre l'une des deux clés étrangères pour dire de quel type de modèle il s'agit. Je veux qu'il en prenne au moins un mais pas les deux. Puis-je avoir encore un modèle ou dois-je le diviser en deux types. Voici le 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)

Le problème, c'est le Inspectionmodèle. Cela doit être lié à un groupe ou à un site, mais pas aux deux. Actuellement, avec cette configuration, il a besoin des deux.

Je préfère ne pas avoir à diviser cela en deux modèles presque identiques GroupInspectionet SiteInspection, donc toute solution qui le garderait comme un seul modèle serait idéale.

CalMac
la source
Il est peut-être préférable d'utiliser le sous-classement ici. Vous pouvez créer une Inspectionclasse, puis une sous-classe dans SiteInspectionet GroupInspectionpour les parties non communes.
Willem Van Onsem
Peut-être sans rapport, mais la unique=Truepartie dans vos champs FK signifie qu'une seule Inspectioninstance peut exister pour une instance donnée GroupIDou SiteID- IOW, c'est une relation un à un, pas une à plusieurs. C'est vraiment ce que tu veux?
bruno desthuilliers
"Actuellement, avec cette configuration, il a besoin des deux." => techniquement, ce n'est pas le cas - au niveau de la base de données, vous pouvez définir les deux, l'une ou l'autre de ces clés (avec la mise en garde mentionnée ci-dessus). Ce n'est que lorsque vous utilisez un ModelForm (directement ou via l'administrateur django) que ces champs seront marqués comme requis, et c'est parce que vous n'avez pas passé l'argument 'blank = True'.
bruno desthuilliers
@brunodesthuilliers Oui, l'idée est d'avoir Inspectionun lien entre le Groupou Siteet le InspectionID, alors je peux avoir plusieurs "inspections" sous la forme de InspectionReportpour cette seule relation. Cela a été fait afin que je puisse plus facilement trier par Datetous les enregistrements liés à un Groupou Site. J'espère que cela a du sens
CalMac
@ Cm0295 J'ai bien peur de ne pas voir l'intérêt de ce niveau d'indirection - mettre les FK de groupe / site directement dans InspectionReport donne exactement le même service AFAICT - filtrer vos rapports d'inspection par la clé appropriée (ou suivez simplement le descripteur inverse depuis Site ou Groupe), triez-les par date et vous avez terminé.
bruno desthuilliers

Réponses:

5

Je suggère que vous fassiez une telle validation à la manière de Django

en redéfinissant la cleanméthode du modèle Django

class Inspection(models.Model):
    ...

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

Notez cependant que, comme Model.full_clean (), la méthode clean () d'un modèle n'est pas invoquée lorsque vous appelez la méthode save () de votre modèle. il doit être appelé manuellement pour valider les données du modèle, ou vous pouvez remplacer la méthode de sauvegarde du modèle pour qu'il appelle toujours la méthode clean () avant de déclencher la Modelméthode de sauvegarde de classe


Une autre solution qui pourrait aider est d'utiliser GenericRelations , afin de fournir un champ polymorphe qui se rapporte à plusieurs tables, mais cela peut être le cas si ces tables / objets peuvent être utilisés de manière interchangeable dans la conception du système dès le départ.

Radwan Abu-Odeh
la source
2

Comme mentionné dans les commentaires, la raison pour laquelle "avec cette configuration, il a besoin des deux" est simplement que vous avez oublié d'ajouter le blank=Trueà vos champs FK, de sorte que votre ModelForm(personnalisé ou généré par défaut par l'administrateur) rendra le champ de formulaire requis . Au niveau du schéma db, vous pouvez remplir les deux, un ou aucun de ces FK, ce serait correct puisque vous avez rendu ces champs db nullables (avec l' null=Trueargument).

De plus, (cf. mes autres commentaires), vous voudrez peut-être vérifier que vous voulez vraiment que les FK soient uniques. Techniquement, cela transforme votre relation un à plusieurs en une relation un à un - vous n'êtes autorisé qu'à un seul enregistrement `` d'inspection '' pour un GroupID ou SiteId donné (vous ne pouvez pas avoir deux ou plusieurs `` inspections '' pour un GroupId ou SiteId) . Si c'est VRAIMENT ce que vous voulez, vous voudrez peut-être utiliser un OneToOneField explicite à la place (le schéma db sera le même mais le modèle sera plus explicite et le descripteur associé beaucoup plus utilisable pour ce cas d'utilisation).

En guise de remarque: dans un modèle Django, un champ ForeignKey se matérialise comme une instance de modèle associée, et non comme un identifiant brut. IOW, étant donné ceci:

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)

bar.foova alors se résoudre à foo, pas à foo.id. Vous voulez donc certainement renommer vos champs InspectionIDet SiteIDen propres inspectionet site. BTW, en Python, la convention de dénomination est 'all_lower_with_underscores' pour autre chose que les noms de classe et les pseudo-constantes.

Maintenant, pour votre question principale: il n'y a pas de méthode SQL standard spécifique pour appliquer une contrainte "l'une ou l'autre" au niveau de la base de données, donc cela se fait généralement en utilisant une contrainte CHECK , ce qui est fait dans un modèle Django avec les méta "contraintes" du modèle option .

Cela étant dit, comment les contraintes sont effectivement prises en charge et mises en œuvre au niveau db dépend de votre fournisseur DB (MySQL <8.0.16 tout simplement les ignorer par exemple), et le type de contrainte que vous aurez besoin ici ne seront pas appliquées à la forme ou la validation au niveau du modèle , uniquement lorsque vous essayez d'enregistrer le modèle, vous devez donc également ajouter la validation au niveau du modèle (de préférence) ou au niveau du formulaire, dans les deux cas dans le modèle (resp.) ou la clean()méthode du formulaire .

Donc, pour faire une longue histoire:

  • vérifiez d'abord que vous voulez vraiment cette unique=Truecontrainte, et si oui, remplacez votre champ FK par un OneToOneField.

  • ajouter un blank=Trueargument à vos deux champs FK (ou OneToOne)

  • ajoutez la contrainte de vérification appropriée dans la méta de votre modèle - le doc est succint mais toujours suffisamment explicite si vous savez faire des requêtes complexes avec l'ORM (et si vous ne le faites pas, il est temps d'apprendre ;-))
  • ajoutez une clean()méthode à votre modèle qui vérifie que vous avez l'un ou l'autre champ et déclenche une erreur de validation sinon

et vous devriez être d'accord, en supposant que votre SGBDR respecte les contraintes de vérification bien sûr.

Notez simplement qu'avec cette conception, votre Inspectionmodèle est une indirection totalement inutile (mais coûteuse!) - vous obtiendriez exactement les mêmes fonctionnalités à moindre coût en déplaçant directement les FK (et les contraintes, la validation, etc.) InspectionReport.

Maintenant, il pourrait y avoir une autre solution - conserver le modèle d'inspection, mais placer le FK en tant que OneToOneField à l'autre extrémité de la relation (dans Site et Groupe):

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)

Et puis, vous pouvez obtenir tous les rapports pour un site ou un groupe donné avec yoursite.inspection.inspectionreport_set.all().

Cela évite d'avoir à ajouter une contrainte ou une validation spécifique, mais au prix d'un niveau d'indirection supplémentaire ( joinclause SQL , etc.).

Laquelle de ces solutions serait "la meilleure" dépend vraiment du contexte, vous devez donc comprendre les implications des deux et vérifier comment vous utilisez généralement vos modèles pour trouver celle qui convient le mieux à vos propres besoins. En ce qui me concerne et sans plus de contexte (ou de doute), je préfère utiliser la solution avec les niveaux d'indirection les moins nombreux, mais YMMV.

NB concernant les relations génériques: celles-ci peuvent être utiles lorsque vous avez vraiment beaucoup de modèles liés possibles et / ou que vous ne savez pas au préalable quels modèles vous voudrez associer aux vôtres. Ceci est particulièrement utile pour les applications réutilisables (pensez aux fonctionnalités "commentaires" ou "tags", etc.) ou extensibles (cadres de gestion de contenu, etc.). L'inconvénient est que cela rend les requêtes beaucoup plus lourdes (et plutôt peu pratiques lorsque vous souhaitez effectuer des requêtes manuelles sur votre base de données). Par expérience, ils peuvent rapidement devenir un bot wrt / code et perfs PITA, donc mieux les conserver quand il n'y a pas de meilleure solution (et / ou lorsque la surcharge de maintenance et d'exécution n'est pas un problème).

Mes 2 cents.

bruno desthuilliers
la source
2

Django a une nouvelle interface (depuis 2.2) pour créer des contraintes de base de données: https://docs.djangoproject.com/en/3.0/ref/models/constraints/

Vous pouvez utiliser un CheckConstraintpour appliquer un et un seul n'est pas nul. J'en utilise deux pour plus de clarté:

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

Cela n'appliquera la contrainte qu'au niveau de la base de données. Vous devrez valider manuellement les entrées dans vos formulaires ou sérialiseurs.

En remarque, vous devriez probablement utiliser OneToOneFieldau lieu de ForeignKey(unique=True). Vous voudrez aussi blank=True.

Jonathan Richards
la source
0

Je pense que vous parlez de relations génériques , docs . Votre réponse ressemble à celle-ci .

Il y a quelque temps, j'avais besoin d'utiliser des relations génériques, mais j'ai lu dans un livre et ailleurs que l'utilisation devait être évitée, je pense que c'était Two Scoops of Django.

J'ai fini par créer un modèle comme celui-ci:

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

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

Je ne sais pas si c'est une bonne solution et comme vous l'avez mentionné, vous préférez ne pas l'utiliser, mais cela fonctionne dans mon cas.

Luis Silva
la source
«Je lis dans un livre et ailleurs» est la pire raison possible de faire (ou d'éviter de faire) quelque chose.
bruno desthuilliers
@brunodesthuilliers Je pensais que Two Scoops of Django était un bon livre.
Luis Silva
Je ne peux pas le dire, je ne l'ai pas lu. Mais ce n'est pas lié: mon point est que si vous ne comprenez pas pourquoi le livre le dit, alors ce n'est pas une connaissance ni une expérience, c'est une croyance religieuse. Cela ne me dérange pas la croyance religieuse en ce qui concerne la religion, mais ils n'ont pas leur place dans CS. Soit vous comprenez quels sont les avantages et les inconvénients d'une fonctionnalité, puis vous pouvez juger si elle est appropriée dans un contexte donné , ou vous ne le faites pas et vous ne devez pas insensiblement perroquet ce que vous avez lu. Il existe des cas d'utilisation très valables pour les relations génériques, il ne s'agit pas du tout de les éviter mais de savoir quand les éviter.
bruno desthuilliers
NB Je comprends parfaitement que l'on ne peut pas tout savoir sur CS - il y a des domaines où je n'ai pas d'autre option que de faire confiance à un livre. Mais alors je ne répondrai probablement pas aux questions sur ce sujet ;-)
bruno desthuilliers
0

Il est peut-être tard pour répondre à votre question, mais j'ai pensé que ma solution pourrait convenir au cas d'une autre personne.

Je créerais un nouveau modèle, appelons-le Dependency, et appliquerais la logique de ce modèle.

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)

Ensuite, j'écrirais la logique pour être applicable de manière très explicite.

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)

Il ne vous reste plus qu'à créer un single ForeignKeypour votre Inspectionmodèle.

Dans vos viewfonctions, vous devez créer un Dependencyobjet puis l'attribuer à votre Inspectiondossier. Assurez-vous que vous utilisez create_dependency_objectdans vos viewfonctions.

Cela rend à peu près votre code explicite et à l'épreuve des bogues. L'application peut être contournée trop facilement. Mais le fait est qu'il a besoin d'une connaissance préalable de cette limitation exacte pour être contourné.

nima
la source