Guía de JPA con Hibernate - Mapeo básico

Hibernate es un proveedor popular de JPA. En este artículo, profundizaremos en las asignaciones básicas en Hibernate y asignaremos un modelo de dominio Java a una base de datos.

Introducción

La API de persistencia de Java (JPA) es el estándar de persistencia del ecosistema de Java. Nos permite mapear nuestro modelo de dominio directamente a la estructura de la base de datos y luego nos brinda la flexibilidad de manipular solo objetos en nuestro código. Esto nos permite no 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, exploraremos la configuración y el mapeo básico en Hibernate:

  • Guía de JPA con Hibernate: Mapeo Básico (usted está aquí)
  • Guide to JPA with Hibernate: Mapeo de relaciones
  • Guide to JPA with Hibernate: Mapeo de herencia
  • Guía de JPA con Hibernate: consulta (próximamente!)

¿Qué es JPA?

API de persistencia de Java

JPA es una API que tiene como objetivo estandarizar la forma en que accedemos a una base de datos relacional desde el software Java utilizando Object Relational Mapping (ORM).

Fue desarrollado como parte del JSR 220 por un grupo de expertos en software EJB 3.0, aunque es no solo dedicado al desarrollo de software EJB.

JPA no es más que una API y, por lo tanto, no proporciona ninguna implementación, sino que únicamente define y estandariza los conceptos de ORM en Java.

Por lo tanto, para poder usarlo, debemos proporcionar una implementación de la API. Afortunadamente para nosotros, no estamos obligados a escribirlo nosotros mismos, ya hay implementaciones, llamadas proveedores, disponibles:

Cada proveedor, además de implementar la API, también proporciona algunas funciones específicas. En este artículo vamos a utilizar Hibernate como nuestro proveedor, aunque no veremos sus peculiaridades.

Asignación relacional de objetos

Mapeo relacional de objetos es una técnica utilizada para crear un mapeo entre una base de datos relacional y objetos de un software, en nuestro caso, objetos Java. La idea detrás de esto es dejar de trabajar con cursores o arreglos de datos obtenidos de la base de datos, y obtener directamente objetos que representen nuestro dominio comercial.

Para lograrlo, utilizamos técnicas para asignar nuestros objetos de dominio a las tablas de la base de datos para que se llenen automáticamente con los datos de las tablas. Luego, podemos realizar la manipulación de objetos estándar en ellos.

Nuestro ejemplo

Antes de comenzar, presentaremos el ejemplo que usaremos a lo largo de la serie. La idea es mapear el modelo de una escuela con estudiantes tomando cursos impartidos por profesores.

Así es como se ve el modelo final:

domain model

Como podemos ver, hay algunas clases con algunas propiedades. Y esas clases tienen relaciones entre ellas. Al final de esta serie, habremos asignado todas esas clases a las tablas de la base de datos y podremos guardar y recuperar datos de la base de datos usándolas.

Comenzando

Vayamos directo al grano con un ejemplo funcional, aunque minimalista. En primer lugar necesitaremos importar la Dependencia JPA/Hibernate. Usando Maven, agreguemos las dependencias necesarias a nuestro pom.xml:

1
2
3
4
5
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>${version}</version>
</dependency>

También necesitaremos una base de datos para trabajar. H2 es ligero y simple, así que seguiremos con eso:

1
2
3
4
5
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>${version}</version>
</dependency>

Luego, tendremos que crear un archivo persistence.xml en nuestro classpath, en un directorio META-INF. Este archivo se usa para configurar JPA, diciendo cuál es el proveedor, qué base de datos vamos a usar y cómo conectarnos, cuáles son las clases para mapear, etc.

Por ahora, se verá así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
             version="2.2">
    <persistence-unit name="guide-to-jpa-with-hibernate">
        <class>com.fdpro.clients.wikihtp.jpa.domain.Student</class>

        <properties>
            <!-- Database configuration -->
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:"/>
            <property name="javax.persistence.jdbc.user" value="user"/>
            <property name="javax.persistence.jdbc.password" value="password"/>

            <!-- Schema configuration -->
            <property name="javax.persistence.schema-generation.database.action" value="create"/>
        </properties>
    </persistence-unit>
</persistence>

