ORM

Introducción:

La mayor parte de las aplicaciones actuales requieren algún tipo de esquema de persistencia, es decir, algún medio a través del cual almacenar la información una vez finalizada su ejecución y recuperarla cuando sea preciso.

La elección de un mecanismo de persistencia adecuado a los propósitos del sistema que se pretende desarrollar supone la toma de un conjunto de decisiones:

    • El tipo de sistema de base de datos a utilizar.

    • La forma en que la aplicación se comunicará con el mismo.

    • La distribución de la lógica: qué parte resolverá la aplicación y qué otra se delegará a mecanismos propios del sistema elegido (stored procedures, consultas precompliladas o vistas, etc).

    • El grado de automatización para los accesos (consultas, etc).

    • Los requerimientos de performance.

    • Etc.

En general, algunas de las cuestiones planteadas han encontrado cierto consenso a la hora de diseñar una solución. En cuanto a la elección del mecanismo de persistencia, nos encontramos, por ejemplo, con las bases de datos de objetos (OODBMS). Como su nombre lo indica, la forma en la que éstas organizan y almacenan la información se acerca bastante a la manera en que se trabaja con objetos y referencias en las aplicaciones OO, por lo que la transición entre aplicación y base de datos prometería ser ideal. Sin embargo, por diversas razones no han ganado la suficiente popularidad en el mercado, entre las que se encuentran la necesidad de la independencia de datos, es decir, que la información se encuentre almacenada de forma tal que sea útil a cualquier aplicación, desarrollada a través de cualquier tecnología.

La utilización de archivos para el mecanismo de persistencia trae aparejados sus propios inconvenientes. En el caso de los archivos XML habrá que unificar los conceptos de orientación a objetos con el enfoque jerárquico. Por otro lado, la cuestión del almacenamiento de la información impone necesidades en torno a la forma de organizar y recuperar los datos en forma eficiente, y un simple archivo no puede proporcionarnos ninguna ventaja en relación con estos aspectos.

Por su parte, las bases relacionales (RDBMS) han demostrado poseer características deseables a la hora de seleccionar un sistema de almacenamiento de la información:

    • Constituyen una aproximación robusta y flexible para el manejo de los datos.

    • Se encuentran soportadas por una teoría capaz de, entre otras cosas, asegurar la integridad de la información.

    • En general son independientes de la aplicación que haga uso de los datos. No dependen de ningún lenguaje ni paradigma de programación, por lo que pueden ser útiles a aplicaciones de la más variada naturaleza y tecnología.

    • Están sustentadas por estándares.

    • Han experimentado una mejora contínua a lo largo de los años en cuanto a la optimización en la forma de acceder y recuperar los datos.

    • Poseen una amplia aceptación en el mercado, y en general la mayoría de las organizaciones cuentan con una.

Habiendo escogido un sistema de estas características podríamos considerar resueltos algunos de los inconvenientes básicos que trae aparejado el desarrollo de una aplicación que requiera un mecanismo de persistencia de la información, ya que ciertas cuestiones se delegan al motor:

    • Almacenamiento, organización y recuperación de información estructurada.

    • Concurrencia e integridad de datos.

    • Administración de los datos compartidos.

Como vemos, la decisión a favor de un RDBMS parece convincente, sin embargo, aún resta abordar la cuestión de cómo comunicar la base de datos con nuestra aplicación para permitir que el estado de nuestros objetos pueda ser eventualmente almacenado en disco y recuperado tiempo más tarde.

Comunicación con la Base de Datos

La forma más elemental de comunicación entre una aplicación y una base de datos relacional debe permitir, mínimamente:

    • Conexión a la base.

    • Emisión de consultas y órdenes de actualización de registros.

    • Obtención de la información de respuesta desde la base de datos, para su posterior procesamiento.

En general, los lenguajes de programación proveen un medio standard de realizar estas operaciones (independientes de cada motor particular). En el caso particular de Java, la forma programática de acceder a una base relacional, se denomina JDBC (Java Database Connectivity).

JDBC consta de una colección de interfaces que dictan la forma en que una aplicación debe conectarse a una base relacional, y provee un conjunto de abstracciones para facilitar el acceso a los datos.

Cada proveedor de un sistema de Base de datos en particular llevará a cabo su propia implementación de las interfaces mencionadas, asegurándose de cumplir los contratos establecidos, y permitir, al mismo tiempo, explotar de la mejor forma las características que ofrece su motor particular.

De esta forma, quien desee conectar su aplicación a una base de datos mediante este esquema sencillo, deberá:

    • Utilizar la biblioteca correspondiente al proveedor elegido (algo así como un driver: Oracle, SQL Server, PostgreSQL, etc. poseen sus propias implementaciones).

    • Proporcionar un conjunto de parámetros de conexión para identificar la conexión a la base de datos.

