Logotipo de Zephyrnet

Una guía para JPA con Hibernate - Mapeo de relaciones

Fecha:


Introducción

En este artículo, nos sumergiremos en Mapeo de relaciones con JPA e Hibernate en Java.

JPA es el estándar de persistencia del ecosistema Java. Nos permite mapear nuestro modelo de dominio directamente a la estructura de la base de datos y luego nos da la flexibilidad de manipular objetos en nuestro código, en lugar de jugar con componentes JDBC engorrosos como Connection, ResultSet, etc.

Haremos una guía completa para usar JPA con Hibernate como su proveedor. En este artículo, cubriremos las asignaciones de relaciones.

  • Guía de JPA con Hibernate - Mapeo básico
  • Guía de JPA con Hibernate - Mapeo de relaciones (estás aquí)
  • Guía de JPA con Hibernate: asignación de herencia (muy pronto!)
  • Guía de JPA con Hibernate: consultas (¡próximamente!)

Nuestro Ejemplo

Antes de comenzar, recordemos el ejemplo que usamos en la parte anterior de esta serie. La idea era mapear el modelo de una escuela con estudiantes que toman cursos impartidos por profesores.

Así es como se ve este modelo:

modelo de dominio

Como podemos ver, hay algunas clases con ciertas propiedades. Estas clases tienen relaciones entre ellas. Al final de este artículo, habremos mapeado todas esas clases a las tablas de la base de datos, manteniendo sus relaciones.

Además, podremos recuperarlos y manipularlos como objetos, sin la molestia de JDBC.

Relaciones

En primer lugar, definamos un relación. Si miramos nuestro diagrama de clases, podemos ver algunas relaciones:

Profesores y cursos - estudiantes y cursos - cursos y materiales del curso.

También hay conexiones entre estudiantes y direcciones, pero no se consideran relaciones. Esto es porque un Address no es una entidad (es decir, no se asigna a una tabla propia). Entonces, en lo que respecta a JPA, no es una relación.

Hay algunos tipos de relaciones:

  • Uno a muchos
  • Muchos a uno
  • Programa XNUMX a XNUMX
  • Muchos a muchos

Abordemos estas relaciones una por una.

Uno a varios / varios a uno

Empezaremos con el Uno a muchos y Muchos a uno relaciones, que están estrechamente relacionadas. Podría seguir adelante y decir que son los lados opuestos de la misma moneda.

Qué es un Uno a muchos ¿relación?

Como su nombre lo indica, es una relación que vincula una entidad a muchos otras entidades.

En nuestro ejemplo, esto sería un Teacher y ellos Courses. Un profesor puede impartir varios cursos, pero un curso lo imparte un solo profesor (ese es el Muchos a uno perspectiva - muchos cursos para un maestro).

Otro ejemplo podría estar en las redes sociales: una foto puede tener muchos comentarios, pero cada uno de esos comentarios pertenece a esa foto.

Antes de sumergirnos en los detalles de cómo mapear esta relación, creemos nuestras entidades:

@Entity
public class Teacher { private String firstName; private String lastName;
} @Entity
public class Course { private String title;
}

Ahora, los campos del Teacher la clase debe incluir una lista de cursos. Dado que nos gustaría mapear esta relación en una base de datos, que no puede incluir una lista de entidades dentro de otra entidad, la anotaremos con un @OneToMany anotación:

@OneToMany
private List<Course> courses;

Hemos usado un List como el tipo de campo aquí, pero podríamos haber optado por un Set o un Map (aunque este requiere un un poco más de configuración).

¿Cómo refleja JPA esta relación en la base de datos? Generalmente, para este tipo de relación, debemos usar una clave externa en una tabla.

JPA hace esto por nosotros, dada nuestra opinión sobre cómo debe manejar la relación. Esto se hace a través del @JoinColumn anotación:

@OneToMany
@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")
private List<Course> courses;

El uso de esta anotación le dirá a JPA que el COURSE la tabla debe tener una columna de clave externa TEACHER_ID que hace referencia al TEACHER mesas ID columna.

Agreguemos algunos datos a esas tablas:

insert into TEACHER(ID, LASTNAME, FIRSTNAME) values(1, 'Doe', 'Jane'); insert into COURSE(ID, TEACHER_ID, TITLE) values(1, 1, 'Java 101');
insert into COURSE(ID, TEACHER_ID, TITLE) values(2, 1, 'SQL 101');
insert into COURSE(ID, TEACHER_ID, TITLE) values(3, 1, 'JPA 101');

Y ahora verifiquemos si la relación funciona como se esperaba:

Teacher foundTeacher = entityManager.find(Teacher.class, 1L); assertThat(foundTeacher.id()).isEqualTo(1L);
assertThat(foundTeacher.lastName()).isEqualTo("Doe");
assertThat(foundTeacher.firstName()).isEqualTo("Jane");
assertThat(foundTeacher.courses()) .extracting(Course::title) .containsExactly("Java 101", "SQL 101", "JPA 101");