No nos preocuparemos mucho por el significado de todo esto por ahora. Finalmente, vamos a mapear nuestra primera clase, Student:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Entity
public class Student {
    @Id
    private Long id;

    public Long id() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}

Esto significa que esta clase será una entidad en nuestra base de datos. Hibernate ahora sabe que debe mapear esta entidad en una tabla de base de datos y que estaremos llenando instancias de esta clase con los datos de la tabla. El @Id obligatorio servirá como clave principal de la tabla coincidente.

Ahora, veamos cómo manipular esta entidad:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("guide-to-jpa-with-hibernate");
EntityManager entityManager = entityManagerFactory.createEntityManager();

entityManager.getTransaction().begin();

Student student = new Student();
student.setId(1L);
entityManager.persist(student);

entityManager.getTransaction().commit();
entityManager.clear();

Student foundStudent = entityManager.find(Student.class, 1L);

assertThat(foundStudent).isEqualTo(student);

entityManager.close();

Nuevamente, no nos molestemos con todo aquí, ya que será mucho más simple. Esto es un poco crudo, pero es un enfoque de prueba de concepto para verificar si podemos acceder a la entidad mediante programación.

Todo lo que tenemos que saber por el momento es que este código nos permite guardar una entidad ‘Estudiante’ en la base de datos y luego recuperarla. La instrucción assertThat() pasa como que foundStudent es genuinamente el que estamos buscando.

Eso es todo para nuestros primeros pasos con la API de persistencia de Java. Tendremos la oportunidad de profundizar en los conceptos que usamos aquí en el resto del tutorial.

Configuración

Ahora es el momento de profundizar en la API, comenzando con el archivo de configuración persistence.xml. Vamos a ver qué tenemos que poner allí.