Como vemos, la arquitectura es muy simple. Una capa se interpone en nuestra aplicación y el motor, abstrayéndonos de ciertos detalles elementales y permitiéndonos el acceso y modificación de la información:

Las interfaces más representativas que provee JDBC para la comunicación con la base de datos son las siguientes:

o <<i>>Connection: Representa la conexión con la base de datos.

o <<i>>Statement: Representa la instrucción que se va a ejecutar.

execute: ejecuta la consulta y devuelve el número de registros afectados

executeQuery: ejecuta la consulta y devuelve un cursor que representa al resultado de la misma

o <<i>>PreparedStatement: Parecido al statement, parametrizable, y cacheado. Además, provee un mayor grado de seguridad ante SQL injections.

SELECT … FROM Persona WHERE nombre=? à Parámetro anónimo. Se usa por posición (setParameter(0, valor)).

SELECT … FROM Persona WHERE nombre=:nombre à Parámetro con nombre (setParameter("nombre", valor)).

o <<i>>ResultSet: Representa un cursor (una matriz) con el resultado de una consulta (filas= registros, y columnas).

La forma más básica de recorrerlo es forward only. Sólo puedo leer el registro actual y avanzar.

La manera en que cada proveedor implementa estas interfaces, remite al patrón Abstract Factory, ya que éste hace foco en la construcción de familias de clases relacionadas.

Así, por ejemplo, en el driver de SQL Server, se buscará mantener coherencia entre los objetos utilizados (no podemos mezclar un Statement de SQL Server con un ResultSet de Oracle). Este es un buen ejemplo de utilización del patrón, a pesar de no ser de los más usados entre los creacionales, por no encontrarnos ante esta situación muy habitualmente en la práctica.

A continuación, un ejemplo de cómo llevaríamos a cabo los pasos básicos, enumerados al principio, haciendo uso de este esquema sencillo:

//Conexión e inicialización

Class.forName("DriverClass");

Connection con = DriverManager.getConnection

( "jdbc:driver://server", "username","pass");

Statement stmt = null;

ResultSet rs = null;

List<Persona> personas = new ArrayList<Persona>();

//Ejecución de la consulta.