Podemos ver que los cursos del profesor se recopilan automáticamente, cuando recuperamos el Teacher ejemplo.

Si no está familiarizado con las pruebas en Java, es posible que le interese leer Pruebas unitarias en Java con JUnit 5!

Poseer lado y bidireccionalidad

En el ejemplo anterior, el Teacher la clase se llama lado dueño de las Uno a muchos relación. Esto se debe a que define la columna de unión entre las dos tablas.

El Course se llama lado de referencia en esa relación.

Podríamos haber hecho Course el lado propietario de la relación mediante el mapeo de la Teacher campo con @ManyToOne existentes Course clase en su lugar:

@ManyToOne
@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")
private Teacher teacher;

No es necesario tener una lista de cursos en el Teacher clase ahora. La relación habría funcionado al revés:

Course foundCourse = entityManager.find(Course.class, 1L); assertThat(foundCourse.id()).isEqualTo(1L);
assertThat(foundCourse.title()).isEqualTo("Java 101");
assertThat(foundCourse.teacher().lastName()).isEqualTo("Doe");
assertThat(foundCourse.teacher().firstName()).isEqualTo("Jane");

Esta vez, usamos el @ManyToOne anotación, de la misma manera que usamos @OneToMany.

Nota: Es una buena práctica colocar el lado propietario de una relación en la clase / tabla donde se mantendrá la clave externa.

Entonces, en nuestro caso, esta segunda versión del código es mejor. Pero, ¿y si todavía queremos nuestro Teacher clase para ofrecer acceso a su Course ¿lista?

Podemos hacer eso definiendo una relación bidireccional:

@Entity
public class Teacher { // ... @OneToMany(mappedBy = "teacher") private List<Course> courses;
} @Entity
public class Course { // ... @ManyToOne @JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID") private Teacher teacher;
}

Mantenemos nuestro @ManyToOne mapeo en el Course entidad. Sin embargo, también mapeamos una lista de Courses a la Teacher entidad.

Lo que es importante tener en cuenta aquí es el uso de la mappedBy bandera en el @OneToMany anotación en el lado de referencia.

Sin él, no tendríamos una relación bidireccional. Tendríamos dos relaciones unidireccionales. Ambas entidades estarían mapeando claves foráneas para la otra entidad.

Con él, le decimos a JPA que el campo ya está mapeado por otra entidad. Está mapeado por el teacher del objeto Course entidad.

Carga ansiosa vs perezosa

Otra cosa que vale la pena señalar es ansioso y perezoso cargando. Con todas nuestras relaciones mapeadas, es aconsejable evitar afectar la memoria del software colocando demasiadas entidades en él si no es necesario.

Imagine That Course es un objeto pesado, y cargamos todos Teacher objetos de la base de datos para alguna operación. No necesitamos recuperar o usar los cursos para esta operación, pero aún se están cargando junto con el Teacher objetos.

Esto puede ser devastador para el rendimiento de la aplicación. Técnicamente, esto se puede resolver utilizando el Patrón de diseño de objetos de transferencia de datos y recuperando Teacher información sin los cursos.

Sin embargo, esto puede ser un masivo exagerado si todo lo que obtenemos del patrón es excluir los cursos.

Afortunadamente, JPA pensó en el futuro e hizo Uno a muchos carga de relaciones perezosamente por defecto

Esto significa que la relación no se cargará de inmediato, sino solo cuando sea realmente necesario.

En nuestro ejemplo, eso significaría que hasta que llamemos al Teacher#courses método, los cursos no se obtienen de la base de datos.

Por el contrario, Muchos a uno las relaciones son ansioso de forma predeterminada, lo que significa que la relación se carga al mismo tiempo que la entidad.

Podemos cambiar estas características estableciendo el fetch argumento de ambas anotaciones:

@OneToMany(mappedBy = "teacher", fetch = FetchType.EAGER)
private List<Course> courses; @ManyToOne(fetch = FetchType.LAZY)
private Teacher teacher;

Eso invertiría la forma en que funcionó inicialmente. Los cursos se cargarán con entusiasmo, tan pronto como carguemos un Teacher objeto. Por el contrario, el teacher no se cargaría cuando buscamos courses si es innecesario en ese momento.

Opcionalidad

Ahora, hablemos de opcionalidad.

Una relación puede ser opcional or obligatorio.

Considerando el Uno a muchos lado: siempre es opcional y no podemos hacer nada al respecto. los Muchos a uno lado, por otro lado, nos ofrece la opción de hacerlo obligatorio.

Por defecto, la relación es opcional, lo que significa que podemos guardar un Course sin asignarle un profesor:

