¿Cómo manejas el polymorphism en una base de datos?

Ejemplo

Tengo Person , SpecialPerson y User . Person y SpecialPerson son solo personas: no tienen un nombre de usuario o una contraseña en un sitio, pero se almacenan en una base de datos para el mantenimiento de registros. El usuario tiene todos los mismos datos que Person y potencialmente SpecialPerson , junto con un nombre de usuario y una contraseña, ya que están registrados en el sitio.


¿Cómo abordarías este problema? ¿Tendría una tabla Person que almacene todos los datos comunes a una persona y use una clave para buscar sus datos en SpecialPerson (si es una persona especial) y User (si es un usuario) y viceversa?

En general, hay tres formas de asignar la herencia de objetos a las tablas de la base de datos.

Puede hacer una gran tabla con todos los campos de todos los objetos con un campo especial para el tipo. Esto es rápido pero desperdicia espacio, aunque las bases de datos modernas ahorran espacio al no almacenar campos vacíos. Y si solo busca a todos los usuarios en la tabla, con todo tipo de personas, las cosas pueden ir más despacio. No todos los mapeadores apoyan esto.

Puede crear tablas diferentes para todas las clases secundarias diferentes con todas las tablas que contienen los campos de clase base. Esto está bien desde una perspectiva de rendimiento. Pero no desde una perspectiva de mantenimiento. Cada vez que su clase base cambia todas las tablas cambian.

También puede hacer una tabla por clase como sugirió. De esta forma, necesitas uniones para obtener todos los datos. Entonces es menos eficiente. Creo que es la solución más limpia.

Lo que quiere usar depende, por supuesto, de su situación. Ninguna de las soluciones es perfecta, por lo que debe sopesar los pros y los contras.

Eche un vistazo a los patrones de architecture de aplicaciones empresariales de Martin Fowler:

  • Herencia de tabla única :

    Al mapear a una base de datos relacional, tratamos de minimizar las uniones que pueden boost rápidamente al procesar una estructura de herencia en múltiples tablas. La herencia de tabla única mapea todos los campos de todas las clases de una estructura de herencia en una sola tabla.

  • Herencia de tabla de clase :

    Desea estructuras de base de datos que se asignen claramente a los objetos y que permitan enlaces en cualquier lugar de la estructura de herencia. La herencia de tabla de clase lo admite al usar una tabla de base de datos por clase en la estructura de herencia.

  • Herencia de Mesa de Concreto :

    Pensando en las tablas desde el punto de vista de una instancia de objeto, una ruta razonable es tomar cada objeto en la memoria y asignarlo a una única fila de la base de datos. Esto implica la herencia de tabla concreta, donde hay una tabla para cada clase concreta en la jerarquía de herencia.

Si el usuario, la persona y la persona especial tienen todas las mismas claves foráneas, entonces tendré una sola tabla. Agregue una columna llamada Tipo que está restringido a ser Usuario, Persona o Persona especial. Luego, según el valor de Tipo, tiene restricciones en las otras columnas opcionales.

Para el código objeto, no hay mucha diferencia si tiene las tablas separadas o múltiples tablas para representar el polymorphism. Sin embargo, si tiene que hacer SQL en contra de la base de datos, es mucho más fácil si el polymorphism se captura en una sola tabla … siempre que las claves externas para los subtipos sean las mismas.

Lo que voy a decir aquí va a enviar arquitectos de bases de datos a los conniptions, pero aquí va:

Considere una vista de base de datos como el equivalente de una definición de interfaz. Y una mesa es el equivalente de una clase.

Entonces, en su ejemplo, todas las clases de 3 personas implementarán la interfaz IPerson. Entonces tiene 3 tablas, una para cada ‘Usuario’, ‘Persona’ y ‘EspecialPersona’.

Luego, tenga una vista ‘PersonView’ o lo que sea que seleccione las propiedades comunes (según lo define su ‘interfaz’) de las 3 tablas en la vista única. Use una columna ‘PersonType’ en esta vista para almacenar el tipo real de la persona que se está almacenando.

