Comment avoir une relation un-à-plusieurs avec un enfant privilégié?

22

Je veux avoir une relation un-à-plusieurs dans laquelle pour chaque parent, un ou zéro des enfants est marqué comme «favori». Cependant, tous les parents n'auront pas d'enfant. (Considérez les parents comme des questions sur ce site, les enfants comme des réponses et les favoris comme la réponse acceptée.) Par exemple,

TableA
    Id            INT PRIMARY KEY

TableB
    Id            INT PRIMARY KEY
    Parent        INT NOT NULL FOREIGN KEY REFERENCES TableA.Id

D'après ce que je vois, je peux soit ajouter la colonne suivante au tableau A:

    FavoriteChild INT NULL FOREIGN KEY REFERENCES TableB.Id

ou la colonne suivante du tableau B:

    IsFavorite    BIT NOT NULL

Le problème avec la première approche est qu'elle introduit une clé étrangère nullable, qui, je comprends, n'est pas sous une forme normalisée. Le problème avec la deuxième approche est que plus de travail doit être fait pour s'assurer qu'au plus un enfant est le favori.

Quels types de critères dois-je utiliser pour déterminer quelle approche utiliser? Ou y a-t-il d'autres approches que je n'envisage pas?

J'utilise SQL Server 2012.

cubetwo1729
la source

Réponses:

19

Une autre façon (sans Nulls et sans cycles dans les FOREIGN KEYrelations) est d'avoir une troisième table pour stocker les "enfants préférés". Dans la plupart des SGBD, vous aurez besoin d'une UNIQUEcontrainte supplémentaire TableB.

@Aaron a été plus rapide à identifier que la convention de dénomination ci-dessus est plutôt lourde et peut entraîner des erreurs. Il est généralement préférable (et vous gardera sain d'esprit) si vous n'avez pas de Idcolonnes partout dans vos tables et si les colonnes (qui sont jointes) ont les mêmes noms dans les nombreuses tables qui apparaissent. Alors, voici un changement de nom:

Parent
    ParentID        INT NOT NULL PRIMARY KEY

Child
    ChildID         INT NOT NULL PRIMARY KEY
    ParentID        INT NOT NULL FOREIGN KEY REFERENCES Parent (ParentID)
    UNIQUE (ParentID, ChildID)

FavoriteChild
    ParentID        INT NOT NULL PRIMARY KEY
    ChildID         INT NOT NULL 
    FOREIGN KEY (ParentID, ChildID) 
        REFERENCES Child (ParentID, ChildID)

Dans SQL-Server (que vous utilisez), vous avez également la possibilité de la IsFavoritecolonne de bits que vous mentionnez. L'enfant préféré unique par parent peut être accompli via un index unique filtré:

Parent
    ParentID        INT NOT NULL PRIMARY KEY

Child
    ChildID         INT NOT NULL PRIMARY KEY
    ParentID        INT NOT NULL FOREIGN KEY REFERENCES Parent (ParentID)
    IsFavorite      BIT NOT NULL

CREATE UNIQUE INDEX is_FavoriteChild
  ON Child (ParentID)
  WHERE IsFavorite = 1 ;

Et la principale raison pour laquelle votre option 1 n'est pas recommandée, du moins pas dans SQL-Server, est que le modèle de chemins circulaires dans les références de clé étrangère a quelques problèmes.

Lire un article assez ancien: SQL By Design: la référence circulaire

Lorsque vous insérez ou supprimez des lignes des deux tableaux, vous rencontrerez le problème "poulet et œuf". Quelle table dois-je insérer en premier - sans violer aucune contrainte?

Pour résoudre ce problème, vous devez définir au moins une colonne nullable. (OK, techniquement, vous n'êtes pas obligé, vous pouvez avoir toutes les colonnes, NOT NULLmais uniquement dans les SGBD, comme Postgres et Oracle, qui ont implémenté des contraintes reportables. Voir la réponse de @ Erwin dans une question similaire: Contrainte de clé étrangère complexe dans SQLAlchemy sur la façon dont cela peut être fait dans Postgres). Pourtant, cette configuration ressemble à du patinage sur de la glace mince.

Vérifiez également une question presque identique à SO (mais pour MySQL) En SQL, est-il correct que deux tables se réfèrent l'une à l'autre? où ma réponse est à peu près la même. MySQL n'a cependant pas d'index partiel, donc les seules options viables sont le FK nullable et la solution de table supplémentaire.

ypercubeᵀᴹ
la source
9

Cela dépend de votre priorité. Voulez-vous éviter le travail ou souhaitez-vous respecter les règles de normalisation les plus strictes?

Personnellement, je pense qu'il vaut mieux avoir IsFavoritedans la table des enfants, et serait prêt à faire le travail pour s'assurer qu'au plus un enfant pour chaque parent est le favori de ce parent. La raison principale n'a rien à voir avec la clé étrangère nullable: je n'aime pas du tout l'idée de clés étrangères pointant dans les deux sens.

La suggestion de @ ypercube est également un bon compromis.

En passant, s'il vous plaît, s'il vous plaît, s'il vous plaît ne pas joncher votre schéma avec des noms de colonnes sans signification comme Id. Je préférerais de beaucoup voir le Idnom d'une manière significative tout au long du schéma. Identifie-t-il un auteur? Ok, appelle ça AuthorID. Représente-t-il un produit? Ok ProductID. Est-ce un employé et, dans certains cas, fait référence à un gestionnaire? Ok, EmployeeIDet ManagerIDme semble plus logique que IDet Parent. Bien qu'il puisse sembler logique de laisser cela de côté (et redondant pour le mettre), lorsque vous commencez à écrire des jointures complexes (ou postez des requêtes ici), vous ressentirez certainement des malédictions lorsque vous essayez de désosser un tas de jointures à ce point à des colonnes comme a.Parent = b.ID... blecch.

Aaron Bertrand
la source
1

Les données appartiennent à la table enfant. Nous le maintenons correct avec un déclencheur sur la table pour nous assurer qu'un seul et unique enregistrement est marqué comme favori (ou dans notre cas comme adresse préférée).

Cependant, l'idée de @ ypercube d'une table séparée est également bonne.

HLGEM
la source