Course course = new Course("C# 101");
entityManager.persist(course);

Ahora, hagamos que esta relación sea obligatoria. Para hacer eso, usaremos el optional argumento de la @ManyToOne anotación y configúrelo en false (su true por defecto):

@ManyToOne(optional = false)
@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")
private Teacher teacher;

Por tanto, ya no podemos guardar un curso sin asignarle un profesor:

Course course = new Course("C# 101");
assertThrows(Exception.class, () -> entityManager.persist(course));

Pero si le damos un maestro, vuelve a funcionar bien:

Teacher teacher = new Teacher();
teacher.setLastName("Doe");
teacher.setFirstName("Will"); Course course = new Course("C# 101");
course.setTeacher(teacher); entityManager.persist(course);

Bueno, al menos eso parece. Si hubiéramos ejecutado el código, se habría lanzado una excepción:

javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: com.fdpro.clients.stackabuse.jpa.domain.Course

¿Por qué es esto? Hemos establecido un valor válido Teacher objeto en el Course objeto que estamos tratando de persistir. Sin embargo, no hemos persistido Teacher objeto antes tratando de persistir el Course objeto.

Por lo tanto, la Teacher el objeto no es un entidad gestionada. Arreglemos eso e intentemos de nuevo:

Teacher teacher = new Teacher();
teacher.setLastName("Doe");
teacher.setFirstName("Will");
entityManager.persist(teacher); Course course = new Course("C# 101");
course.setTeacher(teacher); entityManager.persist(course);
entityManager.flush();

La ejecución de este código persistirá en ambas entidades y mantendrá la relación entre ellas.

Operaciones en cascada

Sin embargo, podríamos haber hecho otra cosa, podríamos haber en cascada, y así propagó la persistencia de la Teacher objeto cuando persistimos el Course objeto.

Esto tiene más sentido y funciona de la manera que esperaríamos en el primer ejemplo que arrojó una excepción.

Para hacer esto, modificaremos el cascade bandera de la anotación:

@ManyToOne(optional = false, cascade = CascadeType.PERSIST)
@JoinColumn(name = "TEACHER_ID", referencedColumnName = "ID")
private Teacher teacher;

De esta manera, Hibernate también sabe que debe persistir el objeto necesario en esta relación.

Hay varios tipos de operaciones en cascada: PERSIST, MERGE, REMOVE, REFRESH, DETACHy ALL (que combina todos los anteriores).

También podemos poner el argumento en cascada en el Uno a muchos lado de la relación, de modo que las operaciones se transmitan en cascada de los profesores a sus cursos también.

Programa XNUMX a XNUMX

Ahora que hemos establecido las bases del mapeo de relaciones en JPA a través de Uno a varios / varios a uno relaciones y su entorno, podemos pasar a Programa XNUMX a XNUMX relaciones.

Esta vez, en lugar de tener una relación entre una entidad en un lado y un grupo de entidades en el otro, tendremos un máximo de una entidad en cada lado.

Esta es, por ejemplo, la relación entre un Course y su CourseMaterial. Primero mapeemos CourseMaterial, que aún no hemos hecho:

@Entity
public class CourseMaterial { @Id private Long id; private String url;
}

La anotación para mapear una sola entidad a una sola otra entidad es, como era de esperar, @OneToOne.

Antes de configurarlo en nuestro modelo, recordemos que una relación tiene un lado propietario, preferiblemente el lado que contendrá la clave externa en la base de datos.

En nuestro ejemplo, eso sería CourseMaterial ya que tiene sentido que haga referencia a un Course (aunque podríamos ir al revés):

@OneToOne(optional = false)
@JoinColumn(name = "COURSE_ID", referencedColumnName = "ID")
private Course course;

No tiene sentido tener material sin un curso que lo abarque. Por eso la relación no es optional en esa direccion.

Hablando de dirección, hagamos la relación bidireccional, para que podamos acceder al material de un curso si lo tiene. En el Course clase, agreguemos:

@OneToOne(mappedBy = "course")
private CourseMaterial material;

Aquí, le estamos diciendo a Hibernate que el material dentro de un Course ya está mapeado por el course del objeto CourseMaterial entidad.

Además, no hay optional atributo aquí como es true por defecto, y podríamos imaginar un curso sin material (de un profesor muy vago).

Además de hacer que la relación sea bidireccional, también podríamos agregar operaciones en cascada o hacer que las entidades se carguen con entusiasmo o pereza.

Muchos a muchos

Ahora, por último, pero no menos importante: Muchos a muchos relaciones. Los dejamos para el final porque requieren un poco más de trabajo que los anteriores.

Efectivamente, en una base de datos, un Muchos a muchos La relación implica una tabla intermedia que hace referencia ambas otras tablas.

