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_states
e popular_countries
, que referência states
e countries
respectivamente. 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 places
tabela 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 states
e 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 states
e 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_id
na verdade é uma coluna com dois significados: é um estado ou um país. Você não iria armazenar uma pessoa de age
e sua phone_number
em uma única coluna, e pela mesma razão que você não deve armazenar tanto state_id
e country_id
em 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 Filters
tabelas, cada uma contendo uma chave estrangeira que faz referência a uma Products
tabela 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.
join
destino também mudará ...... Estou complicando demais ou o quê? Socorro!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,
places
onde cada linha tem um relacionamento um-para-um com uma linha emregions
,countries
oustates
. (Os atributos compartilhados entre regiões, países ou estados, se houver, podem ser enviados para essa tabela de locais.) Vocêpopular_place_id
seria 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.
fonte
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:O que esse design não pode garantir que, para cada linha existente
places
, exista uma linhastates
oucountries
(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.fonte
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 ...
Não tinha certeza de qual era o banco de dados de destino; o acima é MS SQL Server.
fonte
Bem, eu tenho duas tabelas:
a) Número da música b) Título da música ....
e eu tenho um terceiro
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:
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)
Isso é tudo!fonte