É possível fazer uma chave estrangeira do MySQL em uma das duas tabelas possíveis?

180

Bem, aqui está o meu problema, tenho três tabelas; regiões, países, estados. Os países podem estar dentro de regiões, os estados podem estar dentro de regiões. As regiões são o topo da cadeia alimentar.

Agora estou adicionando uma tabela popular_areas com duas colunas; region_id e popular_place_id. É possível tornar popular_place_id uma chave estrangeira para os países OU estados. Provavelmente vou ter que adicionar uma coluna popular_place_type para determinar se o ID está descrevendo um país ou estado de qualquer maneira.

Andrew G. Johnson
fonte

Respostas:

282

O que você está descrevendo é chamado de associações polimórficas. Ou seja, a coluna "chave estrangeira" contém um valor de ID que deve existir em um de um conjunto de tabelas de destino. Normalmente, as tabelas de destino estão relacionadas de alguma forma, como instâncias de alguma superclasse de dados comum. Você também precisaria de outra coluna ao lado da coluna de chave estrangeira, para que em cada linha, você possa designar qual tabela de destino é referenciada.

CREATE TABLE popular_places (
  user_id INT NOT NULL,
  place_id INT NOT NULL,
  place_type VARCHAR(10) -- either 'states' or 'countries'
  -- foreign key is not possible
);

Não há como modelar associações polimórficas usando restrições SQL. Uma restrição de chave estrangeira sempre faz referência a uma tabela de destino.

Associações polimórficas são suportadas por estruturas como Rails e Hibernate. Mas eles dizem explicitamente que você deve desativar as restrições SQL para usar esse recurso. Em vez disso, o aplicativo ou estrutura deve executar um trabalho equivalente para garantir que a referência seja satisfeita. Ou seja, o valor na chave estrangeira está presente em uma das tabelas de destino possíveis.

As associações polimórficas são fracas no que diz respeito à imposição da consistência do banco de dados. A integridade dos dados depende de todos os clientes que acessam o banco de dados com a mesma lógica de integridade referencial imposta e também a imposição deve estar livre de erros.

Aqui estão algumas soluções alternativas que tiram proveito da integridade referencial imposta pelo banco de dados:

Crie uma tabela extra por destino. Por exemplo popular_statese popular_countries, que referência statese countriesrespectivamente. Cada uma dessas tabelas "populares" também faz referência ao perfil do usuário.

CREATE TABLE popular_states (
  state_id INT NOT NULL,
  user_id  INT NOT NULL,
  PRIMARY KEY(state_id, user_id),
  FOREIGN KEY (state_id) REFERENCES states(state_id),
  FOREIGN KEY (user_id) REFERENCES users(user_id),
);

CREATE TABLE popular_countries (
  country_id INT NOT NULL,
  user_id    INT NOT NULL,
  PRIMARY KEY(country_id, user_id),
  FOREIGN KEY (country_id) REFERENCES countries(country_id),
  FOREIGN KEY (user_id) REFERENCES users(user_id),
);

Isso significa que, para obter todos os locais favoritos populares de um usuário, é necessário consultar essas duas tabelas. Mas isso significa que você pode confiar no banco de dados para reforçar a consistência.

Crie uma placestabela como uma supertabela. Como Abie menciona, uma segunda alternativa é que seus locais populares façam referência a uma tabela como places, que é pai de ambos statese countries. Ou seja, estados e países também possuem uma chave estrangeira places(você pode até fazer com que essa chave estrangeira também seja a chave primária de statese countries).

CREATE TABLE popular_areas (
  user_id INT NOT NULL,
  place_id INT NOT NULL,
  PRIMARY KEY (user_id, place_id),
  FOREIGN KEY (place_id) REFERENCES places(place_id)
);

CREATE TABLE states (
  state_id INT NOT NULL PRIMARY KEY,
  FOREIGN KEY (state_id) REFERENCES places(place_id)
);

CREATE TABLE countries (
  country_id INT NOT NULL PRIMARY KEY,
  FOREIGN KEY (country_id) REFERENCES places(place_id)
);