try{

stmt = con.createStatement();

rs = stmt.executeQuery("SELECT edad, nombre, dni FROM Persona");

//Procesamiento de los datos.

while (rs.next()) {

Persona p = new Persona();

p.setEdad(rs.getInt("edad"));

p.setNombre(rs.getString("nombre");

p.setDni(rs.getLong("dni");

personas.add(persona);

}

} catch(SQLException e) {

//Tratar excepción...

} finally{

if (rs!=null)

rs.close();

if (ps!=null)

ps.close();

//cerrar conexión...

}

Como mencionamos anteriormente, los datos se obtienen en forma tabular, es decir, su organización corresponde a la de un cursor, o matriz, y para trabajar con los mismos debemos recorrer esta estructura y solicitar la información como si se tratara de un conjunto de campos individuales. La programación resulta en una mezcla de código propio del lenguaje utillizado y el código de consultas propietario (SQL) del motor en particular.

Si bien la forma en que logramos obtener los datos y trabajar con ellos parece simple, este pequeño ejemplo deja a entrever algunas de las dificultades que impone este tipo de esquema:

    • Se utilizan abstracciones que representan elementos de bajo nivel (conexiones, sentencias, etc.), y tienen más que ver con la naturaleza de la conexión y el tipo de comunicación que con el problema de negocio que se intenta resolver.

    • El código de negocio (marcado en azul) es mínimo respecto al involucrado en el mantenimiento y cierre de la conexión.

    • Se debe prestar atención a ciertas reglas precisas para facilitar la comunicación, y al tratamiento de los errores que puedan surgir en el proceso.

    • El hecho de que tengamos el código SQL de un motor específico entremezclado con el código de nuestra aplicación puede resultar inmantenible, ya que un cambio de motor supondría reemplazar la totalidad del código de acceso a la base de datos en distintas partes del programa.

    • Al tratarse de simples Strings, las consultas no posibilitan la detección automática de los potenciales errores por parte de los IDEs u otras herramientas, ni de los chequeos en tiempo de compilación que proporciona un lenguaje tipado. Sería interesante acercar el código de consultas un poco más al mundo familiar de objetos con el que trabajamos.

    • La forma de recorrer los datos y asignarlos a las variables para su posterior utilización (mapeo de datos en objetos) supone un esfuerzo repetitivo y propenso a errores. Sería interesante contar con una alternativa que respete el principio de Once and Only Once.

Como vemos, estos inconvenientes son comunes a la mayor parte de las aplicaciones basadas en objetos que buscan persistir sus datos, después de todo sólo deseamos escribir código que permita almacenar y recuperar objetos.

Podemos mencionar una evolución de este enfoque simple, a través de un patrón de diseño, que permite separar un tanto nuestra lógica de negocio de aquella implicada en el acceso a una base de datos, el patrón DAO (Data Access Object).

Éste provee una interfaz, capaz de aislar a nuestra aplicación de la tecnología de persistencia subyacente:

Como vemos, nuestros objetos de negocio (BusinessObjects) se comunican con otros (DataAccessObjects), que encapsulan la lógica necesaria para acceder a la base de datos (DataSource) y de esta manera, recuperar, almacenar o actualizar la información.

El resultado obtenido por los DAOs (TransferObject) se comparte con nuestra capa de negocio para permitirle resolver su problématica. A ésta, sólo le interesan los datos, los TransferObjects , los cuales no exponen por sí mismos ningún detalle del mecanismo de persistencia utilizado para obtenerlos.

Gracias a este patrón hemos logrado solucionar algunos de nuestros problemas iniciales:

    • Alejamos la lógica de acceso a la base de datos, enfatizando de esta forma el código de negocio.

    • Centralizamos los accesos al medio persistente en una capa separada.

    • Facilitamos la migración a otro motor, ya que un cambio en la base de datos no necesitaría extender las actualizaciones más allá de los objetos utilizados para acceder a la misma.

Sin embargo, a pesar de constituir el patrón DAO una buena práctica, aún no hemos logrado superar todos los inconvenientes planteados al comienzo.

Dentro de cada DAO continúa siendo necesaria la tarea de mapeo manual entre los resultados obtenidos y los objetos utilizados para transportar la información hacia el sector de utilización dentro del programa. Además, si bien, el impacto de un cambio se ve notablemente reducido, la cantidad de código SQL propietario empleado continúa siendo importante y una modificación en torno al motor involucra un esfuerzo de mantenimiento considerable.

Es necesario mencionar, sin embargo, que el patrón mencionado se emplea en conjunto con otras aproximaciones más avanzadas, como las que mencionaremos a continuación. Por lo tanto, no lo descartamos y veremos más adelante cómo integrarlo con éstas.

Sería entonces deseable recurrir a una solución genérica y reutilizable, adaptable a la mayor parte de los casos, y desarrollada independientemente del sistema que pretendemos llevar a cabo. En general, es normal y deseable agrupar los componentes encargados de ciertas tareas de persistencia en una capa adicional, habitualmente provista por los frameworks, cuyos desarrolladores se han abocado a la resolución del problema particular y, por lo tanto, es natural pensar que intentarán ofrecer las mejores alternativas al abordar cada uno de los inconvenientes que presente la tarea.

En el caso de los problemas de persistencia para Java, los frameworks se ubican siempre por encima de JDBC, ya que este es el medio standard de acceso a un motor relacional, y proporcionan mayores niveles de abstracción y distintas comodidades a la hora de desarrollar nuestras aplicaciones, haciéndose cargo de esas tediosas tareas de bajo nivel que tanto nos alejan de nuestro foco principal: el negocio.

En el presente documento analizaremos uno de los enfoques vigentes en la actualidad, el de los ORM (Object Relational Mappers), que han demostrado abordar las cuestiones planteadas y dar una respuesta eficiente al problema de la persistencia de los datos en un ambiente de objetos.

Object Relational Mapping

El mapeo objeto-relacional, es una técnica de programación por la cual se busca disminuir la brecha existente entre la forma en que manejamos los datos y tipos en nuestra aplicación orientada a objetos, y la manera de hacerlo en las bases de datos relacionales, a través del mapeo automático y transparente de objetos en tablas.

Se busca, entonces, lograr la apariencia de una base de datos de objetos virtual, ubicada sobre la base relacional propiamente dicha, para posiblitar un desarrollo completamente orientado a objetos.

Nuestra arquitectura primitiva, que constaba de una capa sencilla que se interponía entre nuestra aplicación y la base de datos para facilitar la conexión (JDBC), se verá modificada por este enfoque debido al aumento de la complejidad en función de lograr el objetivo mencionado. Así, más que una pequeña capa intermedia tendremos prácticamente un motor, un object relational mapper.

Para acompañar los conceptos de ORM que iremos viendo, utilizaremos un Framework OpenSource muy popular en el mercado llamado Hibernate, en plataformas Java, y que cuenta con su versión NHibernate para .NET. La arquitectura de nuestra aplicación, a partir del agregado del motor de ORM se asemeja más a este esquema:

Esta complejidad adicional en una capa intermedia no viene gratuitamente y su costo puede traducirse en una disminución de la performance. Sin embargo, en muchos casos esta pérdida de rendimiento se compensa favorablemente por la ganancia en torno a la mantenibilidad y portabilidad, entre otras características.

Persistencia en un Ambiente de Objetos

Si bien es cierto que gran parte de la lógica de negocio puede delegarse a un motor, a través de stored procedures, por ejemplo, muchas veces la resolución de los problemas de negocio se ve beneficiada por la utilización de ciertos conceptos característicos de la orientación a objetos, como herencia, polimorfismo, patrones de diseño, etc. Como sabemos, algunos de estos conceptos actúan favorablemente sobre ciertas características deseables de nuestros programas, como reutilización y mantenibilidad.

Es lógico, entonces, que en algunas oportunidades del lado de la aplicación exista un modelo de dominio, y que éste guarde cierta relación con las entidades presentes en los esquemas de la base de datos. Así, en lugar de trabajar con una representación tabular de los datos (filas y columnas), se busca contar con entidades representativas. Por ejemplo, si en la base de datos se encuentran las entidades "Alumno" y "Curso", sería lógico suponer que en la aplicación se cuente con objetos que representen estas abstracciones y sus relaciones de forma más o menos análoga.

Sin embargo, la traducción entre entidades relacionales y objetos, no es tan simple como parece. Ante todo, una tabla no es una clase, un atributo no es una columna y en muchas ocasiones las características favorables que ofrece un paradigma no encuentran su contraparte análoga en el otro. Esto se conoce como "impedance mismatch".

A continuación se enumeran algunas de las diferencias más importantes, luego se dará un tratamiento especial a cada una de ellas, para demostrar qué propuestas ofrece un ORM para superarlas:

    • Subtipos: El paradigma de objetos provee la característica de la herencia de clases, a través de la cual puede generalizarse una abstracción, y hacer que de ésta hereden otras más específicas. Gracias a esto se pueden explotar conceptos interesantes, como el polimorfismo y la reutilización. En las bases relacionales, la cuestión de la herencia de tablas no se encuentra estandarizada. En muchas ocasiones ni siquiera se provee esta característica, y en aquellas en que sí es posible utilizarla, se puede quedar expuesto a problemas de integridad de datos.

    • Identidad: En objetos, la cuestión acerca de la identidad se desdobla en dos conceptos: el de identidad propiamente dicha y el de igualdad. El primero está más relacionado con la posición de memoria que ocupa un objeto, de esta forma dos objetos serán idénticos si ocupan la misma posición, o, dicho en otras palabras, son el mismo objeto (en Java el operador de identidad es ==). La igualdad (asociada al método equals() en Java), permite definir cuándo dos objetos son iguales para los propósitos de nuestra aplicación, por ejemplo, basándonos en algún atributo del mismo, pero sin requerir que se trate de objetos idénticos (podemos establecer por ejemplo, que dos personas sean iguales si poseen el mismo DNI). En las bases de datos, por su parte, la identidad se representa mediante el concepto de clave primaria y se refiere a la unicidad de un registro dentro de una tabla. En realidad ni la identidad, ni la igualdad en objetos se amoldan naturalmente al concepto de identidad en una base de datos. Así encontraremos casos donde, por ejemplo, dos objetos no idénticos pueden referenciar un mismo registro de una determinada tabla.

  • Asociaciones:

      • En objetos las asociaciones se dan a través de referencias, en el enfoque relacional se utilizan claves foráneas.

      • Las asociaciones en objetos son direccionales ("navegables"). En el paradigma relacional, las asociaciones son no direccionales, y arbitrariamente pueden crearse asociaciones de datos mediantes joins, por ejemplo.

      • En objetos pueden darse relaciones muchos a muchos. En las bases relacionales para lograr este tipo de relaciones se requiere de una entidad adicional, que relacione las dos tablas intervinientes, y que no estaría presente en el modelo de domino de objetos con que contemos, por carecer de sentido.

    • Navegación: La forma de recorrer las asociaciones de objetos es a través de sus referencias. Muchas veces se siguen largas cadenas de punteros en busca un de objeto particular. Esta puede no ser una forma eficiente de recuperar la información desde una base de datos, ya que cada pedido adicional supone una inversión en torno a la performance. Por otro lado, en el caso de una entidad persistente deben tenerse en cuenta cuestiones adicionales, como si es deseable traer al mismo tiempo que el objeto recuperado, las cadenas de objetos que se encuentren relacionadas con éste. Se plantea la cuestión de muchos selects pequeños vs. pocos selects grandes, por ejemplo. Un ORM debe también brindar facilidades para posibilitar las decisiones en este tipo de situaciones.

    • Comportamiento: En el paradigma de objetos trabajamos con entidades que poseen, además de estado, comportamiento. En el paradigma relacional, las entidades son meros repositorios de datos con cierta semántica, que se encuentran relacionadas. La consecuencia de esta diferencia es que muchas veces en objetos tendrá sentido que cierta entidad tenga razón de existir en un dominio porque, a pesar de poseer poco o ningún estado, añade significado semántico y comportamiento. Esta entidad podría simplemente existir como una columna de una tabla en un enfoque relacional, por lo que, en general, nos encontraremos con más clases que tablas. Este problema se refiere al nivel de granularidad y debe ser abordado también.

Estos son algunos de los inconvenientes que caracterizan el mismatch, sin embargo no toda la problemática a la que se enfrenta un ORM reside en la diferencia entre los dos paradigmas.

Al intentar una solución de la forma más transparente posible, nos encontramos a su vez con decisiones en torno a cómo mapear las diferencias entre tablas y objetos de forma más conveniente.

Podríamos plantear varias soluciones. Una sería utilizar Reflection, obtener el nombre de las clases de dominio, sus atributos, y mapearlas 1 a 1 con las tablas y sus respectivas columnas. Sin embargo, como vimos la relación entre objetos y entidades relacionales no siempre es 1 a 1, existen importantes diferencias, por lo que necesitamos valernos de un mecanismo de mapeo más expresivo, que permita especificar información adicional. De esta manera, recurrimos a la metadata (como archivos XML o Annotations), para colocar en un solo lugar las diferencias y asociar cada atributo con su columna correspondiente sin ambiguedades.

A continuación analizaremos los problemas planteados al comienzo respecto al mismatch.

El problema de la Herencia

Como vimos este mecanismo no está estandarizado dentro de las bases de datos, por lo que debemos seleccionar una alternativa que, permitiéndonos continuar utilizando la herencia dentro de nuestras clases de dominio no nos comprometa a una solución aportada por un proveedor de bases de datos particular.

En general hay tres estrategias principales para representar la herencia sobre tablas. A continuación mencionaremos cada una de ellas, junto con sus ventajas y desventajas. Emplearemos el mapeo a través de annotations. Para mayor información acerca de mapeos, recurrir al apunte de Mapping y Relaciones.

Tabla por clase concreta:

La idea detrás de esta estrategia es crear una tabla por cada clase concreta, que contenga los atributos propios de la clase en cuestión, junto con aquellos que herede de su superclase.

Supongamos que nuestro dominio cuenta una clase abstracta Persona y dos subclases concretas: Alumno y Docente.

Llevaremos a cabo el mapeo de las clases, mediante annotations, para mostrar cómo funciona este esquema.

Por default, Hibernate considera a las propiedades de las superclases como no persistentes. Por esta razón debemos indicar explícitamente en nuestra clase Persona, la estrategia de herencia que utilizaremos en sus subclases:

@Entity

@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)

public abstract class Persona {

@Id

private Long dni;

@Column

private String nombre;

}

Para el presente ejemplo tomaremos el DNI de la persona como su ID (o clave primaria en el caso de una BD). Como se verá luego, los ORM plantean diversas estrategias para determinar la mejor clave que identifique a una determinada entidad. Por motivos de simplicidad, se pospone esa discusión para el momento en que se aborde la cuestión de identificadores, y se acepta el atributo DNI como Id provisoriamente.

A continuación, mapearemos las clases concretas:

@Entity

public class Alumno extends Persona{

@Column

private Integer legajo;

@Column(name = "FECHA_INGRESO")

private Date fechaIngreso;

public Alumno() {

}

}

@Entity

public class Docente extends Persona{

@Column(name = "COD_DOCENTE")

private Integer codDocente;

@Column(name = "TIPO_DOCENTE")

private Integer tipoDocente;

public Docente() {

}

}

Si suponemos que Persona es abstracta, tendremos sólo dos tablas en nuestro de dominio. Cada tabla, tendrá una columna por cada atributo que le es propio a su clase análoga, y una adicional por cada atributo que dicha clase herede de su superclase. En el ejemplo, ALUMNO tendrá las columnas LEGAJO y FECHA_INGRESO que le son propias, más DNI y NOMBRE que hereda de su clase padre. Lo mismo ocurrirá con DOCENTE.

En los mapeos, debemos tener en cuenta que:

La relación de herencia queda manifestada a través de la metadata, lo que permite que nuestro motor de ORM la comprenda. Sin embargo, a nivel de tablas no hay ninguna columna que indique esta relación, solamente se observan columnas iguales en tablas distintas (en el caso del ejemplo, DNI y NOMBRE aparecen en las tablas ALUMNO y DOCENTE). El mecanismo utilizado en el paradigma relacional para asociar tablas (foreign keys) no se emplea en esta estrategia. Podríamos decir que utilizamos un enfoque transparente para representar la herencia.

Supongamos que deseemos traer un conjunto de "personas" sin indicar el tipo concreto de las mismas (por ejemplo, en el caso de que una clase posea una colección de personas, ya sea de alumnos o docentes, y se quieran traer desde la base de datos). A este tipo de consultas se las denomina "consultas polimórficas", y obedece al concepto de polimorfismo en nuestro paradigma de objetos.

Hibernate enivará al motor la siguiente sentencia, en la que utiliza un subselect con un UNION y utiliza una columna constante (CLAZZ_), la cual le permitirá saber qué clase instanciar por cada registro que devuelva la consulta:

SELECT DNI, NOMBRE, LEGAJO, FECHA_INGRESO, COD_DOCENTE, TIPO_DOCENTE, CLAZZ_

FROM (SELECT DNI, NOMBRE, APELLIDO, LEGAJO, FECHAINGRESO, NULL as CODDOCENTE, NULL as

TIPODOCENTE, 1 as CLAZZ_ FROM ALUMNO

UNION

SELECT DNI, NOMBRE, APELLIDO, NULL AS LEGAJO, NULL AS FECHAINGRESO, CODDOCENTE,

TIPODOCENTE, 2 as CLAZZ_ FROM DOCENTE)

Es necesario aclarar que los campos que se rellenan con null son necesarios para poder efectuar el union, que requiere que se realice sobre el mismo número de columnas en ambas sentencias.

Como ventaja de este esquema podemos mencionar:

    • Favorece el acceso directo a tablas de clases concretas, ya que no requiere un join con otras tablas. Es decir, si queremos traer alumnos o docentes, en lugar de personas, es un enfoque eficiente.

Como desventaja:

    • La repetición de columnas en cada tabla, lo que puede desembocar en un diseño desnormalizado de la base de datos.

Esta estrategia es ideal para los casos en que se realicen escasas consultas polimórficas y los accesos estén frecuentemente orientados a las clases concretas. Para el caso en que periódicamente se realizaran queries a las superclases, la utilización de UNIONS no implica grandes inconvenientes a la performance. Por supuesto, la elección depende también del modelo. Un diseño que implique que una entidad posea una colección de personas y deba acceder frecuentemente a éstas podría traer dificultades bajo este esquema.

Tabla por Jerarquía o Single Table:

Esta estrategia apunta a representar toda la jerarquía a través de una única tabla. En este caso, las columnas de la tabla surgen de todos los atributos de todas las clases intervinientes, más una columna adicional que identifica el tipo concreto.

En nuestro ejemplo, el mapeo resultante sería:

@Entity

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)

