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


Bueno, aquí está mi problema Tengo tres tablas; regiones, países, estados. Los países pueden estar dentro de regiones, los estados pueden estar dentro de regiones. Las regiones son la parte superior de la cadena alimentaria.

Ahora estoy agregando una tabla popular_areas con dos columnas; region_id y popular_place_id. ¿Es posible hacer que popular_place_id sea una clave foránea para cualquiera de los países O estados. Probablemente voy a tener que agregar una columna popular_place_type para determinar si el id es describir un país o estado de cualquier manera.

Author: Tommyixi, 2009-01-14

3 answers

Lo que estás describiendo se llama Asociaciones Polimórficas. Es decir, la columna "clave foránea" contiene un valor de id que debe existir en una de las tablas de destino. Por lo general, las tablas de destino están relacionadas de alguna manera, como ser instancias de alguna superclase común de datos. También necesitaría otra columna junto a la columna de clave foránea, para 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 manera de modelar Asociaciones polimórficas uso de restricciones SQL. Una restricción de clave foránea siempre hace referencia a una tabla de destino.

Las asociaciones polimórficas son soportadas por frameworks como Rails e Hibernate. Pero dicen explícitamente que debe deshabilitar las restricciones SQL para usar esta característica. En su lugar, la aplicación o el marco deben hacer un trabajo equivalente para garantizar que se cumple la referencia. Es decir, el valor de la clave foránea está presente en una de las posibles tablas de destino.

Polimórfico Las asociaciones 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.

Aquí hay algunas soluciones alternativas que aprovechan la integridad referencial impuesta por la base de datos:

Cree una tabla adicional por destino. Por ejemplo popular_states y popular_countries, que hacen referencia a states y countries, respectivamente. Cada uno de estos las tablas "populares" también hacen 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 necesita consultar ambas tablas. Pero significa que puede confiar en la base de datos para hacer cumplir la coherencia.

Cree una tabla places como supertable. Como menciona Abie, una segunda alternativa es que tus lugares populares hagan referencia a una tabla como places, que es un padre tanto de states como de countries. Es decir, tanto los estados como los países también tenga una clave foránea para places (incluso puede hacer que esta clave foránea también sea la clave primaria 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 pueda hacer referencia a cualquiera de las dos tablas de destino, utilice dos columnas. Estas dos columnas pueden ser NULL; de hecho, solo una de ellas debe 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 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 los age y los phone_number de una persona en una sola columna, y por la misma razón no deberías 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 extra que tabla a la que se refiere la clave foránea. En la Tercera Forma Normal, un atributo en una tabla debe depender solo de la clave primaria de esa tabla.


Comentario de @SavasVedova:

No estoy seguro de seguir su descripción sin ver las definiciones de tabla o una consulta de ejemplo, pero parece que simplemente tiene varias tablas Filters, cada una con una clave foránea que hace referencia a una tabla central Products.

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 sabes a qué tipo quieres unirte:

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 arregle 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 unir a todas las tablas de filtro usando uniones externas. Aquellos que no coinciden product_id solo se devolverá como una sola fila de nulos. Pero todavía tiene que codificar todas las tablas unidas, y si agrega nuevas tablas de filtro, tiene que 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 filtro 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.

 233
Author: Bill Karwin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-11-26 17:35:21

Esta no es la solución más elegante del mundo, pero podrías usar la herencia de la tabla de concreto para hacer que esto funcione.

Conceptualmente estás proponiendo una noción de una clase de "cosas que pueden ser áreas populares" de las cuales tus tres tipos de lugares heredan. Podría representar esto como una tabla llamada, por ejemplo, places donde cada fila tiene una relación uno a uno con una fila en regions, countries, o states. (Atributos que se comparten entre regiones, países o estados, si cualquiera, podría ser empujado en esta mesa lugares.) Su popular_place_id sería entonces una referencia de clave foránea a una fila en la tabla places que luego lo llevaría a una región, país o estado.

La solución que propone con una segunda columna para describir el tipo de asociación pasa a ser cómo Rails maneja las asociaciones polimórficas, pero no soy un fan de eso en general. Bill explica en excelente detalle por qué las asociaciones polimórficas no son tus amigos.

 9
Author: Abie,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-01-13 22:36:05

Aquí hay una corrección al enfoque "supertable" de Bill Karwin, usando una clave compuesta ( place_type, place_id ) para resolver las violaciones de forma normal percibidas:

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

Lo que este diseño no puede asegurar que para cada fila places existe una fila en states o countries (pero no ambos). Esta es una limitación de las claves foráneas en SQL. En un DBMS compatible con los estándares SQL-92 completo, podría definir restricciones diferibles entre tablas que le permitirían lograr lo mismo, pero es torpe, implica transacciones y tal DBMS aún no ha llegado al mercado.

 4
Author: onedaywhen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-21 12:55:57