Use duas colunas. Em vez de uma coluna que pode fazer referência a uma das duas tabelas de destino, use duas colunas. Essas duas colunas podem ser NULL; de fato, apenas um deles deve ser não NULL.

CREATE TABLE popular_areas (
  place_id SERIAL PRIMARY KEY,
  user_id INT NOT NULL,
  state_id INT,
  country_id INT,
  CONSTRAINT UNIQUE (user_id, state_id, country_id), -- UNIQUE permits NULLs
  CONSTRAINT CHECK (state_id IS NOT NULL OR country_id IS NOT NULL),
  FOREIGN KEY (state_id) REFERENCES places(place_id),
  FOREIGN KEY (country_id) REFERENCES places(place_id)
);

Em termos de teoria relacional, as Associações Polimórficas violam a Primeira Forma Normal , porque popular_place_idna verdade é uma coluna com dois significados: é um estado ou um país. Você não iria armazenar uma pessoa de agee sua phone_numberem uma única coluna, e pela mesma razão que você não deve armazenar tanto state_ide country_idem uma única coluna. O fato de esses dois atributos terem tipos de dados compatíveis é coincidência; eles ainda significam diferentes entidades lógicas.

As associações polimórficas também violam a terceira forma normal , porque o significado da coluna depende da coluna extra que nomeia a tabela à qual a chave estrangeira se refere. Na Terceira Forma Normal, um atributo em uma tabela deve depender apenas da chave primária dessa tabela.


Re comentário de @SavasVedova:

Não sei se segui sua descrição sem ver as definições da tabela ou uma consulta de exemplo, mas parece que você simplesmente possui várias Filterstabelas, cada uma contendo uma chave estrangeira que faz referência a uma Productstabela central .

CREATE TABLE Products (
  product_id INT PRIMARY KEY
);

CREATE TABLE FiltersType1 (
  filter_id INT PRIMARY KEY,
  product_id INT NOT NULL,
  FOREIGN KEY (product_id) REFERENCES Products(product_id)
);

CREATE TABLE FiltersType2 (
  filter_id INT  PRIMARY KEY,
  product_id INT NOT NULL,
  FOREIGN KEY (product_id) REFERENCES Products(product_id)
);

...and other filter tables...

É fácil associar os produtos a um tipo específico de filtro se você souber a que tipo deseja se associar:

SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)

Se você deseja que o tipo de filtro seja dinâmico, deve escrever o código do aplicativo para construir a consulta SQL. O SQL requer que a tabela seja especificada e corrigida no momento em que você escreve a consulta. Você não pode fazer com que a tabela unida seja escolhida dinamicamente com base nos valores encontrados em linhas individuais de Products.

A única outra opção é associar-se a todas as tabelas de filtro usando associações externas. Aqueles que não possuem product_id correspondente serão retornados apenas como uma única linha de nulos. Mas você ainda precisa codificar todas as tabelas unidas e, se você adicionar novas tabelas de filtros, precisará atualizar seu código.

SELECT * FROM Products
LEFT OUTER JOIN FiltersType1 USING (product_id)
LEFT OUTER JOIN FiltersType2 USING (product_id)
LEFT OUTER JOIN FiltersType3 USING (product_id)
...

Outra maneira de ingressar em todas as tabelas de filtros é fazê-lo em série:

SELECT * FROM Product
INNER JOIN FiltersType1 USING (product_id)
UNION ALL
SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)
UNION ALL
SELECT * FROM Products
INNER JOIN FiltersType3 USING (product_id)
...

Mas esse formato ainda exige que você escreva referências a todas as tabelas. Não há como contornar isso.

