¿Cómo mapeo una relación IS-A en una base de datos?

26

Considera lo siguiente:

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
}

Los diferentes tipos de usuarios (usuarios de inicio de sesión directo y usuarios de OpenID) muestran una relación IS-A; a saber, que ambos tipos de usuarios son usuarios. Ahora, hay varias formas en que esto se puede representar en un RDBMS:

Un camino

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)
)

Camino dos

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
)

Camino tres

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)
)

Estoy casi seguro de que la tercera forma es la equivocada, porque no es posible hacer una unión simple contra usuarios en otra parte de la base de datos.

Sin embargo, mi ejemplo del mundo real no es un usuario con diferentes inicios de sesión; Estoy interesado en cómo modelar esta relación en el caso general.

Billy ONeal
fuente
Edité mi respuesta para incluir el enfoque sugerido en los comentarios de Joel Brown. Debería funcionar para ti. Si está buscando más sugerencias, volvería a etiquetar su pregunta para indicar que está buscando una respuesta específica de MySQL.
Nick Chammas
Tenga en cuenta que realmente depende de cómo se usen las relaciones en el resto de la base de datos. Las relaciones que involucran a las entidades secundarias requieren claves foráneas solo para esas tablas, y prohíben la asignación a una sola tabla unificadora sin cierta intrusión.
beldaz
En bases de datos relacionales de objetos como PostgreSQL, en realidad hay una cuarta forma: puede declarar una tabla HEREDITS desde otra tabla. postgresql.org/docs/current/static/tutorial-inheritance.html
MarkusSchaber

Respuestas:

16

La segunda forma es la correcta.

Su clase base obtiene una tabla, y luego las clases secundarias obtienen sus propias tablas con solo los campos adicionales que introducen, además de referencias de claves externas a la tabla base.

Como Joel sugirió en sus comentarios sobre esta respuesta, puede garantizar que un usuario tendrá un inicio de sesión directo o un inicio de sesión de OpenID, pero no ambos (y posiblemente tampoco ninguno) agregando una columna de tipo a cada tabla de subtipo que devuelve la clave a la tabla raíz. La columna de tipo en cada tabla de subtipo está restringida a tener un solo valor que representa el tipo de esa tabla. Debido a que esta columna tiene una clave externa para la tabla raíz, solo una fila de subtipo puede vincularse a la misma fila raíz a la vez.

Por ejemplo, el DDL de MySQL se vería así:

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)
);

(En otras plataformas, usaría una CHECKrestricción en lugar de ENUM). MySQL admite claves foráneas compuestas, por lo que esto debería funcionar para usted.

La primera forma es válida, aunque está desperdiciando espacio en esas NULLcolumnas habilitadas porque su uso depende del tipo de usuario. La ventaja es que si elige expandir qué tipos de tipos de usuario almacenar y esos tipos no requieren columnas adicionales, simplemente puede expandir el dominio de su ENUMy usar la misma tabla.

La tercera forma obliga a las consultas que hacen referencia a los usuarios a comparar ambas tablas. Esto también evita que haga referencia a una sola tabla de usuarios a través de una clave externa.

Nick Chammas
fuente
1
¿Cómo trato el hecho de que usando la forma 2, no hay forma de exigir que exista exactamente una fila correspondiente en las otras dos tablas?
Billy ONeal
2
@ Billy - Buena objeción. Si sus usuarios solo pueden tener uno u otro, puede aplicar esto a través de su capa de proceso o disparadores. Me pregunto si hay una forma de nivel DDL para hacer cumplir esta restricción. (Por desgracia, las vistas indexadas no permiten UNION , o habría sugerido una vista indexada con un índice único en comparación con el UNION ALLde uidlas dos tablas.)
Nick Chammas
Por supuesto, eso supone que su RDBMS admite vistas indexadas en primer lugar.
Billy ONeal
1
Una forma práctica de implementar este tipo de restricción de tabla cruzada sería incluir un atributo de partición en la tabla de supertipo. Luego, cada subtipo puede verificar para asegurarse de que se relaciona solo con los supertipos que tienen el valor de atributo de partición apropiado. Esto ahorra tener que hacer una prueba de colisión mirando una o más tablas de subtipos.
Joel Brown
1
@Joel - Entonces, por ejemplo, agregamos una typecolumna a cada tabla de subtipo que está restringida por CHECKrestricción para tener exactamente un valor (el tipo de esa tabla). Luego, convertimos las claves externas de la subtabla para la supertabla en compuestas en ambas uidy type. Eso es ingenioso
Nick Chammas
5

Serían nombrados

  1. Herencia de mesa única
  2. Herencia de tabla de clase
  3. Mesa de hormigón Herencia .

y todos tienen sus usos legítimos y son compatibles con algunas bibliotecas. Tienes que averiguar cuál se ajusta mejor.

Tener varias tablas haría que la gestión de datos se adaptara más al código de su aplicación, pero reduciría la cantidad de espacio no utilizado.

flob
fuente
2
Existe una técnica adicional llamada "Clave primaria compartida". En esta técnica, las tablas de subclase no tienen una identificación de clave primaria asignada independientemente. En cambio, el PK de las tablas de subclase es el FK que hace referencia a la tabla de superclase. Esto proporciona varios beneficios, principalmente porque impone la naturaleza uno a uno de las relaciones IS-A. Esta técnica es un complemento de la herencia de tabla de clase.
Walter Mitty