@DiscriminatorColumn(name = "TIPO_PERSONA", discriminatorType = DiscriminatorType.STRING)

public abstract class Persona {

@Id

private Long dni;

@Column

private String nombre;

}

@Entity

@DiscriminatorValue("ALU")

public class Alumno extends Persona{

@Column

private Integer legajo;

@Column(name = "FECHA_INGRESO")

private Date fechaIngreso;

public Alumno() {

}

}

El discriminator, indica la columna adicional que existirá en la tabla para permitirle al motor de ORM identificar la clase concreta que deberá instanciar a partir del registro recibido.

En caso de estar utilizando una base Legacy, sobre la cual no tenemos la posibilidad de definir columnas nuevas, como la que necesitamos para el discriminator (típicamente cuando optamos por un esquema bottom up) añaidmos una fórmula en la superclase, para identificar la clase concreta a instanciar:

@Entity

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)

@org.hibernate.annotations.DiscriminatorFormula("case when LEGAJO is not null then 'ALU' else 'DOC' end")

public abstract class Persona {

@Id

private Long dni;

@Column

private String nombre;

}

Para el caso de las consultas polimórficas, nuestro motor de ORM emitirá un simple query a la base de datos, y en caso de particularizar nuestra consulta para una determinada clase concreta, añadirá una cláusula WHERE, donde filtrará por el nombre del discriminador (en el ejemplo, WHERE PERSONA_TIPO = 'ALU').