Bill Karwin
fonte
Qual deles você sugeriria Bill? Estou no meio da criação de um banco de dados, mas estou perdido. Basicamente, preciso associar filtros a um produto e os valores dos filtros serão preenchidos em diferentes tabelas. Mas o problema é que os filtros serão gerados pelos administradores, portanto, dependendo do tipo de filtro, os dados podem variar e, portanto, o joindestino também mudará ...... Estou complicando demais ou o quê? Socorro!
Savas Vedova
+1 obrigado por uma solução incrível. Uma pergunta que tenho com a primeira / segunda solução é: existe alguma violação de normalização com o fato de que várias tabelas podem se referir à mesma chave primária nessa meta-tabela? Sei que você pode resolver isso com lógica, mas não vejo como o banco de dados aplicá-lo, a menos que esteja faltando alguma coisa.
21314 Rob
5
Eu realmente gosto de abordagem com "CONSTRAINT CHECK". Mas pode ser melhorado se mudarmos "OU" para "XOR". Dessa forma, assegurar que apenas uma coluna de conjunto não é NULL
alex_b
1
@alex_b, sim, isso é bom, mas o XOR lógico não é SQL padrão e não é suportado por todas as marcas SQL. O MySQL possui, mas o PostgreSQL não. A Oracle tem, mas a Microsoft não faz até 2016. E assim por diante.
Bill Karwin
1
"Estas duas colunas podem ser NULL; na verdade, apenas um deles deve ser não-NULL" - este seria violar 1NF!
precisa saber é o seguinte
10

Esta não é a solução mais elegante do mundo, mas você pode usar a herança de tabela concreta para fazer isso funcionar.

Conceitualmente, você está propondo uma noção de uma classe de "coisas que podem ser áreas populares" das quais seus três tipos de lugares herdam. Você poderia representar isso como uma tabela chamada, por exemplo, placesonde cada linha tem um relacionamento um-para-um com uma linha em regions, countriesou states. (Os atributos compartilhados entre regiões, países ou estados, se houver, podem ser enviados para essa tabela de locais.) Você popular_place_idseria uma referência de chave estrangeira para uma linha na tabela de locais que o levaria a uma região, país ou estado.

A solução que você propõe com uma segunda coluna para descrever o tipo de associação é como o Rails lida com associações polimórficas, mas eu não sou fã disso em geral. Bill explica em excelentes detalhes por que as associações polimórficas não são seus amigos.

Abie
fonte
1
aka "the
supertype
Também este artigo expalin bem o conceito duhallowgreygeek.com/polymorphic-association-bad-sql-smell
Marco Staffoli
5

Aqui está uma correção para a abordagem "supertabela" de Bill Karwin, usando uma chave composta ( place_type, place_id )para resolver as violações percebidas da forma normal:

CREATE TABLE places (
  place_id INT NOT NULL UNIQUE,
  place_type VARCHAR(10) NOT NULL
     CHECK ( place_type = 'state', 'country' ),
  UNIQUE ( place_type, place_id )
);

CREATE TABLE states (
  place_id INT NOT NULL UNIQUE,
  place_type VARCHAR(10) DEFAULT 'state' NOT NULL
     CHECK ( place_type = 'state' ),
  FOREIGN KEY ( place_type, place_id ) 
     REFERENCES places ( place_type, place_id )
  -- attributes specific to states go here
);

CREATE TABLE countries (
  place_id INT NOT NULL UNIQUE,
  place_type VARCHAR(10) DEFAULT 'country' NOT NULL
     CHECK ( place_type = 'country' ),
  FOREIGN KEY ( place_type, place_id ) 
     REFERENCES places ( place_type, place_id )
  -- attributes specific to country go here
);

CREATE TABLE popular_areas (
  user_id INT NOT NULL,
  place_id INT NOT NULL,
  UNIQUE ( user_id, place_id ),
  FOREIGN KEY ( place_type, place_id ) 
     REFERENCES places ( place_type, place_id )
);

O que esse design não pode garantir que, para cada linha existente places, exista uma linha statesou countries(mas não ambas). Esta é uma limitação de chaves estrangeiras no SQL. Em um DBMS completo compatível com os padrões SQL-92, você pode definir restrições diferíveis entre tabelas que permitiriam obter o mesmo, mas é desajeitado, envolve transações e esse DBMS ainda não foi lançado no mercado.

um dia quando
fonte
0

Percebo que esse tópico é antigo, mas vi isso e uma solução veio à mente e pensei em lançá-lo lá fora.

Regiões, países e estados são locais geográficos que vivem em uma hierarquia.

