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!