Como se observa, esta estrategia representa las siguientes ventajas:

    • Es la alternativa más performante para consultas polimórficas y no polimórficas, por no requerir joins ni unions.

    • Se basa en un diseño simple.

Entre sus desventajas encontramos:

    • Implica caer en la desnormalización a un grado que puede comprometer la evolución del esquema en un futuro y llevar a un diseño inmantenible.

    • Se desperdicia espacio de almacenamiento, por la existencia de registros con varios campos nulos.

    • Las columnas que representen atributos de las subclases concretas, no pueden tener restricciones de NOT NULL. Esto puede atentar contra la integridad de los datos y el mantenimiento de reglas de negocio del lado de la base.

Esta alternativa resulta muy conveniente para diseños que requieran frecuentes accesos a superclases y en los que las subclases difieran de éstas mayormente en comportamiento, y poco en estado. Como se observó el ORM resuelve de forma sencilla la instanciación de los objetos de las clases concretas en tiempo de ejecución.

Joined Subclass:

Este esquema es el más parecido al de la herencia propiamente dicha. Aquí se requiere una tabla por cada clase (sea abstracta o concreta). De esta forma, cada entidad con atributos persistentes tiene su propia tabla.

Las tablas de las subclases tendrán una columna por cada atributo que le es propio. A éstas habrá que sumar un ID, que será, por un lado primary key de esta tabla, y a la vez, foreign key a la tabla que represente a la clase padre.

