sábado, 25 de diciembre de 2010

Manual Relaciones entre Entities

Relaciones entre entities

En las secciones anteriores hemos visto cómo programar un Entity Bean de EJB 3.0. Pero en todo diseño orientado a objetos, se establecen relaciones entre las diferentes entidades para representar el modelo en cuestión. En esta sección veremos cómo implementar estas relaciones en JPA.

Tipos de relaciones

Existen cuatro tipos de relaciones que podemos establecer con JPA:

  • 1:1
  • 1:N
  • N:1
  • N:N

Para cada tipo de relación, existen además dos variantes: unidireccional y bidireccional. En total, esto nos da ocho tipos de relaciones. Desde el punto de vista de la implementación, sin embargo, los casos unidireccionales son muy similares a los bidireccionales. Exploraremos algunos ejemplos de cada uno, pero nos focalizaremos en los casos relaciones bidireccionales. Analizaremos, además, las consecuencias que estas implementaciones tienen en el modelo relacional resultante.

Relaciones 1:1

En nuestro modelo de dominio, cada empleado tiene asociado un currículum:


relacion 1:1


Para implementar esta relación, debemos agregar en la clase Empleado un atributo que la represente, utilizando la anotación @OneToOne:

@OneToOne(cascade = CascadeType.ALL, optional = false)
@JoinColumn(name = "curriculum")
private Curriculum curriculum;

La anotación @OneToOne, entonces, se utiliza para describir relaciones 1:1. Si pensamos en el modelo de datos necesario para persistir la relación, tenemos dos opciones:

  1. Incluir en la tabla de empleados una clave foranea a la tabla de currículum.
  2. Incluir en la tabla de currículum una clave foranea a la tabla de empleados.

En este caso, dado que la relación es unidireccional con sentido Empleado -> Curriculum, lo natural es que la tabla empleados tenga la clave foránea a la table curriculum. Para indicar la información de esta clave foránea en la clase Empleado, utilizamos la anotación @JoinColumn. Según el código que hemos visto, entonces, la columna con la clave foránea se llama curriculum. La especificación de EJB 3.0 denomina a la clase que contiene la definición de la clave foránea en cualquier tipo de relación como clase dueña de la relación.

El modelo de datos queda, entonces:



relacion 1:1

Dado que la relación es unidireccional, en la clase Curriculum no debemos incluir ninguna anotación en particular. Pero si quisiéramos que fuera bidireccional, tendríamos que agregarle un atributo para representarla, de la siguiente manera:

@OneToOne(mappedBy="curriculum")
private Empleado empleado;

Vemos que en este extremo de la relación no estamos definiendo la cláve foránea. Como habíamos dicho, la clave foránea sólo es necesaria en una tabla; en este caso, elegimos colocarla en la table empleados. Dentro de la clase Curriculum, en cambio, utilizamos el atributo mappedBy para indicar el nombre de la propiedad en la clase Empleado que define la relación.

En general, entonces, en una relación bidireccional existen dos extremos; uno dueño de la relación, y otro subordinado. El extremo dueño utiliza la anotación @JoinColumn para definir la clave foránea. El extremo subordinado utiliza el atributo mappedBy de la anotación correspondiente para indicar el nombre de la propiedad correspondiente en el extremo dueño.

¿Pero por qué necesitamos el atributo mappedBy? En nuestro caso, la relacion entre ambas clases se da en un único atributo para ambos extremos, por lo que el proveedor de persistencia podría utilizar una convención y deducir el atributo correspondiente. La especificación no indica esta convención, sin embargo, y por lo tanto se utiliza el atributo mappedBy para vincular los atributos correspondientes a los dos extremos de la misma relación.

Analizaremos el atributo cascade de la anotación OneToMany más adelante.

Relaciones 1:N

A su vez, la clase Curriculum contiene una lista de ItemCurriculum. Cada item representa una entrada del curriculum:


relacion 1:N

En la representación del modelo relacional para esta relación, la tabla que almacena los items tendrá la clave foránea a la tabla de los currículum. Aún así, la entidad que define la relación debe ser Curriculum, no ItemCurriculum, dado que la relación es unidireccional con sentido Curriculum -> ItemCurriculum. Entonces, debemos usar la anotación @JoinColumn en la clase Curriculum.

El modelo de datos para esta relación queda definido, entonces, de la siguiente manera:


Relacion 1:N

En la implementación vemos el uso de las anotaciones indicadas previamente

@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "curriculum")
private List items = new Vector();

Si, en cambio, la relación fuese bidireccional, lo correcto sería usar la anotación @JoinColumn en la clase ItemCurriculum, y el atributo mappedBy en la clase Curriculum:

public class Curriculum {

@OneToMany(cascade = CascadeType.ALL, mappedBy="curriculum")
private List items = new Vector();

// Otros atributos y métodos.
}

public class ItemCurriculum {

@ManyToOne(optional = false)
@JoinColumn(name = "curriculum")
private Curriculum curriculum;

// Otros atributos y métodos.
}

Nótese que en el extremo N (la clase ItemCurriculum) se utiliza la anotación @ManyToOne, que veremos a continuación.

