Comment mapper une relation IS-A dans une base de données?

26

Considérer ce qui suit:

entity User
{
    autoincrement uid;
    string(20) name;
    int privilegeLevel;
}

entity DirectLoginUser
{
    inherits User;
    string(20) username;
    string(16) passwordHash;
}

entity OpenIdUser
{
    inherits User;
    //Whatever attributes OpenID needs... I don't know; this is hypothetical
}

Les différents types d'utilisateurs (utilisateurs à connexion directe et utilisateurs OpenID) affichent une relation IS-A; à savoir que les deux types d'utilisateurs sont des utilisateurs. Maintenant, il existe plusieurs façons de représenter cela dans un SGBDR:

Première voie

CREATE TABLE Users
(
    uid INTEGER AUTO_INCREMENT NOT NULL,
    name VARCHAR(20) NOT NULL,
    privlegeLevel INTEGER NOT NULL,
    type ENUM("DirectLogin", "OpenID") NOT NULL,
    username VARCHAR(20) NULL,
    passwordHash VARCHAR(20) NULL,
    //OpenID Attributes
    PRIMARY_KEY(uid)
)

Deuxième voie

CREATE TABLE Users
(
    uid INTEGER AUTO_INCREMENT NOT NULL,
    name VARCHAR(20) NOT NULL,
    privilegeLevel INTEGER NOT NULL,
    type ENUM("DirectLogin", "OpenID") NOT NULL,
    PRIMARY_KEY(uid)
)

CREATE TABLE DirectLogins
(
    uid INTEGER NOT_NULL,
    username VARCHAR(20) NOT NULL,
    passwordHash VARCHAR(20) NOT NULL,
    PRIMARY_KEY(uid),
    FORIGEN_KEY (uid) REFERENCES Users.uid
)

CREATE TABLE OpenIDLogins
(
    uid INTEGER NOT_NULL,
    // ...
    PRIMARY_KEY(uid),
    FORIGEN_KEY (uid) REFERENCES Users.uid
)

Troisième voie

CREATE TABLE DirectLoginUsers
(
    uid INTEGER AUTO_INCREMENT NOT NULL,
    name VARCHAR(20) NOT NULL,
    privlegeLevel INTEGER NOT NULL,
    username VARCHAR(20) NOT NULL,
    passwordHash VARCHAR(20) NOT NULL,
    PRIMARY_KEY(uid)
)

CREATE TABLE OpenIDUsers
(
    uid INTEGER AUTO_INCREMENT NOT NULL,
    name VARCHAR(20) NOT NULL,
    privlegeLevel INTEGER NOT NULL,
    //OpenID Attributes
    PRIMARY_KEY(uid)
)

Je suis presque certain que la troisième voie est la mauvaise, car il n'est pas possible de faire une simple jointure contre des utilisateurs ailleurs dans la base de données.

Mon exemple dans le monde réel n'est pas un exemple d'utilisateurs avec des connexions différentes; Je souhaite savoir comment modéliser cette relation dans le cas général.

Billy ONeal
la source
J'ai modifié ma réponse pour inclure l'approche suggérée dans les commentaires de Joel Brown. Il devrait fonctionner pour vous. Si vous cherchez plus de suggestions, je voudrais réétiqueter votre question pour indiquer que vous recherchez une réponse spécifique à MySQL.
Nick Chammas,
Notez que cela dépend vraiment de la façon dont les relations sont utilisées dans le reste de la base de données. Les relations impliquant les entités enfants nécessitent des clés étrangères uniquement pour ces tables et interdisent le mappage vers une seule table unificatrice sans piratage.
beldaz
Dans les bases de données relationnelles objet comme PostgreSQL, il existe en fait une quatrième façon: vous pouvez déclarer une table INHERITS à partir d'une autre table. postgresql.org/docs/current/static/tutorial-inheritance.html
MarkusSchaber

Réponses:

16

La deuxième façon est la bonne.

Votre classe de base obtient une table, puis les classes enfants obtiennent leurs propres tables avec uniquement les champs supplémentaires qu'elles introduisent, ainsi que les références de clé étrangère à la table de base.