Espacio de nombres, esquema y versión {#esquema y versión del espacio de nombres}

En primer lugar, aquí está la etiqueta de apertura:

1
2
3
4
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
             version="2.2">

Aquí podemos ver que estamos definiendo el espacio de nombres, http://xmlns.jcp.org/xml/ns/persistence, y la ubicación del esquema, http://xmlns.jcp.org/xml/ns/ persistencia http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd (tenga en cuenta la versión).

Además, aunque ya lo mencionamos en la ubicación del esquema, estamos mencionando la versión nuevamente.

Entonces, aquí estamos trabajando con la versión 2.2 de JPA.

Unidad de persistencia

Luego, justo después de la etiqueta de apertura, declaramos una etiqueta <unidad-de-persistencia>:

1
<persistence-unit name="guide-to-jpa-with-hibernate">

Una unidad de persistencia define un conjunto de entidades administradas por una aplicación y que se encuentran en una base de datos determinada. Debe tener un nombre, que se usará más adelante. Toda la siguiente configuración estará dentro de esta unidad de persistencia, ya que se refiere a esa única base de datos.

Si tuviéramos que tener varias bases de datos diferentes y, por lo tanto, diferentes conjuntos de entidades, tendríamos que definir varias unidades de persistencia, todas con nombres diferentes.

Clases asignadas {#clases asignadas}

Entonces, lo primero que notamos en la unidad de persistencia es una etiqueta <clase> con el nombre calificado de nuestra clase Estudiante:

1
<class>com.fdpro.clients.wikihtp.jpa.domain.Student</class>

Eso es porque debemos definir manualmente cada clase mapeada en el archivo persistence.xml.

Los marcos como [Primavera] (https://spring.io/) simplificaron mucho este proceso al presentarnos la propiedad packagesToScan, que escanea automáticamente paquetes completos en busca de anotaciones.

Base de datos

Después de eso, están las propiedades, comenzando con la configuración de la base de datos:

1
2
3
4
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:"/>
<property name="javax.persistence.jdbc.user" value="user"/>
<property name="javax.persistence.jdbc.password" value="password"/>

Hay algunas líneas aquí, vamos a repasarlas una tras otra:

  • javax.persistence.jdbc.driver: El nombre calificado del controlador necesario para comunicarse con la base de datos.
  • javax.persistence.jdbc.url: La URL de la base de datos, aquí indicamos que queremos comunicarnos con una [instancia en memoria de H2](http://www.h2database.com/html/features.html #en_memoria_bases de datos).
  • javax.persistence.jdbc.user: El usuario para conectarse a la base de datos. En realidad, no importa lo que pongamos allí, ya que la instancia H2 no tiene un usuario específico. Incluso habríamos podido omitir esta línea.
  • javax.persistence.jdbc.password: La contraseña que coincide con el usuario. Lo mismo se aplica aquí para la instancia H2, podemos omitir esto o poner lo que queramos.

Esquema

Finalmente, le decimos a JPA que cree nuestro esquema al inicio. Lo hacemos principalmente porque estamos usando una base de datos en memoria y, por lo tanto, el esquema se pierde cada vez que se detiene la base de datos.

1
<property name="javax.persistence.schema-generation.database.action" value="create"/>

En una aplicación de producción con una base de datos persistente, probablemente no confiaríamos en este mecanismo para crear nuestro esquema de base de datos.

Mapeo de clases

Ahora que hemos cubierto nuestra configuración mínima, vayamos al tema principal: las asignaciones. Como recordatorio, el mapeo es el mecanismo para vincular nuestras clases de Java a las tablas de la base de datos.

Entonces, lo primero que debemos hacer para asignar una clase a una tabla de base de datos es anotarla con la anotación @Entity:

1
2
@Entity
public class Student {}

Si nos detenemos ahí, entonces JPA deducirá el nombre de la tabla del nombre de la clase: STUDENT. Las tablas de la base de datos no distinguen entre mayúsculas y minúsculas, pero para mayor claridad vamos a usar mayúsculas cuando nos referimos a ellas.

Pero ahora, ¿qué pasa si queremos asignar esa clase a una tabla con un nombre diferente, como STUD? Luego tenemos que usar la anotación @Table, que toma un atributo de nombre:

1
2
3
@Entity
@Table(name = "STUD")
public class Student {}

Ahora, nuestra clase está asignada a la tabla STUD en lugar de STUDENT. Esto resulta particularmente útil cuando se trabaja con una base de datos heredada, que puede tener nombres de tabla que son abreviaturas o nombres engorrosos. Entonces, podemos dar nombres propios a nuestras clases, incluso si los nombres de las tablas de la base de datos son muy diferentes.

Asignación de campos

Ahora, pasemos a mapear nuestros campos a las columnas de la base de datos. Dependiendo de los campos, hay algunas técnicas disponibles.

Conceptos básicos

Comencemos con los fáciles. Hay un montón de tipos que JPA maneja automáticamente:

  • Primitivos
  • Envoltorios de primitivas
  • Cadena
  • BigInteger, BigDecimal
  • Fechas (aunque su asignación podría requerir alguna configuración, por lo que tendrán su propia sección)

Cuando colocamos un campo de uno de esos tipos en nuestras clases, se asignan automáticamente a una columna del mismo nombre.

Entonces, si tuviéramos que agregar el apellido y el nombre a nuestro ‘Estudiante’:

1
2
3
4
public class Student {
    private String lastName;
    private String firstName;
}

Luego, estos campos se asignarían a las columnas denominadas APELLIDO y FIRSTNAME, respectivamente.

Nuevamente, ciertamente nos gustaría personalizar los nombres de nuestras columnas. Para hacer eso tendríamos que usar la anotación @Column y su atributo name:

1
2
3
4
5
6
public class Student {
    private String lastName;

    @Column(name = "FIRST_NAME")
    private String firstName;
}

Así, nuestro campo firstName se asigna a una columna FIRST_NAME.

Veamos si esto funciona recuperando un estudiante de la base de datos. En primer lugar, vamos a crear un archivo de conjunto de datos, data.sql, que pondremos en la raíz de nuestro classpath:

1
insert into STUD(ID, LASTNAME, FIRST_NAME) values(2, 'Doe', 'John');

Entonces, digámosle a JPA que cargue este conjunto de datos. Eso se hace usando la propiedad javax.persistence.sql-load-script-source en nuestro persistence.xml:

1
<property name="javax.persistence.sql-load-script-source" value="data.sql"/>

Finalmente podemos escribir una prueba afirmando que recuperamos a nuestro alumno y que sus datos son correctos:

1
2
3
4
5
Student foundStudent = entityManager.find(Student.class, 2L);

assertThat(foundStudent.id()).isEqualTo(2L);
assertThat(foundStudent.lastName()).isEqualTo("Doe");
assertThat(foundStudent.firstName()).isEqualTo("John");

identificaciones

Ahora, hablemos rápidamente sobre las identificaciones. Hay mucho que decir sobre ellos, aunque aquí solo entraremos en lo básico. Para declarar una ID, necesitamos usar la anotación @Id:

1
2
3
4
public class Student {
    @Id
    private Long id;
}

Pero, ¿qué es exactamente una identificación? Es el mapeo de la clave principal de nuestra tabla, es decir, la columna que identifica nuestras filas. A veces, queremos que nuestras claves principales se generen automáticamente. Para hacer eso en JPA, debemos usar la anotación @GeneratedValue junto con @Id:

1
2
3
4
5
public class Student {
    @Id
    @GeneratedValue
    private Long id;
}

Hay múltiples estrategias de generación de valor, que puedes especificar configurando la bandera strategy:

1
@GeneratedValue(strategy = GenerationType.TYPE)

Sin establecer la estrategia, Hibernate elegirá la que mejor se adapte a nuestro proveedor de base de datos.

Fechas

Mencionamos fechas antes, diciendo que naturalmente fueron manejadas por JPA, pero con algunas peculiaridades.

Entonces, antes que nada, recordemos que Java nos proporciona dos representaciones de fecha y hora: la del paquete java.util (Date, Timestamp, etc.) y la del java Paquete .time (LocalDate, LocalTime, LocalDateTime, etc.).

Los primeros se manejan mediante el uso de la anotación @Temporal, mientras que los últimos se manejan de forma predeterminada, pero solo desde la versión 2.2 de JPA. Antes de eso, hubiéramos tenido que usar convertidores, que veremos más adelante en este artículo para proyectos heredados.

Comencemos con el mapeo de un campo Fecha, digamos la fecha de nacimiento de un estudiante:

1
2
3
4
public class Student {
    @Temporal(TemporalType.DATE)
    private Date birthDate;
}

Podemos notar que la anotación @Temporal toma un argumento de tipo TemporalType. Esto debe especificarse para definir el tipo de columna en la base de datos.

¿Tendrá una fecha? ¿Un momento? ¿Una fecha y una hora?

Hay un valor enum para cada una de estas posibilidades: DATE, TIME y TIMESTAMP, respectivamente.

Necesitamos hacer eso porque un objeto Date mantiene juntas la fecha y la hora, lo que significa que debemos especificar qué parte de los datos realmente necesitamos.

La nueva representación de tiempo de Java nos lo facilitó, ya que hay un tipo específico para fecha, hora y fecha y hora.

Por lo tanto, si queremos usar una ‘Fecha local’ en lugar de una ‘Fecha’, simplemente podemos mapear el campo sin la anotación ‘@Temporal’:

1
2
3
public class Student {
    private LocalDate birthDate;
}

Y tan simple como eso, ¡nuestro campo está mapeado!

Enumeraciones

Otro tipo de campo que necesita atención específica son los enums. Fuera de la caja, JPA ofrece una anotación para mapear enums - @Enumerated. Esta anotación toma un argumento de tipo EnumType, que es un enum que ofrece los valores ORDINAL y STRING.

El primero asigna el enum a un número entero que representa su posición de declaración, lo que hace que esté prohibido cambiar el orden de las constantes enum. Este último utiliza los nombres de las constantes enum como el valor correspondiente en la base de datos. Con esta solución, no podemos cambiar el nombre de las constantes enum.

Además, si estamos trabajando con una base de datos heredada, es posible que nos veamos obligados a usar nombres ya almacenados para nuestras constantes enum, lo que quizás no queramos si esos nombres no son significativos. Entonces, la solución sería darle a enum un campo que represente el valor de la base de datos, permitiéndonos elegir cualquier nombre constante que consideremos adecuado, y usar un convertidor para mapear el tipo enum. Veremos convertidores en la siguiente sección.

Entonces, ¿qué dice todo esto sobre nuestro ejemplo de ‘Estudiante’? Digamos que queremos agregar género al estudiante, que está representado por un enum:

1
2
3
4
5
6
7
8
public enum Gender {
    MALE,
    FEMALE
}

public class Student {
    private Gender gender;
}

Luego, debemos agregar la anotación @Enumerated a nuestro campo de género para que sea mapeado:

1
2
3
4
public class Student {
    @Enumerated
    private Gender gender;
}

Pero, ¿qué pasa con el argumento del que hablamos antes? Por defecto, el EnumType seleccionado es ORDINAL. Sin embargo, podríamos querer cambiar eso a STRING:

1
2
3
4
public class Student {
    @Enumerated(EnumType.STRING)
    private Gender gender;
}

Y ahí estamos, los géneros de los estudiantes ahora se asignarán como ‘MASCULINO’ y ‘FEMENINO’ en la base de datos.

Convertidores

Esta sección será sobre los convertidores de los que hablamos mucho antes. Los convertidores se deben usar cuando queremos que una columna de la base de datos se asigne a un tipo que JPA no maneja de forma inmediata.

Digamos, por ejemplo, que tenemos una columna que nos dice si un estudiante quiere recibir el boletín escolar o no, pero los datos almacenados en esta columna son Y y N para "sí" y \ “no", respectivamente. Entonces tenemos múltiples posibilidades:

  • Asigne la columna a una Cadena, pero eso será engorroso de usar en el código.
  • Asigne la columna a algún tipo de SíNo enum, pero eso parece una exageración.
  • Asigne la columna a un ‘booleano’, ¡y ahora estamos llegando a alguna parte!

Entonces, ¿cómo logramos esto último? Mediante el uso de un convertidor. En primer lugar, debemos crear una clase YesNoBooleanConverter, que implemente la interfaz AttributeConverter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class YesNoBooleanConverter implements AttributeConverter<Boolean, String> {
    @Override
    public String convertToDatabaseColumn(Boolean attribute) {
        return null;
    }

    @Override
    public Boolean convertToEntityAttribute(String dbData) {
        return null;
    }
}

Notamos entonces que hay dos métodos para implementar. El primero convierte nuestro booleano en una Cadena para almacenarlo en la base de datos, mientras que el otro convierte un valor de la base de datos en un booleano. Vamos a implementarlos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class YesNoBooleanConverter implements AttributeConverter<Boolean, String> {
    @Override
    public String convertToDatabaseColumn(Boolean attribute) {
        return attribute ? "Y" : "N";
    }

    @Override
    public Boolean convertToEntityAttribute(String dbData) {
        return dbData.equals("Y");
    }
}

Aquí, consideramos que nuestra columna siempre tendrá un valor, pase lo que pase, y que este valor siempre será Y o N. Es posible que tengamos que escribir un poco más de código en casos más complejos (para manejar valores nulos, por ejemplo).

Ahora, ¿qué hacemos con eso? Asignaremos nuestro campo de estudiante con una anotación @Convert, que toma nuestra clase como argumento:

1
2
3
4
public class Student {
    @Convert(converter = YesNoBooleanConverter.class)
    private boolean wantsNewsletter;
}

Observe cómo mapeamos nuestro campo como un booleano primitivo, no como un tipo contenedor. Podemos hacer eso porque sabemos que nuestra columna siempre tendrá un valor y que el convertidor que escribimos nunca devuelve null como valor.

Pero, aún no hemos terminado. Todavía debemos agregar el convertidor a nuestro archivo persistence.xml:

1
<class>com.fdpro.clients.wikihtp.jpa.domain.converters.YesNoBooleanConverter</class>

Y ahora funciona. Sin embargo, ¿qué podemos hacer si tenemos un montón de columnas de sí/no en nuestra base de datos y nos resulta agotador repetir la anotación @Convert para esos tipos todo el tiempo? Luego podemos agregar una anotación @Converter a nuestra clase YesNoBooleanConverter y pasarle el argumento autoApply = true.

Luego, cada vez que tengamos un valor String en la base de datos que queramos mapear como un Boolean en nuestro código, se aplicará este convertidor. Vamos a agregarlo:

1
2
@Converter(autoApply = true)
public class YesNoBooleanConverter implements AttributeConverter<Boolean, String>

Y luego elimine la anotación @Convert de la clase `Student':

1
2
3
public class Student {
    private boolean wantsNewsletter;
}

Incrustado

Finalmente, hablemos de los tipos incrustados. ¿Para qué son? Imaginemos que nuestra tabla STUD contiene la información de la dirección de los estudiantes: calle, número y ciudad. Pero, en nuestro código nos gustaría usar un objeto Dirección, haciéndolo reutilizable y, sobre todo, un objeto (¡porque todavía estamos haciendo programación orientada a objetos!).

Ahora, hagámoslo en el código:

1
2
3
4
5
6
7
8
9
public class Address {
    private String street;
    private String number;
    private String city;
}

public class Student {
    private Address address;
}

Por supuesto, no funcionará así todavía. Debemos decirle a JPA qué tiene que ver con este campo. Para eso están las anotaciones @Embeddable y @Embedded. El primero irá en nuestra clase Dirección y el segundo en el campo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Embeddable
public class Address {
    private String street;
    private String number;
    private String city;
}

public class Student {
    @Embedded
    private Address address;
}

Veamos de nuevo nuestro conjunto de datos:

1
2
insert into STUD(ID, LASTNAME, FIRST_NAME, BIRTHDATE, GENDER, WANTSNEWSLETTER, STREET, NUMBER, CITY)
    values(2, 'Doe', 'John', TO_DATE('2000-02-18', 'YYYY-MM-DD'), 'MALE', 'Y', 'Baker Street', '221B', 'London');

Ha evolucionado un poco desde el principio. Puede ver aquí que agregamos todas las columnas de las secciones anteriores, así como la calle, el número y la ciudad. Hemos hecho esto como si los campos pertenecieran a la clase Estudiante, no a la clase Dirección.

Ahora, ¿nuestra entidad todavía está mapeada correctamente? Vamos a probarlo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Student foundStudent = entityManager.find(Student.class, 2L);

assertThat(foundStudent.id()).isEqualTo(2L);
assertThat(foundStudent.lastName()).isEqualTo("Doe");
assertThat(foundStudent.firstName()).isEqualTo("John");
assertThat(foundStudent.birthDateAsDate()).isEqualTo(DateUtil.parse("2000-02-18"));
assertThat(foundStudent.birthDateAsLocalDate()).isEqualTo(LocalDate.parse("2000-02-18"));
assertThat(foundStudent.gender()).isEqualTo(Gender.MALE);
assertThat(foundStudent.wantsNewsletter()).isTrue();

Address address = new Address("Baker Street", "221B", "London");
assertThat(foundStudent.address()).isEqualTo(address);

¡Todavía funciona bien!

Ahora, ¿qué pasa si queremos reutilizar la clase Dirección para otras entidades, pero los nombres de las columnas son diferentes? No entremos en pánico, JPA nos tiene cubiertos con la anotación @AttributeOverride.

Digamos que las columnas de la tabla STUD para la dirección son: ST_STREET, ST_NUMBER y ST_CITY. Puede parecer que nos estamos volviendo creativos, pero seamos honestos, el código heredado y las bases de datos son definitivamente lugares creativos.

Luego debemos decirle a JPA que anulamos el mapeo predeterminado:

1
2
3
4
5
6
public class Student {
    @AttributeOverride(name = "street", column = @Column(name = "ST_STREET"))
    @AttributeOverride(name = "number", column = @Column(name = "ST_NUMBER"))
    @AttributeOverride(name = "city", column = @Column(name = "ST_CITY"))
    private Address address;
}

Y ahí lo tenemos, nuestro mapeo está arreglado. Debemos tener en cuenta que, desde JPA 2.2, la anotación @AttributeOverride es repetible.

Antes de eso, hubiéramos tenido que envolverlos con la anotación @AttributeOverrides:

1
2
3
4
5
6
7
8
public class Student {
    @AttributeOverrides({
        @AttributeOverride(name = "street", column = @Column(name = "ST_STREET")),
        @AttributeOverride(name = "number", column = @Column(name = "ST_NUMBER")),
        @AttributeOverride(name = "city", column = @Column(name = "ST_CITY"))
    })
    private Address address;
}

Conclusión

En este artículo, nos sumergimos en lo que son JPA e Hibernate y su relación. Configuramos Hibernate en un proyecto de Maven y nos sumergimos en el mapeo relacional básico de objetos.

El código de esta serie se puede encontrar en GitHub. r/jpa).