Afortunadamente para nosotros, JPA hace la mayor parte del trabajo, solo tenemos que lanzar algunas anotaciones y se encarga del resto por nosotros.

Entonces, para nuestro ejemplo, el Muchos a muchos la relación será la que hay entre Student y Course Los casos en que un estudiante puede asistir a varios cursos y un curso puede ser seguido por varios estudiantes.

Para mapear un Muchos a muchos relación usaremos el @ManyToMany anotación. Sin embargo, esta vez también usaremos un @JoinTable anotación para configurar la tabla que representa la relación:

@ManyToMany
@JoinTable( name = "STUDENTS_COURSES", joinColumns = @JoinColumn(name = "COURSE_ID", referencedColumnName = "ID"), inverseJoinColumns = @JoinColumn(name = "STUDENT_ID", referencedColumnName = "ID")
)
private List<Student> students;

Ahora, repase lo que está pasando aquí. La anotación toma algunos parámetros. En primer lugar, debemos darle un nombre a la tabla. Lo hemos elegido para ser STUDENTS_COURSES.

Después de eso, necesitaremos decirle a Hibernate a qué columnas unirse para completar STUDENTS_COURSES. El primer parámetro, joinColumns define cómo configurar la columna de unión (clave externa) del lado propietario de la relación en la tabla. En este caso, el lado propietario es un Course.

Por otro lado, la inverseJoinColumns El parámetro hace lo mismo, pero para el lado de referencia (Student).

Configuremos un conjunto de datos con estudiantes y cursos:

Student johnDoe = new Student();
johnDoe.setFirstName("John");
johnDoe.setLastName("Doe");
johnDoe.setBirthDateAsLocalDate(LocalDate.of(2000, FEBRUARY, 18));
johnDoe.setGender(MALE);
johnDoe.setWantsNewsletter(true);
johnDoe.setAddress(new Address("Baker Street", "221B", "London"));
entityManager.persist(johnDoe); Student willDoe = new Student();
willDoe.setFirstName("Will");
willDoe.setLastName("Doe");
willDoe.setBirthDateAsLocalDate(LocalDate.of(2001, APRIL, 4));
willDoe.setGender(MALE);
willDoe.setWantsNewsletter(false);
willDoe.setAddress(new Address("Washington Avenue", "23", "Oxford"));
entityManager.persist(willDoe); Teacher teacher = new Teacher();
teacher.setFirstName("Jane");
teacher.setLastName("Doe");
entityManager.persist(teacher); Course javaCourse = new Course("Java 101");
javaCourse.setTeacher(teacher);
entityManager.persist(javaCourse); Course sqlCourse = new Course("SQL 101");
sqlCourse.setTeacher(teacher);
entityManager.persist(sqlCourse);

Por supuesto, esto no funcionará de inmediato. Tendremos que agregar un método que nos permita agregar estudiantes a un curso. Modifiquemos el Course clase un poco:

public class Course { private List<Student> students = new ArrayList<>(); public void addStudent(Student student) { this.students.add(student); }
}

Ahora, podemos completar nuestro conjunto de datos:

Course javaCourse = new Course("Java 101");
javaCourse.setTeacher(teacher);
javaCourse.addStudent(johnDoe);
javaCourse.addStudent(willDoe);
entityManager.persist(javaCourse); Course sqlCourse = new Course("SQL 101");
sqlCourse.setTeacher(teacher);
sqlCourse.addStudent(johnDoe);
entityManager.persist(sqlCourse);

Una vez que este código se haya ejecutado, persistirá nuestro Course, Teacher y Student instancias así como sus relaciones. Por ejemplo, recuperemos a un estudiante de un curso persistente y verifiquemos si todo está bien:

Course courseWithMultipleStudents = entityManager.find(Course.class, 1L); assertThat(courseWithMultipleStudents).isNotNull();
assertThat(courseWithMultipleStudents.students()) .hasSize(2) .extracting(Student::firstName) .containsExactly("John", "Will");

Por supuesto, todavía podemos mapear la relación como bidireccional de la misma manera que lo hicimos para las relaciones anteriores.

También podemos realizar operaciones en cascada y definir si las entidades deben cargarse de forma lenta o impaciente (Muchos a muchos las relaciones son perezosas por defecto).

Conclusión

Con esto concluye este artículo sobre las relaciones de las entidades mapeadas con JPA. Hemos cubierto Muchos a uno, Uno a muchos, Muchos a muchos y Programa XNUMX a XNUMX relaciones. Además, hemos explorado operaciones en cascada, bidireccionalidad, opcionalidad y tipos de recuperación de carga ansiosa / perezosa.

El código de esta serie se puede encontrar en GitHub.

Fuente: https://stackabuse.com/a-guide-to-jpa-with-hibernate-relationship-mapping/

punto_img

Información más reciente

punto_img