Para nuestro ejemplo, contaremos con tres tablas:

    • La clase Persona, a pesar de ser abstracta tendrá una tabla PERSONA, con el dni como PK, y el resto de sus atributos persistentes, ya mencionados.

    • La clase Alumno tendrá su propia tabla cuyas columnas estarán integradas por sus respectivos atributos, más un ID que será su PK y a la vez FK a la columna DNI de la tabla Persona.

    • El caso de Docente es análogo al de Alumno.

El proceso de dar de alta, por ejemplo, un Alumno, requerirá insertar un registro en la tabla Alumno con los datos correspondientes, y otro en la tabla Persona con los campos restantes. Como vemos, cada alta requiere dos INSERTS al menos, y la recuperación requerirá un JOIN entre ALUMNO o DOCENTE (según corresponda) y PERSONA.

El mapeo para Persona sería:

@Entity

@Inheritance(strategy = InheritanceType.JOINED)

public abstract class Persona {

@Id

private Long dni;

@Column

private String nombre;

}

Si en la subclases no quiero que la columna que haga de PK y FK tenga un nombre distinto al de la tabla padre, no necesito añadir metadata adicional.

Si quisiera que tenga un nombre diferente, debo mapear la clase de esta forma:

@Entity

@PrimaryKeyJoinColumn(name = "ALU_ID")

