Modo más eficiente de mapear una relación @OneToMany con JPA e Hibernate

Elegí abrir este post con esta cita porque soy fan de Linus Torvalds.😉

Este es mi primer artículo. En este voy a cubrir todos los casos posibles en One-to-Many/Many-to-One asociación de entidades. Los restantes Many-to-Many y One-to-One serán cubiertos en próximos artículos.

Espero que esto ayude definitivamente a todos los novatos que quieran aprender jpa/hibernate, Por favor, lea todo el artículo 😛

NOTA:

Aquí he cubierto todos los casos posibles de One-to-Many/Many-to-One mapeo. Entre todos, la asociación bidireccional `@OneToMany` es la mejor manera de mapear una relación de base de datos de uno a muchos.

La asociación hibernada se clasifica en One-to-One, One-to-Many/Many-to-One y Many-to-Many.

  • La dirección de una relación puede ser bidireccional o unidireccional.
  • Una relación bidireccional tiene tanto un lado propietario como un lado inverso.
  • Una relación unidireccional sólo tiene un lado propietario. El lado propietario de una relación determina cómo el tiempo de ejecución de Persistence realiza las actualizaciones de la relación en la base de datos.
  • Unidireccional es una relación en la que uno de los lados no conoce la relación.
  • En una relación unidireccional, sólo una entidad tiene un campo de relación o una propiedad que hace referencia a la otra. Por ejemplo, Partida tendría un campo de relación que identifica a Producto, pero Producto no tendría un campo de relación o propiedad para Partida. En otras palabras, Artículo de línea sabe sobre Producto, pero Producto no sabe qué instancias de Artículo de línea se refieren a él.

Relaciones bidireccionales:

  • La relación bidireccional proporciona acceso de navegación en ambas direcciones, de modo que se puede acceder al otro lado sin consultas explícitas.
  • En una relación bidireccional, cada entidad tiene un campo de relación o propiedad que se refiere a la otra entidad. A través del campo o propiedad de relación, el código de una clase de entidad puede acceder a su objeto relacionado. Si una entidad tiene un campo de relación, se dice que la entidad «conoce» su objeto relacionado. Por ejemplo, si Orden sabe qué instancias de Partida tiene y si Partida sabe a qué Orden pertenece, tienen una relación bidireccional.

Las relaciones bidireccionales deben seguir estas reglas.

  • El lado inverso de una relación bidireccional debe referirse a su lado propietario (Entidad que contiene la clave extranjera) utilizando el elemento mappedBy de la anotación @OneToOne, @OneToMany, o @ManyToMany. El elemento mappedBy designa la propiedad o campo de la entidad propietaria de la relación.
  • El lado múltiple de las relaciones bidireccionales @ManyToOne no debe definir el elemento mappedBy. El lado many es siempre el lado propietario de la relación.
  • Para las relaciones bidireccionales @OneToOne, el lado propietario corresponde al lado que contiene @JoinColumn, es decir, la clave externa correspondiente.
  • Para las relaciones bidireccionales @ManyToMany, cualquiera de los lados puede ser el lado propietario.

@OneToMany relationship with JPA and Hibernate

Simplemente, el mapeo uno-a-muchos significa que una fila en una tabla se mapea a múltiples filas en otra tabla.

Cuándo utilizar el mapeo uno a muchos

Utilizar el mapeo uno a uno para crear 1…N relación entre entidades u objetos.

Tenemos que escribir dos entidades es decir. Company y Branch de tal forma que se puedan asociar múltiples sucursales a una sola empresa, pero una sola sucursal no puede ser compartida entre dos o más empresas.

Soluciones de mapeo de uno a varios en Hibernate:

  1. Mapeo de uno a varios con asociación de clave foránea
  2. Mapeo de uno a varios con tabla de unión

Este problema se puede resolver de dos maneras diferentes.

Una es tener una columna de clave foránea en la tabla de sucursales, es decir, company_id. Esta columna se referirá a la clave primaria de la tabla Empresa. De esta manera no hay dos rama puede ser asociado con múltiples company.

