Como mapeio um relacionamento IS-A em um banco de dados?

26

Considere o seguinte:

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
}

Os diferentes tipos de usuários (usuários de logon direto e usuários OpenID) exibem um relacionamento IS-A; a saber, que ambos os tipos de usuários são usuários. Agora, existem várias maneiras de representá-lo em um RDBMS:

Caminho Um

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

Caminho Dois

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
)

Caminho Três

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

Estou quase certo de que a terceira maneira é errada, porque não é possível fazer uma junção simples contra usuários em outras partes do banco de dados.

Meu exemplo do mundo real não é um exemplo de usuários com logins diferentes; Estou interessado em como modelar esse relacionamento no caso geral.

Billy ONeal
fonte
Editei minha resposta para incluir a abordagem sugerida nos comentários de Joel Brown. Isso deve funcionar para você. Se você estiver procurando por mais sugestões, gostaria de re-marcar sua pergunta para indicar que você está procurando uma resposta específica do MySQL.
Nick Chammas
Observe que realmente depende de como as relações são usadas no restante do banco de dados. Os relacionamentos que envolvem as entidades filhas requerem chaves estrangeiras para apenas essas tabelas e proíbem o mapeamento para uma única tabela unificadora sem alguma hackiness.
beldaz
Em bancos de dados relacionais a objetos como o PostgreSQL, existe realmente uma quarta maneira: você pode declarar uma tabela INHERITS de outra tabela. postgresql.org/docs/current/static/tutorial-inheritance.html
MarkusSchaber 4/18

Respostas:

16

O caminho dois é o caminho correto.

Sua classe base obtém uma tabela e as classes filho obtêm suas próprias tabelas apenas com os campos adicionais que eles introduzem, além de referências de chave estrangeira à tabela base.

Como Joel sugeriu em seus comentários sobre esta resposta, você pode garantir que um usuário tenha um login direto ou um OpenID, mas não os dois (e possivelmente também nenhum) adicionando uma coluna de tipo a cada tabela de subtipo que retorna para a tabela raiz. A coluna de tipo em cada tabela de subtipo é restrita a ter um valor único que representa o tipo dessa tabela. Como esta coluna é com chave estrangeira para a tabela raiz, apenas uma linha de subtipo pode vincular à mesma linha raiz de cada vez.

Por exemplo, o DDL do MySQL se pareceria com:

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

(Em outras plataformas, você usaria uma CHECKrestrição em vez de ENUM.) O MySQL suporta chaves estrangeiras compostas, portanto, isso deve funcionar para você.

A primeira maneira é válida, embora você esteja desperdiçando espaço nessas NULLcolunas ativáveis ​​porque o uso delas depende do tipo de usuário. A vantagem é que, se você optar por expandir quais tipos de usuários armazenar e esses tipos não exigirem colunas adicionais, basta expandir o domínio ENUMe usar a mesma tabela.

A maneira três força qualquer consulta que faça referência aos usuários a verificar nas duas tabelas. Isso também impede que você faça referência a uma única tabela de usuários via chave estrangeira.

Nick Chammas
fonte
11
Como lidar com o fato de que, usando o modo 2, não há como impor que exista exatamente uma linha correspondente nas duas outras tabelas?
Billy ONeal
2
@ Billy - Boa objeção. Se seus usuários puderem ter apenas um ou outro, você poderá aplicá-lo através da sua camada de proc ou gatilhos. Gostaria de saber se existe uma maneira no nível DDL de impor essa restrição. (Alas, exibições indexadas não permitem UNION , ou eu teria sugerido uma exibição indexada com um índice exclusivo contra o UNION ALLde uidpartir as duas tabelas.)
Nick Chammas
Obviamente, isso pressupõe que seu RDBMS suportou visualizações indexadas em primeiro lugar.
Billy ONeal
11
Uma maneira útil de implementar esse tipo de restrição entre tabelas seria incluir um atributo de particionamento na tabela supertipo. Em seguida, cada subtipo pode verificar para garantir que esteja relacionado apenas aos supertipos que possuem o valor do atributo de particionamento apropriado. Isso evita a necessidade de fazer um teste de colisão observando uma ou mais outras tabelas de subtipo.
Joel Brown
11
@ Joel - Então, por exemplo, adicionamos uma typecoluna a cada tabela de subtipo restrita por CHECKrestrição para ter exatamente um valor (o tipo dessa tabela). Em seguida, transformamos as chaves estrangeiras da sub-tabela da super-tabela em chaves compostas em ambos uide type. Isso é engenhoso.
Nick Chammas
5

Eles seriam nomeados

  1. Herança de tabela única
  2. Herança de tabela de classe
  3. Herança de mesa de concreto .

e todos têm seus usos legítimos e são suportados por algumas bibliotecas. Você tem que descobrir qual se encaixa melhor.

Ter várias tabelas levaria o gerenciamento de dados mais para o código do aplicativo, mas reduziria a quantidade de espaço não utilizado.

flob
fonte
2
Existe uma técnica adicional chamada "Chave Primária Compartilhada". Nesta técnica, as tabelas de subclasse não possuem um ID de chave primária atribuído independentemente. Em vez disso, a PK das tabelas da subclasse é a FK que faz referência à tabela da superclasse. Isso fornece vários benefícios, principalmente porque impõe a natureza individual dos relacionamentos IS-A. Essa técnica é um complemento para a Herança de tabela de classe.
22816 Walter Mitty #