public class Alumno extends Persona{

...

}

Para consultas polimórficas (por ejemplo, traer un conjunto de Personas) la consulta requerirá utilizar outer joins:

SELECT P.DNI, P.NOMBRE, P.APELLIDO, A.LEGAJO, A.FECHAINGRESO, D.CODDOCENTE, D.TIPODOCENTE,

CASE WHEN A.ALUMNO_DNI IS NOT NULL THEN 1

WHEN D.DOCENTE_DNI IS NOT NULL THEN 2

END AS CLAZZ_

FROM PERSONA P LEFT JOIN ALUMNO A ON P.DNI = A.ALUMNO_DNI

LEFT JOIN DOCENTE D ON P.DNI = D.DOCENTE_DNI

Nuevamente el motor de ORM utiliza la columna CLAZZ_ como referencia para determinar la clase a instanciar.

Una consulta a una clase concreta, en cambio requiere un inner join entre las tablas involucradas:

SELECT P.DNI, P.NOMBRE, P.APELLIDO, A.LEGAJO, A.FECHAINGRESO

FROM PERSONA P INNER JOIN ALUMNO A ON P.DNI = A.ALUMNO_DNI

Podemos resumir las ventajas de este esquema en las siguientes:

    • Se obtiene un diseño normalizado de la base de datos, fácil de evolucionar.

    • Es ideal para los casos en que los accesos se realizan mayormente a tablas de las clases base.

Sus desventajas, por su parte, son:

    • Para ciertos escenarios, la disminución de performance por la proliferación de los JOINS puede resultar inaceptable.

    • En casos en que se opte por una programación mixta de consultas SQL y accesos a la BD a través del ORM, la codificación de la consulta puede resultar engorrosa por la complejidad de los queries.

Esta estrategia resulta conveniente en escenarios que requieran de consultas polimórficas, en el cual las subclases difieran de las superclases mayormente en estado y en el que la profundidad de la jerarquía no implique un anidamiento de JOINS importante.

Para resumir el problema de la herencia y sus soluciones, los siguientes cuadros resume las estrategias planteadas:

Identidad

Todos los objetos tienen su identidad y ciclo de vida. La identidad en objetos se relaciona con la posición de memoria que ocupan. A su vez, como mencionamos en apartados anteriores, en POO manejamos el concepto de igualdad, que se refiere a cuándo dos objetos son el mismo desde el punto de vista lógico, es decir de acuerdo a nuestro negocio o una condición que hayamos definido.