Segundo enfoque es tener una tabla de unión común digamos Company_Branch, Esta tabla tendrá dos columnas es decir, company_id que será clave externa que se refiere a la clave primaria en la tabla de la empresa y de manera similar branch_id que será clave externa que se refiere a la clave primaria de la tabla Branch.

Si @OneToMany/@ManyToOne no tiene una asociación @ManyToOne/@OneToMany en el lado hijo, entonces, la asociación @OneToMany/@ManyToOne es unidireccional.

@OneToMany Unidirectional Relationship

En este enfoque, cualquier entidad será responsable de hacer la relación y mantenerla. O bien la empresa declara la relación como uno a muchos, o la sucursal declara la relación desde su extremo como muchos a uno.

CASO 1: (Mapeo con asociación de clave foránea)

Si usamos sólo @OneToMany entonces habrá 3 tablas. Como company, branch y company_branch.

NOTA:
En el ejemplo anterior he utilizado términos como cascada, orphanRemoval, fetch y targetEntity, que explicaré en mi próximo post.

La tabla company_branch tendrá dos claves foráneas company_id y branch_id.

Ahora, si persistimos una Empresa y dos Sucursales:

Hibernate va a ejecutar las siguientes sentencias SQL:

  • La asociación @OneToMany es por definición una asociación padre(no propietaria), aunque sea unidireccional o bidireccional. Sólo el lado del padre de una asociación tiene sentido para la cascada de sus transiciones de estado de la entidad a los hijos.
  • Al persistir la entidad Company, la cascada propagará la operación de persistencia a los hijos de la rama subyacente también. Al eliminar un Branch de la colección de ramas, la fila de la asociación se elimina de la tabla de enlaces, y el atributo orphanRemoval desencadenará una eliminación de la rama también.

Las asociaciones unidireccionales no son muy eficientes cuando se trata de eliminar entidades hijas. En este ejemplo concreto, al vaciar el contexto de persistencia, Hibernate elimina todas las entradas hijas de la base de datos y reinserta las que aún se encuentran en el contexto de persistencia en memoria.

En cambio, una asociación bidireccional @OneToMany es mucho más eficiente porque la entidad hija controla la asociación.

CASO 2: (Mapeo con asociación de clave foránea)

Si utilizamos sólo @ManyToOne entonces habrá 2 tablas. Como empresa, sucursal.

Este código anterior generará 2 tablas Company(company_id,name) y Branch(branch_id,name,company_company_id). Aquí Branch es la parte propietaria ya que tiene la asociación de clave foránea.

CASO 3: (Mapeo con asociación de clave foránea)

Si usamos tanto @ManyToOne como @OneToMany entonces se crearán 3 tablas Company(id,name), Branch(id,name,company_id), Company_Branch(company_id,branch_id)

Este mapeo de abajo puede parecer bidireccional pero no lo es. No define una relación bidireccional, sino dos relaciones unidireccionales separadas.

CASO 4: (Unidireccional @OneToMany con @JoinColumn)(Mapeo con asociación de clave foránea)

Si utilizamos @OneToMany con @JoinColumn entonces habrá 2 tablas. Como empresa, sucursal

En la entidad anterior dentro de @JoinColumn, el nombre se refiere al nombre de la columna de clave foránea que es companyId i.e company_id y rferencedColumnName indica la clave primaria i.e id de la entidad (Compañía) a la que se refiere la clave foránea companyId.

CASO 5. (Mapeo con clave foránea) (Mapeo con asociación de clave foránea)

Si utilizamos @ManyToOne con @JoinColumn entonces habrá 2 tablas. Como empresa,sucursal.

La anotación @JoinColumn ayuda a Hibernate a averiguar que hay una columna company_id de clave foránea en la tabla sucursal que define esta asociación.

La rama tendrá la clave foránea company_id, por lo que es el lado del propietario.

Ahora, si persistimos 1 Empresa y 2 Sucursal(es):

al eliminar la primera entrada de la colección hija:

company.getBranches().remove(0);

Hibernate ejecuta dos sentencias en lugar de una :

  • Primero hará que el campo de clave foránea sea nulo(para romper la asociación con el padre) y luego eliminará el registro.
