Persistencia con JPA (1): entidades y acceso a la base de datos
Introducción
Sección titulada «Introducción»Los desarrolladores de aplicaciones orientadas a objetos (OO), como Java, que necesitan guardar sus datos en una base de datos relacional se enfrentan a un problema conocido como impedance mismatch (desajuste objeto-relacional): el modelo de datos de OO y el modelo relacional no coinciden, y alguien tiene que traducir entre los dos.
- En OO, los objetos se conectan por referencia: un objeto navega directamente hacia otro a través de un atributo (
empleado.getDepartment()), formando un grafo de objetos. - En el modelo relacional, los datos viven en tablas independientes: la relación entre dos filas no se navega, se reconstruye con una llave foránea y una consulta.
flowchart LR
subgraph app["Aplicación (objetos en memoria)"]
direction TB
e["empleado: EmployeeEntity"]
d["departamento: DepartmentEntity"]
c["compañía: CompanyEntity"]
e -->|"getDepartment()"| d
d -->|"getCompany()"| c
end
mapping{{"JPA<br/>(mapping)"}}
subgraph db["Base de datos relacional"]
direction TB
et[("EMPLOYEE_ENTITY")]
dt[("DEPARTMENT_ENTITY")]
ct[("COMPANY_ENTITY")]
end
app --> mapping --> db
Figura 1. Impedance mismatch.
Los Object-Relational Mapping (ORM) existen para resolver este problema: se encargan de traducir automáticamente entre objetos y tablas. En Java, el estándar es Java Persistence API (JPA), un conjunto de interfaces independiente de un proveedor específico. Existen varias implementaciones (Hibernate, EclipseLink, TopLink); este curso usa Hibernate, que es el proveedor que trae por defecto spring-boot-starter-data-jpa.
Esta primera parte muestra cómo JPA mapea una entidad a una tabla, dónde vive esa tabla (H2) y cómo se accede a ella con un repository. La segunda parte trata las asociaciones entre entidades y el ciclo de vida de los objetos.
Entidades y tablas
Sección titulada «Entidades y tablas»Una clase con @Entity se mapea a una tabla y cada uno de sus atributos a una columna. @Id identifica la llave primaria, y @GeneratedValue permite que la base de datos genere su valor.
Como todas las entidades del curso necesitan un identificador manejado de la misma forma, conviene definirlo una sola vez en una superclase y que las demás lo hereden, en vez de repetir @Id/@GeneratedValue en cada una:
@MappedSuperclasspublic abstract class BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;}@MappedSuperclass le indica a JPA que BaseEntity no es una entidad con tabla propia: sus atributos se agregan a la tabla de cada subclase que la extiende. Toda entidad del curso hereda de BaseEntity en vez de declarar su propio id:
@Data@Entitypublic class EmployeeEntity extends BaseEntity { private String name; private Double salary;}@GeneratedValue(strategy = ...) acepta tres estrategias:
| Estrategia | La llave primaria la genera… |
|---|---|
GenerationType.IDENTITY |
la columna autoincremental de la base de datos. |
GenerationType.SEQUENCE |
un objeto SEQUENCE de la base de datos, aparte de la tabla. |
GenerationType.TABLE |
una tabla auxiliar que simula una secuencia; funciona en cualquier base de datos, a costa de ser la más lenta. |
Convención de nombres de este curso: la clase de una entidad lleva el mismo nombre del concepto en el modelo de dominio, con el sufijo Entity (Employee se convierte en EmployeeEntity).
En este caso, los atributos se representan como columnas de EMPLOYEE_ENTITY, incluido el id heredado de BaseEntity.
Cuando el nombre o el tipo por defecto no sirven
Sección titulada «Cuando el nombre o el tipo por defecto no sirven»Sin anotaciones adicionales, JPA deriva el nombre de la tabla del nombre de la clase (EmployeeEntity se convierte en EMPLOYEE_ENTITY) y el nombre de cada columna del atributo correspondiente. Mientras la base de datos se crea desde cero a partir de las entidades, esto alcanza y evita anotar de más.
Hay dos situaciones donde el valor por defecto no sirve: la base de datos ya existe con sus propios nombres de tabla y columna, o el tipo SQL que JPA elegiría no es el que se necesita. Para esos casos, @Table y @Column fijan el nombre o el tipo de forma explícita.
@Entity@Table(name = "empleados")public class EmployeeEntity extends BaseEntity { @Column(name = "nombre_completo") private String name;
@Column(name = "salario", columnDefinition = "DECIMAL(10,2)") private Double salary;}@Table(name = "...") reemplaza el nombre que JPA le habría dado a la tabla (EMPLOYEE_ENTITY) por empleados; sirve tanto para elegir otro nombre como para que la entidad coincida con una tabla que ya existe. @Column(name = "...") hace lo mismo para una columna puntual, y columnDefinition reemplaza el tipo SQL que JPA generaría por defecto: aquí, DECIMAL(10,2) en vez del tipo que le correspondería a un Double, útil cuando se necesita precisión exacta o cuando el tipo debe coincidir con una columna ya existente.
H2, la base de datos del curso
Sección titulada «H2, la base de datos del curso»Todo lo anterior describe cómo se mapea una entidad a una tabla, pero no dónde vive esa tabla. En este curso esa base de datos es H2: una base de datos relacional liviana, escrita en Java, que corre embebida en el mismo proceso de la aplicación Spring Boot. No hay que instalar ni levantar un servidor de base de datos aparte, lo que la hace cómoda para desarrollo.
Spring Boot se conecta a H2 mediante application.properties:
spring.datasource.url=jdbc:h2:mem:nombre_bdspring.datasource.driverClassName=org.h2.Driverspring.jpa.hibernate.ddl-auto=updatespring.jpa.hibernate.ddl-auto=update es la pieza que conecta esta configuración con todo lo visto hasta ahora: al arrancar, Hibernate lee las clases anotadas con @Entity y crea o actualiza las tablas correspondientes; no hay que escribir el CREATE TABLE a mano.
jdbc:h2:mem: indica que la base de datos vive en memoria: sus datos desaparecen cada vez que la aplicación se detiene. La alternativa jdbc:h2:file: la guarda en un archivo en disco, que persiste entre ejecuciones.
Si además se carga un archivo data.sql con datos iniciales, hay que agregar spring.jpa.defer-datasource-initialization=true. Por defecto Spring Boot ejecuta data.sql antes de que Hibernate cree las tablas a partir de las entidades, y con una base H2 en memoria el primer INSERT falla porque la tabla todavía no existe. Con esa propiedad, data.sql se ejecuta después de que Hibernate las crea.
Con spring.h2.console.enabled=true, H2 expone una consola web (en /h2-console) para ver las tablas y ejecutar consultas SQL sin instalar un cliente de base de datos aparte.
Acceso a la base de datos
Sección titulada «Acceso a la base de datos»Una entidad por sí sola no accede a la base de datos: hace falta un repository. Declara el tipo de entidad y el tipo de su identificador:
public interface EmployeeRepository extends JpaRepository<EmployeeEntity, Long> {}JpaRepository provee, entre otras, estas operaciones:
| Operación | Método | Parámetros | Retorno |
|---|---|---|---|
| Crear o actualizar | save |
el objeto a persistir | el objeto guardado, con el id que la base de datos generó si era nuevo |
| Leer uno | findById |
id de la entidad buscada |
Optional con el objeto, si existe |
| Leer todos | findAll |
ninguno | la colección completa de esa entidad |
| Borrar | deleteById |
id de la entidad a borrar |
ninguno |
Un repository también puede declarar consultas derivadas del nombre del método, como findByName. La página de Lógica de negocio muestra varios ejemplos reales de este tipo de consultas.