Mysql
 sql >> Base de Dados >  >> RDS >> Mysql

Possível fazer uma chave estrangeira do MySQL para uma das duas tabelas possíveis?


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 conjunto de tabelas de destino. Normalmente, as tabelas de destino estão relacionadas de alguma forma, como instâncias de alguma superclasse comum de dados. 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 um tabela de destino.

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

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 acessando o banco de dados com a mesma lógica de integridade referencial aplicada, e também a aplicação deve ser livre de bugs.

Aqui estão algumas soluções alternativas que aproveitam a integridade referencial imposta pelo banco de dados:

Crie uma tabela extra por destino. Por exemplo popular_states e popular_countries , que fazem referência a 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 lugares favoritos populares de um usuário, você precisa consultar essas duas tabelas. Mas isso significa que você pode confiar no banco de dados para impor consistência.

Crie um places tabela como uma supertabela. Como Abie menciona, uma segunda alternativa é que seus lugares populares façam referência a uma tabela como places , que é pai de ambos os states e countries . Ou seja, estados e países também têm uma chave estrangeira para 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 qualquer uma das duas tabelas de destino, use duas colunas. Essas duas colunas podem ser NULL; na verdade, 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 First Normal Form , porque o popular_place_id é na verdade uma coluna com dois significados:é um estado ou um país. Você não armazenaria a age de uma pessoa e seu phone_number em uma única coluna e, pelo mesmo motivo, você não deve armazenar os dois 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 tenho certeza se sigo sua descrição sem ver as definições da tabela ou uma consulta de exemplo, mas parece que você simplesmente tem vários Filters tabelas, cada uma contendo uma chave estrangeira que faz referência a um Products central tabela.
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...

Unir os produtos a um tipo específico de filtro é fácil se você souber em qual tipo deseja unir:
SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)

Se desejar que o tipo de filtro seja dinâmico, você 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 é juntar-se a todos filtrar tabelas usando junções externas. Aqueles que não têm product_id correspondente serão retornados apenas como uma única linha de nulos. Mas você ainda precisa codificar todos as tabelas unidas e, se você adicionar novas tabelas de filtro, 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 juntar-se a todas as tabelas de filtro é 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.