Update branch set branch_id = null where where id = 1 delete from branch where id = 1 ;

CASO 6. (Mapeo con tabla join) (Mapeo con tabla de unión)

Ahora, consideremos la relación one-to-many donde Person(id,name) se asocia con múltiples Veichle(id,name,number) y múltiples vehículos pueden pertenecer a la misma persona.

Un día, mientras que en la gasolina de la carretera sheriff Carlos encontró algunos vehículos abandonados, probablemente robados. Ahora el sheriff tiene que actualizar los detalles de los vehículos (número, nombre) en su base de datos, pero el principal problema es que no hay ningún propietario para esos vehículos, por lo que el campo person_id(foreign key ) permanecerá nulo.

Ahora el sheriff guarda dos vehículos robados en la base de datos de la siguiente manera:

Vehicle vehicle1 = new Vehicle("ford", 1); 
Vehicle vehicle2 = new Vehicle("gmc", 2);
List<Vehicle> vehicles = new ArrayList<>(); vehicles.add(vehicle1);
vehicles.add(vehicle2);
entityManager.persist(veichles);

Hibernate va a ejecutar las siguientes sentencias SQL:

insert into vehicle (id, name, person_id) values (1, "ford", null); insert into vehicle (id, name, person_id) values (2, "gmc", null);id |name|person_id|
-----|----|---------|
1 |ford|NULL |
2 |benz|NULL |
---------------------

Esta estrategia anterior nos obligará a poner valores nulos en la columna para manejar las relaciones opcionales.

Típicamente, pensamos en relaciones many-to-many cuando consideramos una join table, pero, utilizar una tabla join, en este caso, puede ayudarnos a eliminar estos valores nulos:

Este enfoque utiliza una tabla join para almacenar las asociaciones entre las entidades Branch y Company. Se ha utilizado la anotación @JoinTable para realizar esta asociación.

En este ejemplo hemos aplicado @JoinTable en el lado del Vehículo(Many Side).

Veamos como será el esquema de la base de datos:

El código anterior generará 3 tablas como person(id,name), vehicle(id,name) y vehicle_person(vehicle_id,person_id). Aquí vehicle_person mantendrá la relación de clave foránea con las entidades Persona y Vehículo.

Así que cuando el sheriff guarde los detalles del vehículo, ningún valor nulo tendrá que ser perseguido en la tabla vehículo porque estamos manteniendo la asociación de clave foránea en las tablas vehicle_person no en la tabla vehículo.

Vehicle vehicle1 = new Vehicle("ford", 1);
Vehicle vehicle2 = new Vehicle("gmc", 2);
List<Vehicle> vehicles = new ArrayList<>(); vehicles.add(vehicle1);
vehicles.add(vehicle2);
entityManager.persist(veichles);

Hibernate va a ejecutar las siguientes sentencias SQL:

insert into vehicle (id, name) values (1, "ford"); insert into vehicle (id, name) values (2, "gmc");id |name|
-----|----|
1 |ford|
2 |benz|
-----------

Este ejemplo anterior muestra, cómo nos deshicimos de la inserción de valores nulos.

@OneToMany Bi-directional Relationship

  • La asociación bidireccional @OneToMany también requiere una asociación @ManyToOne en el lado hijo. Aunque el Modelo de Dominio expone dos lados para navegar por esta asociación, entre bastidores, la base de datos relacional sólo tiene una clave foránea para esta relación.
  • Toda asociación bidireccional debe tener un solo lado propietario (el lado hijo), el otro se denomina lado inverso (o elmappedBy).
  • Si utilizamos el @OneToMany con el atributo mappedBy establecido, entonces tenemos una asociación bidireccional, lo que significa que necesitamos tener una asociación @ManyToOne en el lado hijo al que hace referencia el mappedBy.
  • El elemento mappedBy define una relación bidireccional. Este atributo permite referenciar las entidades asociadas desde ambos lados.

La mejor manera de mapear una asociación @OneToMany es confiar en el lado @ManyToOne para propagar todos los cambios de estado de la entidad.

Si persistimos 2 Branch(s)

Hibernate genera sólo una sentencia SQL para cada entidad Branch persistida:

insert into company (name, id) values ("company1",1); 
insert into branch (company_id, name, id) values (1,"branch1",1); insert into branch (company_id, name, id) values (1,"branch2",2);

Si eliminamos un Branch:

Company company = entityManager.find( Company.class, 1L ); Branch branch = company.getBranches().get(0); company.removeBranches(branch);

Sólo se ejecuta una sentencia SQL de borrado:

delete from Branch where id = 1

Así que la asociación bidireccional @OneToMany es la mejor manera de mapear una relación de base de datos de uno a muchos cuando realmente necesitamos la colección en el lado padre de la asociación.

@JoinColumnEspecifica una columna para unir una asociación de entidades o una colección de elementos. La anotación @JoinColumn indica que esta entidad es la dueña de la relación. Es decir, la tabla correspondiente tiene una columna con una clave foránea a la tabla referenciada.

En el ejemplo anterior, la rama de la entidad propietaria, tiene una columna de unión llamada company_id que tiene una clave foránea a la entidad no propietaria Compañía.

Siempre que se forme una asociación bidireccional, el desarrollador de la aplicación debe asegurarse de que ambas partes estén sincronizadas en todo momento. Los addBranches() y removeBranches() son métodos de utilidad que sincronizan ambos extremos cada vez que se añade o elimina un elemento hijo (es decir, Branch).

A diferencia de la unidireccional @OneToMany, la asociación bidireccional es mucho más eficiente a la hora de gestionar el estado de persistencia de la colección.

Cada eliminación de un elemento sólo requiere una única actualización (en la que la columna de clave foránea se establece en NULL) y si el ciclo de vida de la entidad hija está vinculado a su padre propietario de manera que el hijo no puede existir sin su padre, entonces podemos anotar la asociación con el atributo orphan-removal y la desasociación del hijo desencadenará una sentencia de eliminación en la fila de la tabla hija real también.

NOTA:

  • En el ejemplo bidireccional anterior el término Parent y Child en Entity/OO/Model( es decir, en la clase java) se refiere a Non-woning/Inverse y Owning side en SQL respectivamente.

En el punto de vista de java Company es el Parent y branch es el child aquí. Dado que una rama no puede existir sin el padre.

En el punto de vista de SQL rama es el lado propietario y la empresa es el lado no-woning(Inverse). Como hay 1 empresa para N sucursales, cada sucursal contiene una clave foránea a la Empresa a la que pertenece.Esto significa que la sucursal «posee» (o contiene literalmente) la conexión (información). Esto es exactamente lo contrario del mundo OO/modelo.

@OneToMany Bi-directional Relationship(Mapping with join table)

Desde CASE 6 tenemos un mapeo unidireccional con @JoinTable, así que si añadimos el atributo mappedBy a la entidad Person entonces la relación se convertirá en bidireccional.

Sumario

Hay varias cosas a tener en cuenta en el mapeo mencionado:

  • La asociación @ManyToOne utiliza FetchType.LAZY porque, de lo contrario, volveríamos a la búsqueda EAGER que es mala para el rendimiento.
  • Las asociaciones bidireccionales deben actualizarse siempre en ambos lados, por lo que el lado Parent debe contener el combo addChild y removeChild(Parent side Company contiene dos métodos de utilidad addBranches y removeBranches). Estos métodos aseguran que siempre sincronizamos ambos lados de la asociación, para evitar problemas de corrupción de objetos o datos relacionales.
  • La entidad hija, Branch, implementa los métodos equals y hashCode. Dado que no podemos confiar en un identificador natural para la comprobación de la igualdad, tenemos que utilizar el identificador de la entidad en su lugar. Sin embargo, tenemos que hacerlo correctamente para que la igualdad sea consistente en todas las transiciones de estado de la entidad. Debido a que nos basamos en la igualdad para el removeBranches, es una buena práctica anular equals y hashCode para la entidad hija en una asociación bidireccional.
  • La asociación @OneToMany es por definición una asociación padre, incluso si es unidireccional o bidireccional. Sólo el lado del padre de una asociación tiene sentido para cascadear sus transiciones de estado de la entidad a los hijos.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.