Relaciones N:1

Para las relaciones N:1, el modelo relacional es similar al de las relaciones 1:N. La tabla que representa el extreno N de la relación contiene una clave foránea a la tabla que representa el extreno 1. En este caso, sin embargo, el extremo dueño de la relación es el extreno N. Por lo tanto, la anotación @JoinColumn debe usarse en la clase que represente el extremo N.

La anotación para indicar este tipo de relación es @ManyToOne.

En nuestro modelo, el ejemplo de este tipo de relaciones es la existente entre Empleado y CategoriaEmpleado, definida en el siguiente model de clases:


Relacion N:1

El modelo relacional involucra las tablas empleados y categorias_empleado:


Relacion N:1

Vemos que la implementación similar a la utilizada en el ejemplo de relación 1:N bidireccional, en el extremo N (en este caso, la clase Empleado):

@ManyToOne(optional = false)
@JoinColumn(name = "categoria")
private CategoriaEmpleado categoria;

Dado que la relación es unidireccional, no es necesario modificar la clase CategoriaEmpleado. Si quisiéramos que la relación fuese bidireccional, entonces la clase CategoriaEmpleado tendría un atributo empleados anotado con @OneToMany. Lo mismo sucedería en el caso de tener una relación 1:N bidireccional. En otras palabras, las relaciones 1:N y N:1 bidireccionales se implementan de manera similar, utilizando @OneToMany en el extreno 1 y @ManyToOne en el extremo N; lo único que varía es el extremo en el que se usa la anotación @JoinColumn. Desde un punto de vista semántico de la relación, no hay diferencia. Simplemente existe una entidad que desde el punto de vista del modelo es más fuerte en la relación, y por eso decimos que la misma es 1:N o N:1.

Si analizamos el ejemplo de una relación 1:N bidireccional, vemos que la implementación es igual a la de un relación N:1 bidireccional.

Relaciones N:N

Para definir relaciones N:N, existe la anotación @ManyToMany, usada en ambos extremos. El modelo relacional requiere que exista una tabla de relación que contenga registros con pares de claves foráneas a cada tabla involucrada. La definición de esta tabla y sus claves foráneas se realiza con la anotación @JoinTable, en la clase del extremo dueño de la relación.

Tomaremos como ejemplo la relación entre las clases Cliente y Proyecto, definida en el siguiente modelo de clases:


Relacion N:N

Para persistir esta relación, necesitamos utilizar una tabla que contenga las relaciones entre ambas clases. Esta tabla tendrá claves foráneas a las tablas en las que se persisten las clases Cliente y Proyecto. El modelo de datos para esta relación es:


Relacion N:N

Para implementar esta relación, seleccionamos a la clase Proyecto como "dueña" de la misma. Esto significa eque en atributo clientes de esta clase vamos a definir la tabla de relación a utilizar, y las claves foráneas.

@ManyToMany
@JoinTable(name = "clientes_proyectos",
joinColumns = {@JoinColumn(name = "proyecto", referencedColumnName = "codigo") },
inverseJoinColumns = { @JoinColumn(name = "cliente", referencedColumnName = "codigo") })
private Set clientes = new HashSet();

Con el atributo name de la anotación @JoinTable indicamos el nombre de la tabla de relación. Con el atributo joinColumn definimos la clave foránea en la tabla clientes_proyectos a la tabla proyectos. Con el atributo inverseJoinColumns definimos la clave foránea en la tabla clientes_proyectos a la tabla clientes. Si alguna de las clases tuviera una clave compuesta, en estos atributos deberíamos especificar todas las columnas de dicha clave.

En la clase Cliente sólo tenemos que anotar la relación con @ManyToMany, y utilizar el atributo mappedBy para indicar que la relación está definida en el atributo clientes de la clase Proyecto:

@OneToMany(mappedBy = "clientes")
private Set proyectos = new HashSet();

Operaciones en cascada

Cuando trabajamos con las entidades, en ocasiones creamos un árbol de objetos, y queremos persistir todo ese árbol al mismo tiempo. Para no tener que solicitar a JPA que persista los objetos uno a uno, podemos indicar en la definición de las relaciones que algunas operaciones se propaguen en cascada. Esto significa que, por ejemplo, persistiendo el objeto "raíz" del árbol que creamos, JPA propagará la operación de persistencia a todos los objetos relacionados. Esta propagación se define para cada relación en particular; de manera que, para una clase determinada, podemos definir qué operaciones (si alguna) se propagarán para qué relaciones.

En nuestra aplicación, un Empleado está compuesto por un Curriculum, y cada Curriculum está compuesto por un conjunto de ItemCurriculum. Es natural que querramos crear el empleado, su curriculum, y los ítems del curriculum, y persistir todos estos objetos en una única operación. Asimismo, en algún momento modificaremos los datos del empleado, y agregaremos ítems a su currículum. También querremos, en este caso, propagar la operación de actualización.

