Tengo una tabla containers
que puede tener una relación de muchos a muchos con varias tablas, digamos que son plants
, animals
y bacteria
. Cada contenedor puede contener un número arbitrario de plantas, animales o bacterias, y cada planta, animal o bacteria puede estar en un número arbitrario de contenedores.
Hasta ahora, esto es muy sencillo, pero la parte con la que tengo un problema es que cada contenedor solo debe contener elementos del mismo tipo. Los recipientes mixtos que, por ejemplo, contienen plantas y animales deberían ser una violación de restricciones en la base de datos.
Mi esquema original para esto fue el siguiente:
containers
----------
id
...
...
containers_plants
-----------------
container_id
plant_id
containers_animals
------------------
container_id
animal_id
containers_bacteria
-------------------
container_id
bacterium_id
Pero con este esquema, no se me ocurre cómo implementar la restricción de que los contenedores deben ser homogéneos.
¿Hay alguna manera de implementar esto con integridad referencial y asegurar en el nivel de la base de datos que los contenedores sean homogéneos?
Estoy usando Postgres 9.6 para esto.
fuente
Respuestas:
Hay una manera de implementar esto declarativamente solo sin cambiar mucho su configuración actual, si acepta introducir algo de redundancia. Lo que sigue puede considerarse un desarrollo en la sugerencia de RDFozz , aunque la idea se formó completamente en mi mente antes de leer su respuesta (y de todos modos es lo suficientemente diferente como para justificar su propia publicación de respuestas).
Implementación
Esto es lo que haces, paso a paso:
Cree una
containerTypes
tabla similar a la sugerida en la respuesta de RDFozz:Rellene con ID predefinidos para cada tipo. Para el propósito de esta respuesta, permítales que coincidan con el ejemplo de RDFozz: 1 para plantas, 2 para animales, 3 para bacterias.
Agregue una
containerType_id
columnacontainers
y hágala no anulable y una clave foránea.Suponiendo que la
id
columna ya es la clave principal decontainers
, cree una restricción única en(id, containerType_id)
.Aquí es donde comienzan las redundancias. Si
id
se declara que es la clave principal, podemos estar seguros de que es única. Si es único, cualquier combinación deid
y otra columna también será única sin una declaración adicional de unicidad, entonces, ¿cuál es el punto? El punto es que al declarar formalmente el par de columnas único, dejamos que sean referibles , es decir, que sean el objetivo de una restricción de clave externa, que es de lo que trata esta parte.Añadir una
containerType_id
columna para cada una de las tablas de unión (containers_animals
,containers_plants
,containers_bacteria
). Convertirlo en una clave externa es completamente opcional. Lo crucial es asegurarse de que la columna tenga el mismo valor para todas las filas, diferente para cada tabla: 1 paracontainers_plants
, 2 paracontainers_animals
, 3 paracontainers_bacteria
, de acuerdo con las descripciones encontainerTypes
. En cada caso, también puede hacer que ese valor sea el predeterminado para simplificar sus declaraciones de inserción:En cada una de las tablas de unión, haga del par de columnas
(container_id, containerType_id)
una referencia de restricción de clave externacontainers
.Si
container_id
ya se definió como una referenciacontainers
, no dude en eliminar esa restricción de cada tabla, ya que ya no es necesario.Cómo funciona
Al agregar la columna de tipo de contenedor y hacer que participe en las restricciones de clave externa, prepara un mecanismo que evita que cambie el tipo de contenedor. Cambiar el tipo en el
containers
tipo sería posible solo si las claves externas se definieran con laDEFERRABLE
cláusula, que no se supone que estén en esta implementación.Incluso si fueran diferibles, cambiar el tipo seguiría siendo imposible debido a la restricción de verificación en el otro lado de la
containers
relación de la tabla de funciones. Cada tabla de unión permite solo un tipo de contenedor específico. Eso no solo evita que las referencias existentes cambien el tipo, sino que también evita la adición de referencias de tipo incorrecta. Es decir, si tiene un contenedor de tipo 2 (animales), solo puede agregarle elementos usando la tabla donde está permitido el tipo 2, que escontainers_animals
, y no podría agregar filas que lo hagan referencia, digamoscontainers_bacteria
, que acepta solo contenedores tipo 3.Finalmente, su propia decisión de tener diferentes tablas para
plants
,animals
ybacteria
, y diferentes tablas de unión para cada tipo de entidad, ya hace imposible que un contenedor tenga elementos de más de un tipo.Por lo tanto, todos estos factores combinados aseguran, de una manera puramente declarativa, que todos sus contenedores serán homogéneos.
fuente
Una opción es agregar
containertype_id
a laContainer
tabla. Haga que la columna NO sea NULL y que sea una clave externa para unaContainerType
tabla, que tendría entradas para cada tipo de elemento que puede ir en un contenedor:Para asegurarse de que el tipo de contenedor no se puede cambiar, cree un activador de actualización que verifique si
containertype_id
se actualizó y revierte el cambio en ese caso.Luego, en los desencadenadores de inserción y actualización en sus tablas de enlaces de contenedor, compruebe el containertype_id con el tipo de entidad en esa tabla, para asegurarse de que coincidan.
Si lo que pones en un contenedor tiene que coincidir con el tipo, y el tipo no se puede cambiar, entonces todo en el contenedor será del mismo tipo.
NOTA: Dado que el disparador en las tablas de enlaces es lo que decidirá qué coincide, si necesita tener un tipo de contenedor que pueda contener plantas y animales, puede crear ese tipo, asignarlo al contenedor y verificarlo . Por lo tanto, conserva la flexibilidad si las cosas cambian en algún momento (por ejemplo, obtiene los tipos "revistas" y "libros" ...).
OBSERVE el segundo: si la mayor parte de lo que sucede con los contenedores es igual, independientemente de lo que haya en ellos, entonces tiene sentido. Si suceden cosas muy diferentes (en el sistema, no en nuestra realidad física) en función del contenido del contenedor, entonces la idea de Evan Carroll de tener tablas separadas para los tipos de contenedores separados tiene mucho sentido. Esta solución establece que los contenedores tienen diferentes tipos en el momento de la creación, pero los mantiene en la misma tabla. Si tiene que verificar el tipo cada vez que realiza una acción en un contenedor, y si la acción que realiza depende del tipo, las tablas separadas en realidad podrían ser más rápidas y fáciles.
fuente
Si solo necesita 2 o 3 categorías (plantas / metazoos / bacterias) y desea modelar una relación XOR, tal vez un "arco" sea la solución para usted. Ventaja: no hay necesidad de disparadores. Se pueden encontrar diagramas de ejemplo [aquí] [1]. En su situación, la tabla de "contenedores" tendría 3 columnas con una restricción CHECK, permitiendo una planta, animal o bacteria.
Esto probablemente no sea apropiado si será necesario distinguir entre muchas categorías (por ejemplo, géneros, especies, subespecies) en el futuro. Sin embargo, para 2-3 grupos / categorías esto puede ser el truco.
ACTUALIZACIÓN: Inspirada en las sugerencias y comentarios del contribuyente, una solución diferente que permite muchos taxones (grupos de organismos relacionados, clasificados por biólogo) y evita nombres de tabla "específicos" (PostgreSQL 9.5).
Código DDL:
Datos de prueba:
Pruebas:
Gracias a @RDFozz y @Evan Carroll y @ypercube por su aporte y paciencia (leyendo / corrigiendo mis respuestas).
fuente
Primero, estoy de acuerdo con @RDFozz en la lectura de la pregunta. Sin embargo, él plantea algunas preocupaciones sobre la respuesta de stefans ,
Para abordar sus preocupaciones, solo
PRIMARY KEY
UNIQUE
restricciones para proteger contra entradas duplicadas.EXCLUSION
restricciones para garantizar que los contenedores sean "homogéneos"c_id
para garantizar un rendimiento decente.Así es como se ve,
Ahora puede tener un contenedor con varias cosas, pero solo un tipo de cosas en un contenedor.
Y todo está implementado en índices GIST.
La Gran Pirámide de Giza no tiene nada en PostgreSQL.
fuente
Esa es una mala idea.
Y ahora ya sabes por qué. =)
Creo que está estancado en la idea de herencia de la programación orientada a objetos (OO). La herencia OO resuelve un problema con la reutilización de código. En SQL, el código redundante es el menor de nuestros problemas. La integridad es ante todo. El rendimiento es a menudo el segundo. Nos deleitará el dolor por los dos primeros. No tenemos un "tiempo de compilación" que pueda eliminar los costos.
Así que simplemente renuncie a su obsesión por la reutilización del código. Los contenedores para plantas, animales y bacterias son fundamentalmente diferentes en todos los lugares del mundo real. El componente de reutilización de código de "contiene cosas" simplemente no lo hará por usted. Romperlos aparte. No solo le dará más integridad y más rendimiento, sino que en el futuro le resultará más fácil expandir su esquema: después de todo, en su esquema ya tuvo que separar los elementos que están contenidos (plantas, animales, etc.) , parece al menos posible que tengas que romper los contenedores. Entonces no querrá rediseñar todo su esquema.
fuente
plant_containers
, y así sucesivamente. Las cosas que solo necesitan un contenedor de plantas se seleccionan solo de laplant_containers
tabla. Las cosas que necesitan cualquier contenedor (es decir, buscar todos los tipos de contenedores) pueden hacerUNION ALL
en las tres tablas con contenedores.