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:
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 Course
s 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
, DETACH
y 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/