Para definir, en una releación con cualquier cardinalidad, qué operaciones deben propagarse, se utiliza el atributo cascade en la anotación de la relación. Este atributo puede tener los siguientes valores:

  • CascadeType.PERSIST: se propaga la operación de persistir.
  • CascadeType.MERGE: se propaga la operación de sincronizar la base de datos con las modificaciones realizadas al objeto.
  • CascadeType.REFRESH: se propaga la operación de actualizar el objeto con los datos de la base de datos.
  • CascadeType.REMOVE: se propaga la operación de eliminar el objeto.
  • CascadeType.ALL: valor de convenciencia que inca que se propagan todas las operaciones anteriores.

En el atributo cascade se puede utilizar más de uno de estos valores. Por ejemplo, una relación 1:N podría anotarse de la siguiente manera:

@OneToMany(cascade={CascadeType.PERSIST, CascadeType.MERGE})

Con estos valores en el atributo cascade, se propagarán las operaciones de persistencia y sincronización de la base de datos, pero no las otras dos.

Si se utiliza el valor CascadeType.ALL, no debe utilizarse ningún otro valor.

Por defecto, JPA no propaga ninguna operación. Por lo tanto, si para una relación determinada no especificamos CascadeType.PERSIST o CascadeType.ALL, deberemos persistir todos los objetos explícitamente, uno a uno.

Estrategias de recuperación de relaciones

Cuando se busca un objeto que tiene relaciones con otros, particularmente en relaciones 1:N y N:N, obtener todas las colecciones con las que está relacionado el objeto puede ser costoso en tiempo de ejecución. Pensemos, por ejemplo, en una Compania que tiene 100.000 empleados. Es esperable que la mayor parte de los casos de uso en los que se requiera utilizar una Compania, no sea necesario utilizar todos sus Empleados. ¿Para qué obtenerlos, entonces?

Para estos casos, se pude definir en cualquier tipo de relación, que el/los objeto/s relacionados se obtengan siempre al obtener el objeto "padre", o que se obtengan al momento de utilizarlos. Por ejemplo, en el caso de de una relación 1:N, hasta que no realicemos una operación con el extremo N de la relación, no es necesario cargar todos los objetos.

Cuando no queremos que un objeto u objetos relacionados se carguen inmediatamente, decimos que esta relación es perezosa o lazy. Esta definición puede realizarse para cualquiera de los cuatro tipos de relaciones que analizamos en esta sección, y es independiente de la estrategia de propagación de operaciones establecida para la relación.

Para definir este comportamiento, se utiliza el atributo fetch de la anotación de una relación. Este atributo puede tener uno de dos valores:

  • FetchType.EAGER: es la estrategia por defecto. La relación se cargará al obtener el objeto.
  • FetchType.LAZY: la estrategia perezosa; la relación se cargará al utilizarla por primera vez. La especificación de JPA dice que utilizar FetchType.LAZY es un consejo para el proveedor de persistencia. Es decir, la especificación no prescribe exactamente cómo se debe tratar la relación en este caso.

Consecuencias del uso de relaciones LAZY

A pesar de que el uso de FetchType.LAZY es un consejo para los proveedores de persistencia, éstos en general tienen mecanismos para tratar estos casos. ¿Pero cómo hacen para obtener las relaciones al primer acceso? Dejar el atributo en null no es factible, dado que no se estaría respetando la semántica del atributo. Analicemos el siguiente extracto de código para entender la situación:

Empleado e = null.
// Obtenemos un empleado con JPA con un mecanismo que aún no hemos estudiado...
// Ahora la variable e referencia a un objeto
System.out.println(e.getNombre()); // Muestra el nombre del empleado.
System.out.println(e.getCurriculum().getItems().size()) // Muestra la cantidad de items en el curriculum del empleado.

La última línea, si "no cargar la relación Curriculum -> ItemCurriculum" signifca que el atributo items de la clase Curriculum será null, generará una NullPointerException, lo cual es semánticamente incorrecto, aún si esa colección estuviera vacía en la base de datos. En otras palabras, si el Curriculum no tuviera items cargados, la última línea debería mostrar 0 sin generar un error. Que una colección esté vacía no es lo mismo que la colección no exista. Lo mismo ocurre con la relación Empleado -> Curriculum. Según el modelo de dominio, un Empleado siempre tiene un Curriculum asociado, con lo cual es correcto que el programador realice operaciones sobre e.getCurriculum() sin verificar previamente si el getter devuelve null.

Para no violar esta semántica, los proveedores de persistencia utilizan proxies. Estos proxies se encargan de encapsular el comportamiento de obtener el objeto real de la relación (en los casos de relaciones 1:1 y N:1) o la colección (en los casos de relaciones 1:N y N:N). A través de los proxies, el proveedor de persistencia puede "interceptar" la llamada a algún método de ese objeto o colección, y obtenerlos sólo cuando se los utilice.

Saber esto es importante. Si intentamos pasar un Entity Bean entre servidor y cliente, es fundamental saber si el proveedor de persistencia inyectó un proxy para alguna relación; los proxies no funcionarán en la capa del cliente, sino solamente en la capa del servidor.

No hay comentarios:

Publicar un comentario