Hibernate – OneToOne, OneToMany, ManyToOne y ManyToMany

1
88688
hibernate

Analizaremos como funcionan algunas de las anotaciones que proporciona JPA que nos permiten manejar las relaciones de nuestra aplicación.

Índice de Contenidos

  1. Introducción
  2. Relaciones
    1. @OneToOne
    2. @OneToMany – @ManyToOne
    3. @OneToMany (unidireccional)
    4. @ManyToMany

 

1. Introducción

A través de las anotaciones que proporciona JPA cuando usamos Hibernate, podemos gestionar las relaciones entre dos tablas como si de objetos se tratasen. Esto facilita el mapeo de atributos de base de datos con el modelo de objetos de la aplicación. Dependiendo de la lógica de negocio y cómo modelemos, se podrán crear relaciones unidireccionales o bidireccionales.

 

2. Relaciones

2.1 @OneToOne (bidireccional)

La siguiente tabla muestra nuestro modelo de base de datos. student_id es la Foreign Key (a partir de ahora FK) que apunta a student.

modelo base de datos

Lo primero que deberíamos hacer es preguntarnos quién es el propietario de la relación, ya que esto determinará dónde irá la respectiva FK. Un estudiante tiene asociada una matrícula y esa matrícula está asociada a un único estudiante.

Una buena práctica es usar cascade en la entidad padre ya que nos permite propagar los cambios y aplicarlos a los hijos. En nuestro ejemplo, tuition no tiene sentido que exista si student no existe, por lo que student es el que tendrá el rol padre.

Si observamos la imagen anterior, he decidido que la FK la tenga tuition. Podemos decir que tuition es el propietario de la relación o propietario de esa FK (owning side) y student, la no propietaria de la relación, no posee esa FK (non-owning side). Pero, ¿Cómo creo una relación bidireccional en caso de que student quiera obtener las propiedades de tuition? Podríamos pensar en tener otra FK en student apuntando a tuition pero esto generaría una duplicidad innecesaria en nuestro modelo de base de datos. Para poder realizar este mapeo correctamente, entran en juego las anotaciones @JoinColumn y mappedBy.

@Entity
@Table(name = "student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToOne(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private Tuition tuition;

    /* Getters and setters */
}
@Entity
@Table(name = "tuition")
public class Tuition {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private Double fee;

    // Que columna en la tabla Tuition tiene la FK
    @JoinColumn(name = "student_id")
    @OneToOne(fetch = FetchType.LAZY)
    private Student student;

    /* Getters and setters */
}

@JoinColumn nos permite indicar el nombre de la columna a la que queremos hacer referencia en la tabla tuition

Con mappedBy, podemos establecer una relación bidireccional ya que a pesar de tener una única FK, podemos relacionar ambas tablas. Al final, el objetivo de las anotaciones es dejar claro donde está la clave que mapea las relaciones.

orphanRemoval= true especifica que la entidad hijo debe ser eliminada automáticamente por el propio ORM si ha dejado de ser referenciada por una entidad padre. p.ej., tenemos una colección de items y eliminamos uno, ese item ha dejado de tener una referencia y será eliminado. Ojo, no confundir con cascadeType que son operaciones a nivel de base de datos.

fetchType=LAZY, Recupera la entidad solo cuando realmente la necesitamos. Importante destacar que la sesión debe estar abierta para poder invocar al Getter correspondiente y recuperar la entidad, ya que hibernate usa el patrón Proxy (object proxying) . En caso contrario (al cerrar la sesión), la entidad pasaría de estado persistent a detach y se lanzaría una excepción LazyInitializationException.

Vamos a crear un test muy simple para comprobar la sentencia sql que se está ejecutando.

    @Test
    @Transactional
    @Rollback(false)
    public void check_sql_statement_when_persisting_in_one_to_one_bidirectional() {

        Student student = new Student();
        student.setName("Jonathan");
                
        Tuition tuition = new Tuition();
        tuition.setFee(150);
        tuition.setStudent(student);
        
        student.setTuition(tuition);

        entityManager.persist(student);
    }

sql

El uso de cascade en la entidad padre, hace que al persistir student se persista también tuition.

Una alternativa en el ejemplo que acabamos de ver es usar @MapsId. Como especifiqué anteriormente, matrícula no tiene sentido  que exista si estudiante no existe, solo puede haber asociada una matrícula  por estudiante. Con @MapsId  estamos especificando a Hibernate que student_id es PK (Primary Key) de tuition pero también es FK de student. Ambas entidades compartirán el mismo valor de identificación y no nos haría falta @GeneratedValue para la generación de nuevos ids en tuition.

modelo base de datos

@Entity
@Table(name = "tuition")
public class Tuition {

    @Id
    private Long id;

    private Double fee;
    
    @MapsId
    @OneToOne(fetch = FetchType.LAZY)
    private Student student;
    