Los registros de una tabla, en una base de datos, se identifican unívocamente por primary keys, que son atributos que deben ser:

· Requeridos: No nulos.

· Únicos: No repetirse en varios registros de la tabla.

· Constantes: Su valor no puede cambiar.

En una determinada tabla podemos tener varias columnas o combinaciones de éstas que puedan ser candidatas a ser tomadas en cuenta para formar una clave primaria.

Dentro de estas claves candidatas, podemos diferenciar dos grupos:

· Claves naturales: Poseen importancia desde el punto de vista de la semántica de negocio y tienen sentido incluso fuera del contexto de la base de datos. Un ejemplo habitual de clave natural sería el DNI. Sin embargo es difícil encontrar casos de claves naturales que cumplan las tres condiciones mencionadas, y aún llegado el caso en que así fuera, pueden llevar a un diseño difícil de mantener.

· Claves sustitutas: Son valores únicos generados por la aplicación o la base de datos. No poseen importancia desde el punto de vista del negocio, y en general sirven a fines internos de la base de datos (en este caso, para mantener la integridad referencial y la unicidad de los registros). En general esta opción resulta satisfactoria.

Volviendo a los ORMs, cada clase mapeada debe definir un identificador para cada columna que la representa. La forma de hacerlo en annotations, sería, por ejemplo:

@Id

@SequenceGenerator(name="s1", sequenceName="SEQ")

@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="s1")

private Long id;

Algunas estrategias que provee Hibernate para la generación de claves sustitutas son:

· NATIVE: Selecciona otros generadores, ejemplo, identity, sequence, etc. dependiendo en las capacidades de la base de datos subyacente.

· IDENTITY: Retorna un identificador del tipo long, short, o int, indicando el identificador generado en las bases que soporten identity columns.

· SEQUENCE: Crea secuencias en la BD y retorna el identificador un long, short o int.

· INCREMENT: Genera una clave única de tipo long, short o int, siempre y cuando no haya otro proceso insertando datos en la misma tabla.

· etc.

A partir de esto vemos que cada objeto persitente que tengamos tendrá una propiedad o conjunto de propiedades que representen a la/s columnas claves de la tabla correspondiente. Con esto nos aseguramos de tener una correspondencia unívoca entre cada objeto persistido y el registro que representa en la BD.

Si bien es posible combinar varias estrategias de generación de Ids entre las distintas tablas, se recomienda mantener una elección consistente en todo el modelo, siempre y cuando no se esté trabajando con un sistema legacy que imponga una restricción propia. En este último caso hay otra opción que adaptarse al modelo subyacente e incorporar a cada clase la estrategia de generación de Ids que la tabla requiera.

Qué sucede con a equals?

En este caso, nos encontramos con el inconveniente que, a menudo hacemos uso del mismo, y una mala definición de éste en nuestras clases persistentes podría traernos problemas. Por ejemplo, el Set que conocemos basa su contrato en equals para determinar los elementos repetidos, no sería bueno tener dentro de un Set dos objetos distintos que representen al mismo registro, ni impedir a dos objetos diferentes encontrarse en el Set, por una mala definición de la igualdad.

Por otro lado, si usamos una clave generada, hasta que no persistamos la entidad no poseerá un valor en su atributo que hace las veces de clave sustituta, ya que en aquel momento es cuando se determina el valor del mismo y se asigna.

Tenemos dos opciones frente a esta situación:

· Utilizar en equals y hashcode todos los atributos persistentes de la clase (sin contar la clave sustituta). Sin embargo este approach nos traería inconvenientes en un escenario en que tengamos dos registros con mismos valores de sus propiedades persistentes, pero distinta PK. De esta forma equals daría true, en objetos que representen filas distintas, lo que puede no estar bien.

· Identificar un atributo o conjunto de ellos que funcionen como clave natural del registro. Algo así como si hubiéramos optado por el primer enfoque mencionado en el comentario sobre claves candidatas. De esta manera nos aseguramos que nuestro negocio no permitiría dos entidades con mismos valores en estos campos y su participación en equals nos aseguraría tratar con objetos distintos.

Algunos tips para encontrar claves naturales dentro de clases persistentes:

· Ponerse del lado del cliente y pensar en qué atributos suele basarse para buscar un registro único en una tabla.

· Prestar atención a los atributos inmutables o aquellos que rara vez se actualizan.

· Tener en cuenta a los atributos asociado a columnas con restricciones UNIQUE.

· Los atributos que contengan información temporal (ej. Date) pueden resultar útiles.

· Tener en cuenta los atributos que representan columnas con foreign keys.