Entonces, cuando está ejecutando una consulta que puede ser operada en cualquier tipo de persona, simplemente consulte la vista de PersonView.

Esto podría no ser lo que el OP quería hacer, pero pensé que podría arrojar esto aquí.

Recientemente tuve un caso único de polymorphism de db en un proyecto. Tuvimos entre 60 y 120 clases posibles, cada una con su propio conjunto de 30 a 40 atributos únicos y entre 10 y 12 atributos comunes en todas las clases. Decidimos ir a la ruta SQL-XML y terminamos con una sola tabla. Algo como :

 PERSON (personid,persontype, name,address, phone, XMLOtherProperties) 

que contiene todas las propiedades comunes como columnas y luego una gran bolsa de propiedades XML. La capa ORM fue entonces responsable de leer / escribir las propiedades respectivas de XMLOtherProperties. Un poco como :

  public string StrangeProperty { get { return XMLPropertyBag["StrangeProperty"];} set { XMLPropertyBag["StrangeProperty"]= value;} } 

(Terminamos mapeando la columna xml como un archivo Hastable en lugar de XML, pero puede usar lo que mejor se adapte a su DAL)

No va a ganar ningún premio de diseño, pero funcionará si tiene un número grande (o desconocido) de clases posibles. Y en SQL2005 todavía puede usar XPATH en sus consultas SQL para seleccionar filas basadas en alguna propiedad que esté almacenada como XML. Es solo una pequeña penalización de rendimiento.

A riesgo de ser un “astronauta de la architecture” aquí, estaría más inclinado a ir con tablas separadas para las subclases. La clave principal de las tablas de subclase también debe ser una clave externa que vincule de nuevo al supertipo.

La razón principal para hacerlo de esta manera es que se vuelve mucho más lógicamente consistente y no se terminan con muchos campos que son NULOS y sin sentido para ese registro en particular. Este método también hace que sea mucho más fácil agregar campos adicionales a los subtipos a medida que itera su proceso de diseño.

Esto agrega la desventaja de agregar JOINs a sus consultas, lo que puede afectar el rendimiento, pero casi siempre voy con un diseño ideal primero, y luego miro para optimizarlo más adelante si resulta ser necesario. Las pocas veces que he tomado el camino “óptimo” primero casi siempre me arrepiento más tarde.

Entonces mi diseño sería algo así como

PERSONA (personid, nombre, dirección, teléfono, …)

ESPECIALISTA (personid REFERENCES PERSON (personid), campos adicionales …)

USUARIO (personid REFERENCES PERSON (personid), nombre de usuario, contraseña cifrada, campos adicionales …)

También puede crear VIEWs más adelante que agregue el supertipo y el subtipo, si es necesario.

El único defecto en este enfoque es si se encuentra buscando intensamente los subtipos asociados con un supertipo particular. No hay una respuesta fácil a esto fuera de mi cabeza, podrías rastrearla programáticamente si es necesario, o ejecutar más consultas globales y almacenar en caché los resultados. Realmente dependerá de la aplicación.

Yo diría que, dependiendo de lo que diferencie a la Persona y la Persona especial, probablemente no desee el polymorphism para esta tarea.

Crearía una tabla de usuario, una tabla de persona que tiene un campo de clave externa anulable para el usuario (es decir, la persona puede ser un usuario, pero no tiene que hacerlo).
Luego, crearía una tabla de SpecialPerson que se relaciona con la tabla Persona con campos adicionales en ella. Si un registro está presente en SpecialPerson para un Person.ID dado, él / ella / ella es una persona especial.

Hay tres estrategias básicas para manejar la herencia en una base de datos relacional, y una serie de alternativas más complejas / personalizadas según sus necesidades exactas.

  • Tabla por jerarquía de clases. Una tabla para toda la jerarquía.
  • Tabla por subclase. Se crea una tabla separada para cada subclase con una asociación 0-1 entre las tablas subclasificadas.
  • Tabla por clase concreta. Se crea una sola tabla para cada clase concreta.

Cada uno de estos participantes plantea sus propios problemas sobre normalización, código de acceso a datos y almacenamiento de datos, aunque mi preferencia personal es usar tabla por subclase a menos que haya un rendimiento específico o una razón estructural para ir con una de las alternativas.