    /* Getters and setters */
}

 

2.2 @OneToMany (bidireccional)

La siguiente tabla muestra nuestro modelo de base de datos. university_id es la FK que apunta a university.

modelo base de datos

Normalmente el owning side en este tipo de relaciones suele estar en el @ManyToOne y el mappedBy iría en la entidad padre.

@Entity
@Table(name = "university")
public class University {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "university", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Student> students;

    /* Getters and setters */
@Entity
@Table(name = "students")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne()
    @JoinColumn(name = "university_id")
    private University university;

    /* Getters and setters */
}

 

2.3 @OneToMany (unidireccional)

modelo base de datos

En una relación unidireccional @OneToMany, la anotación @JoinColumn hace referencia a la tabla en base de datos del many (student en este caso). Por este motivo, vemos en la siguiente imagen @JoinColumn en la clase University. La clase Student únicamente tendrá los atributos id y name.

@Entity
@Table(name = "university")
public class University {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "university_id")
    private List<Student> students;

    /* Getters and setters */
}

Vamos a hacer el test y comprobar las sentencias sql que se generan.

   @Test
    @Transactional
    @Rollback(false)
    public void check_sql_statement_when_persisting_in_one_to_many_unidirectional() {
        University university = new University();
        university.setName("Universidad de Las Palmas de Gran Canaria");

        Student student1 = new Student();
        student1.setName("Ana");

        Student student2 = new Student();
        student2.setName("Jonathan");

        university.setStudents(List.of(student1, student2));

        entityManager.persist(university);
    }

sql

¿Por qué se ejecutan los Update?

Al no indicar a student que debe tener una FK mapeando a university (como hicimos en el ejemplo anterior), Hibernate tiene que ejecutar sentencias adicionales para resolverlo. Una buena práctica es usar @ManyToOne si queremos que sea unidireccional o si no crear directamente una relación bidireccional, de este modo nos ahorraremos la ejecución de queries innecesarias

 

2.4 @ManyToMany (bidireccional)

Nuestro modelo de Base de datos es el siguiente

modelo base de datos

Con @ManyToMany debemos crear una tercera tabla para realizar el mapeo de ambas entidades. Esta tercera tabla tendrá dos FK apuntando a sus respectivas tablas padre. Por lo tanto, student_id apunta a la tabla student y course_id apunta a la tabla course.

@Entity
@Table(name="course")
public class Course {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private Double fee;

    @ManyToMany(mappedBy = "courses")
    private Set<Student> students;

    /* Getters and setters */
}
@Entity
@Table(name="student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToMany(cascade = {
            CascadeType.PERSIST,
            CascadeType.MERGE
    })
    @JoinTable(
            name = "student_course",
            joinColumns = {@JoinColumn(name = "student_id")},
            inverseJoinColumns = {@JoinColumn(name = "course_id")}
    )
    private Set<Course> courses;

    /* Getters and setters */
}

En este ejemplo el owning side es student y es donde se usa la anotación @JoinTable. Con ella, especificamos el nombre de la tabla que realiza el mapeo (student_course). JoinColumns apunta a la tabla del owning side (student) e InverseJoinColumns apunta a la tabla inversa del owning side (course). He decidido usar el cascade Merge y Persist, pero no cascade.Remove ya que si elimino un curso, no quiero que elimine los estudiantes asociados a él.

Como podemos ver en el ejemplo, estoy usando un Set en vez de List en la asociación. Esto es porque usando List, Hibernate elimina las filas del objeto que queremos eliminar y vuelve a insertar las demás. Esto es completamente innecesario e ineficiente.

La siguiente imagen muestra las sentencias sql generadas usando List. En este caso, tenemos un estudiante asociado a 4 cursos y queremos eliminar uno de ellos.


 
Recordemos el uso de Set si queremos evitar este tipo de comportarmiento indeseado

Si quieres leer el tutorial en inglés, puedes encontrarlo en mi perfil de dev.to

1 COMENTARIO

  1. No entendí lo de orphanRemoval, ¿que necesidad existe?, sin con cascadeType.Delete o cascadeType.All se dice a la base de datos que elimine en cascada (padre a todos los hijos)? o ¿cual es la diferencia?

    Por otro lado, en la relación muchos a muchos me confundí cuando dijo «He decidido usar el cascade Merge y Persist, pero no cascade.Remove ya que si elimino un curso, no quiero que elimine los estudiantes asociados a él»; si a nivel de base de datos los padres solo eliminan a los hijos, entonces una tupla de la tabla «course» a lo más que puede eliminar es a una tupla de «student_curse» y no así a una tupla «course» porque es una tabla padre.

    Creo que me confundí en esas relaciones, sería bueno un tutorial con un CRUD de cada tipo de relación y en ambos sentidos. gracias por el aporte…

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad