sql >> Base de Datos >  >> RDS >> Mysql

¿Es posible hacer una clave externa de MySQL para una de las dos tablas posibles?

Lo que estás describiendo se llama asociaciones polimórficas. Es decir, la columna "clave externa" contiene un valor de identificación que debe existir en una de las tablas de destino. Por lo general, las tablas de destino están relacionadas de alguna manera, como instancias de alguna superclase común de datos. También necesitaría otra columna junto a la columna de clave externa, de modo que en cada fila pueda designar a qué tabla de destino se hace referencia.

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

No hay forma de modelar asociaciones polimórficas usando restricciones de SQL. Una restricción de clave externa siempre hace referencia a uno tabla de destino.

Las asociaciones polimórficas son compatibles con marcos como Rails e Hibernate. Pero dicen explícitamente que debe deshabilitar las restricciones de SQL para usar esta función. En su lugar, la aplicación o el marco debe hacer un trabajo equivalente para garantizar que se cumpla la referencia. Es decir, el valor de la clave externa está presente en una de las posibles tablas de destino.

Las asociaciones polimórficas son débiles con respecto a la aplicación de la coherencia de la base de datos. La integridad de los datos depende de que todos los clientes accedan a la base de datos con la misma lógica de integridad referencial aplicada, y también la aplicación debe estar libre de errores.

Estas son algunas soluciones alternativas que aprovechan la integridad referencial impuesta por la base de datos:

Cree una tabla adicional por objetivo. Por ejemplo popular_states y popular_countries , que hace referencia a states y countries respectivamente. Cada una de estas tablas "populares" también hace referencia al perfil del usuario.

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

Esto significa que para obtener todos los lugares favoritos populares de un usuario, debe consultar ambas tablas. Pero significa que puede confiar en la base de datos para hacer cumplir la coherencia.

Crea un places mesa como supermesa. Como menciona Abie, una segunda alternativa es que sus lugares populares hagan referencia a una tabla como places , que es padre de ambos states y countries . Es decir, tanto los estados como los países también tienen una clave externa para places (incluso puede hacer que esta clave externa también sea la clave principal de states y 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)
);

Utilice dos columnas. En lugar de una columna que puede hacer referencia a cualquiera de las dos tablas de destino, use dos columnas. Estas dos columnas pueden ser NULL; de hecho, solo uno de ellos debería ser no 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)
);

En términos de teoría relacional, las asociaciones polimórficas violan la primera forma normal , porque el popular_place_id es en efecto una columna con dos significados:es un estado o un país. No almacenarías la age de una persona y su phone_number en una sola columna, y por la misma razón no debe almacenar ambos state_id y country_id en una sola columna. El hecho de que estos dos atributos tengan tipos de datos compatibles es una coincidencia; todavía significan diferentes entidades lógicas.

Las asociaciones polimórficas también violan la Tercera forma normal , porque el significado de la columna depende de la columna adicional que nombra la tabla a la que se refiere la clave externa. En la Tercera Forma Normal, un atributo en una tabla debe depender solo de la clave principal de esa tabla.

Re comentario de @SavasVedova:

No estoy seguro de seguir su descripción sin ver las definiciones de la tabla o una consulta de ejemplo, pero parece que simplemente tiene varios Filters tablas, cada una de las cuales contiene una clave externa que hace referencia a un Products central mesa.

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 los productos a un tipo específico de filtro es fácil si sabe a qué tipo desea unirse:

SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)

Si desea que el tipo de filtro sea dinámico, debe escribir código de aplicación para construir la consulta SQL. SQL requiere que la tabla se especifique y se corrija en el momento de escribir la consulta. No puede hacer que la tabla unida se elija dinámicamente en función de los valores encontrados en filas individuales de Products .

La única otra opción es unirse a todos filtrar tablas usando uniones externas. Aquellos que no tengan un product_id coincidente se devolverán como una sola fila de valores nulos. Pero todavía tiene que codificar todo las tablas unidas, y si agrega nuevas tablas de filtro, debe actualizar su 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)
...

Otra forma de unirse a todas las tablas de filtros es hacerlo en serie:

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

Pero este formato aún requiere que escriba referencias a todas las tablas. No hay forma de evitar eso.