Você pode evitar completamente o problema criando uma tabela de domínio chamada geográfica_localização_tipo que você preencheria com três linhas (Região, País, Estado).

Em seguida, em vez das três tabelas de localização, crie uma única tabela de localização geográfica que possua uma chave estrangeira de localização_ geográfica_tipo_id (para saber se a instância é uma região, país ou estado).

Modele a hierarquia, fazendo esta tabela auto-referenciar para que uma instância State retenha o fKey na instância Country principal que, por sua vez, retenha o fKey na instância Region principal. Instâncias de região manteriam NULL nesse fKey. Isso não é diferente do que você faria com as três tabelas (você teria 1 - muitos relacionamentos entre região e país e entre país e estado), exceto que agora está tudo em uma tabela.

A tabela popular_user_location seria uma tabela de resolução de escopo entre user e georgraphical_location (muitos usuários podem gostar de muitos lugares).

Tããão ...

insira a descrição da imagem aqui

CREATE TABLE [geographical_location_type] (
    [geographical_location_type_id] INTEGER NOT NULL,
    [name] VARCHAR(25) NOT NULL,
    CONSTRAINT [PK_geographical_location_type] PRIMARY KEY ([geographical_location_type_id])
)

-- Add 'Region', 'Country' and 'State' instances to the above table


CREATE TABLE [geographical_location] (
   [geographical_location_id] BIGINT IDENTITY(0,1) NOT NULL,
    [name] VARCHAR(1024) NOT NULL,
    [geographical_location_type_id] INTEGER NOT NULL,
    [geographical_location_parent] BIGINT,  -- self referencing; can be null for top-level instances
    CONSTRAINT [PK_geographical_location] PRIMARY KEY ([geographical_location_id])
)

CREATE TABLE [user] (
    [user_id] BIGINT NOT NULL,
    [login_id] VARCHAR(30) NOT NULL,
    [password] VARCHAR(512) NOT NULL,
    CONSTRAINT [PK_user] PRIMARY KEY ([user_id])
)


CREATE TABLE [popular_user_location] (
    [popular_user_location_id] BIGINT NOT NULL,
    [user_id] BIGINT NOT NULL,
    [geographical_location_id] BIGINT NOT NULL,
    CONSTRAINT [PK_popular_user_location] PRIMARY KEY ([popular_user_location_id])
)

ALTER TABLE [geographical_location] ADD CONSTRAINT [geographical_location_type_geographical_location] 
    FOREIGN KEY ([geographical_location_type_id]) REFERENCES [geographical_location_type] ([geographical_location_type_id])



ALTER TABLE [geographical_location] ADD CONSTRAINT [geographical_location_geographical_location] 
    FOREIGN KEY ([geographical_location_parent]) REFERENCES [geographical_location] ([geographical_location_id])



ALTER TABLE [popular_user_location] ADD CONSTRAINT [user_popular_user_location] 
    FOREIGN KEY ([user_id]) REFERENCES [user] ([user_id])



ALTER TABLE [popular_user_location] ADD CONSTRAINT [geographical_location_popular_user_location] 
    FOREIGN KEY ([geographical_location_id]) REFERENCES [geographical_location] ([geographical_location_id])

Não tinha certeza de qual era o banco de dados de destino; o acima é MS SQL Server.

Ferramentas
fonte
0

Bem, eu tenho duas tabelas:

  1. músicas

a) Número da música b) Título da música ....

  1. listas de reprodução a) Número da lista de reprodução b) Título da lista de reprodução ...

e eu tenho um terceiro

  1. songs_to_playlist_relation

O problema é que alguns tipos de playlists têm links para outras playlists. Mas no mysql não temos chave estrangeira associada a duas tabelas.

Minha solução: vou colocar uma terceira coluna em songs_to_playlist_relation. Essa coluna será booleana. Se 1, então a música, o outro será vinculado à tabela da lista de reprodução.

Assim:

  1. songs_to_playlist_relation