En nuestra compañía tratamos el polymorphism combinando todos los campos en una tabla y su peor y no se puede forzar la integridad referencial y el modelo es muy difícil de entender. Recomendaría contra ese enfoque seguro.

Me gustaría ir con la tabla por subclase y también evitar el impacto en el rendimiento, pero usando ORM donde podemos evitar unirnos a todas las tablas de subclases construyendo una consulta sobre la marcha basándonos en el tipo. La estrategia antes mencionada funciona para la extracción de un solo nivel de registro, pero para la actualización masiva o seleccione no puede evitarla.

sí, también consideraría un TypeID junto con una tabla PersonType si es posible que haya más tipos. Sin embargo, si solo hay 3 que no deberían ser nec.

Esta es una publicación más antigua, pero pensé que evaluaría desde un punto de vista conceptual, procedimental y de rendimiento.

La primera pregunta que haré es la relación entre persona, persona especial y usuario, y si es posible que alguien sea tanto una persona especial como un usuario simultáneamente. O bien, cualquier otra de 4 combinaciones posibles (clase a + b, clase b + c, clase a + c, o a + b + c). Si esta clase se almacena como un valor en un campo de type y, por lo tanto, colapsaría estas combinaciones, y ese colapso es inaceptable, entonces creo que se requeriría una tabla secundaria que permita una relación de uno a varios. He aprendido que no juzgas eso hasta que evalúes el uso y el costo de perder tu información de combinación.

El otro factor que me hace inclinarme hacia una sola tabla es su descripción del escenario. User es la única entidad con un nombre de usuario (por ejemplo, varchar (30)) y contraseña (por ejemplo, varchar (32)). Si la longitud posible de los campos comunes es un promedio de 20 caracteres por 20 campos, entonces el aumento del tamaño de la columna es 62 sobre 400 o aproximadamente 15%. Hace 10 años esto hubiera sido más costoso que con los sistemas RDBMS modernos, especialmente con un tipo de campo como varchar (por ejemplo, para MySQL) disponible.

Y, si la seguridad le preocupa, podría ser ventajoso tener una tabla uno a uno secundaria llamada credentials ( user_id, username, password) . Esta tabla se invocaría en un JOIN contextualmente a la hora de inicio de sesión, pero estructuralmente separada de solo “anyone” en la tabla principal. Y, un LEFT JOIN está disponible para consultas que podrían considerar “usuarios registrados”.

Mi principal consideración durante años es todavía considerar la importancia del objeto (y, por lo tanto, la posible evolución) fuera del DB y en el mundo real. En este caso, todos los tipos de personas tienen corazones palpitantes (espero), y también pueden tener relaciones jerárquicas entre sí; entonces, en el fondo de mi mente, incluso si no es así, es posible que tengamos que almacenar esas relaciones por otro método. Eso no está explícitamente relacionado con su pregunta aquí, pero es otro ejemplo de la expresión de la relación de un objeto. Y ahora (7 años después) deberías tener una buena idea de cómo tu decisión funcionó de todos modos 🙂

Personalmente, almacenaría todas estas clases de usuarios diferentes en una sola tabla. Luego puede tener un campo que almacena un valor ‘Tipo’, o puede indicar qué tipo de persona está tratando con qué campos se rellenan. Por ejemplo, si ID de usuario es NULO, entonces este registro no es un Usuario.

Puede vincularse a otras tablas utilizando un tipo de unión uno a uno o ninguno, pero luego en cada consulta agregará combinaciones adicionales.

El primer método también es soportado por LINQ-to-SQL si decides ir por esa ruta (lo llaman ‘Table Per Hierarchy’ o ‘TPH’).

En el pasado lo hice exactamente como sugieres: tener una tabla Person para cosas comunes, y luego SpecialPerson vinculado para la clase derivada. Sin embargo, estoy reconsiderando que, como Linq2Sql quiere tener un campo en la misma tabla, indique la diferencia. Sin embargo, no he analizado demasiado el modelo de entidad, bastante seguro de que permite el otro método.