Comme Joel l'a suggéré dans ses commentaires sur cette réponse, vous pouvez garantir qu'un utilisateur aura soit une connexion directe, soit une connexion OpenID, mais pas les deux (et peut-être même ni l'un ni l'autre) en ajoutant une colonne de type à chaque table de sous-type qui revient en arrière. à la table racine. La colonne type de chaque table de sous-type est limitée à une seule valeur représentant le type de cette table. Étant donné que cette colonne est dotée d'une clé étrangère pour la table racine, une seule ligne de sous-type peut être liée à la même ligne racine à la fois.

Par exemple, le DDL MySQL ressemblerait à quelque chose comme:

CREATE TABLE Users
(
      uid               INTEGER AUTO_INCREMENT NOT NULL
    , type              ENUM("DirectLogin", "OpenID") NOT NULL
    // ...

    , PRIMARY_KEY(uid)
);

CREATE TABLE DirectLogins
(
      uid               INTEGER NOT_NULL
    , type              ENUM("DirectLogin") NOT NULL
    // ...

    , PRIMARY_KEY(uid)
    , FORIGEN_KEY (uid, type) REFERENCES Users (uid, type)
);

CREATE TABLE OpenIDLogins
(
      uid               INTEGER NOT_NULL
    , type              ENUM("OpenID") NOT NULL
    // ...

    PRIMARY_KEY(uid),
    FORIGEN_KEY (uid, type) REFERENCES Users (uid, type)
);

(Sur d'autres plates-formes, vous utiliseriez une CHECKcontrainte au lieu de ENUM.) MySQL prend en charge les clés étrangères composites, cela devrait donc fonctionner pour vous.

La première façon est valide, bien que vous gaspilliez de l'espace dans ces NULLcolonnes activables car leur utilisation dépend du type d'utilisateur. L'avantage est que si vous choisissez d'étendre les types de types d'utilisateurs à stocker et que ces types ne nécessitent pas de colonnes supplémentaires, vous pouvez simplement développer le domaine de votre ENUMet utiliser la même table.

La troisième méthode force toutes les requêtes qui référencent les utilisateurs à comparer avec les deux tables. Cela vous empêche également de référencer une table d'utilisateurs unique via une clé étrangère.

Nick Chammas
la source
1
Comment puis-je gérer le fait qu'en utilisant la méthode 2, il n'y a aucun moyen de faire en sorte qu'il y ait exactement une ligne correspondante dans les deux autres tables?
Billy ONeal
2
@Billy - Bonne objection. Si vos utilisateurs ne peuvent avoir que l'un ou l'autre, vous pouvez l'imposer via votre couche proc ou vos déclencheurs. Je me demande s'il existe un moyen au niveau DDL d'appliquer cette contrainte. (Hélas, les vues indexées ne le permettent pas UNION , ou j'aurais suggéré une vue indexée avec un index unique par rapport aux UNION ALLde uiddes deux tables.)
Nick Chammas
Bien sûr, cela suppose que votre SGBDR prend en charge les vues indexées en premier lieu.
Billy ONeal
1
Un moyen pratique d'implémenter ce type de contrainte de table croisée serait d'inclure un attribut de partitionnement dans la table de super-type. Ensuite, chaque sous-type peut vérifier qu'il ne concerne que les super-types qui ont la valeur d'attribut de partitionnement appropriée. Cela évite d'avoir à effectuer un test de collision en consultant une ou plusieurs autres tables de sous-types.
Joel Brown
1
@Joel - Ainsi, par exemple, nous ajoutons une typecolonne à chaque table de sous-type qui est restreinte via une CHECKcontrainte pour avoir exactement une valeur (le type de cette table). Ensuite, nous transformons les clés étrangères de la sous-table de la super-table en clés composites sur uidet type. C'est ingénieux.
Nick Chammas
5

Ils seraient nommés

  1. Héritage de table unique
  2. Héritage de table de classe
  3. Héritage de table en béton .

et tous ont leurs utilisations légitimes et sont pris en charge par certaines bibliothèques. Vous devez trouver celle qui vous convient le mieux.

Avoir plusieurs tables rendrait la gestion des données plus importante pour votre code d'application, mais réduirait la quantité d'espace inutilisé.

flob
la source
2
Il existe une technique supplémentaire appelée "clé primaire partagée". Dans cette technique, les tables de sous-classe n'ont pas d'ID de clé primaire attribué indépendamment. Au lieu de cela, le PK des tables de sous-classe est le FK qui référence la table de super-classe. Cela offre plusieurs avantages, principalement parce qu'il renforce la nature univoque des relations IS-A. Cette technique est un ajout à l'héritage de table de classe.
Walter Mitty