a) Número da lista de reprodução (int) b) É a música (booleana) c) Número relativo (número da música ou número da lista de reprodução) (int) ( não é uma chave estrangeira para qualquer tabela)

 # criar músicas de mesa 
    consultas . anexar ( "SET SQL_MODE = NO_AUTO_VALUE_ON_ZERO;" ) 
    consultas . append ( "CREATE TABLE songs( NUMBERint (11) NOT NULL, SONG POSITIONint (11) NOT NULL, PLAY SONGtinyint (1) NOT NULL PADRÃO '1', SONG TITLEvarchar (255) CHARACTER SET utf8 COLLATE utf8_general_ci NÃO NULL, DESCRIPTIONvarchar (1000) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, ARTISTvarchar (255) CHARACTER SET utf8 COLLATE utf8_general_ci padrão NÃO nULO 'Άγνωστος καλλιτέχνης', AUTHORvarchar (255) CHARACTER SET utf8 COLLATE utf8_general_ci padrão NÃO nULO 'Άγνωστος στιχουργός', COMPOSERvarchar (255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστος συνθέτης',ALBUMvarchar (255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'ULTγνωστο άλμπουμ', YEARint (11) NOT NULL DEFAULT '33', RATINGint (11) NOT NULL DEFAULT '5', int (11) NOT NULL DEFAULT '5', IMAGEvarchar (600) CHARACTER SET utf8 COLLATE utf8 , SONG PATHvarchar (500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, SONG REPEATint (11) NOT NULL DEFAULT '0', VOLUMEfloat NOT NULL DEFAULT '1', SPEEDfloat NOT NULL DEFAULT '1') MOTOR = InnoDB DEFAULT CHARSET = utf8; " ) 
    consultas . append ( "ALTER TABLE songsADICIONAR CHAVE PRIMÁRIA ( NUMBER), ADICIONAR CHAVE ÚNICA POSITION( SONG POSITION), ADICIONAR CHAVE ÚNICA TITLE( SONG TITLE), ADICIONAR CHAVE ÚNICA PATH( SONG PATH);") 
    consultas. append ( "ALTER TABLE songsMODIFY NUMBERint (11) NÃO NULL AUTO_INCREMENT;" )

#create table playlists
queries.append("CREATE TABLE `playlists` (`NUMBER` int(11) NOT NULL,`PLAYLIST POSITION` int(11) NOT NULL,`PLAYLIST TITLE` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`PLAYLIST PATH` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;")
queries.append("ALTER TABLE `playlists` ADD PRIMARY KEY (`NUMBER`),ADD UNIQUE KEY `POSITION` (`PLAYLIST POSITION`),ADD UNIQUE KEY `TITLE` (`PLAYLIST TITLE`),ADD UNIQUE KEY `PATH` (`PLAYLIST PATH`);")
queries.append("ALTER TABLE `playlists` MODIFY `NUMBER` int(11) NOT NULL AUTO_INCREMENT;")

#create table for songs to playlist relation
queries.append("CREATE TABLE `songs of playlist` (`PLAYLIST NUMBER` int(11) NOT NULL,`SONG OR PLAYLIST` tinyint(1) NOT NULL DEFAULT '1',`RELATIVE NUMBER` int(11) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;")
queries.append("ALTER TABLE `songs of playlist` ADD KEY `PLAYLIST NUMBER` (`PLAYLIST NUMBER`) USING BTREE;")
queries.append("ALTER TABLE `songs of playlist` ADD CONSTRAINT `playlist of playlist_ibfk_1` FOREIGN KEY (`PLAYLIST NUMBER`) REFERENCES `playlists` (`NUMBER`) ON DELETE RESTRICT ON UPDATE RESTRICT")

Isso é tudo!

playlists_query = "SELECIONAR s1. *, s3. *, s4. * FROM músicas como s1 INNER JOIN` músicas da playlist` como s2 ON s1.`NÚMERO` = s2.Número RELATIVO` INNER JOIN `playlists` como s3 ON s3 .`NÚMERO` = s2.` NÚMERO DE LISTA DE REPRODUZIR 'JOGAR INTERIORES `playlists' como s4 EM s4.NÚMERO` = s2.NÚMERO RELATIVO` ENCOMENDAR POR s3. POSIÇÃO DE LISTA DE JOGOS`;
Chris P
fonte