En una arquitectura limpia, los repositorios tienen la responsabilidad de gestionar el acceso a los datos desde la perspectiva del dominio. Esto significa que trabajan exclusivamente con modelos de dominio, abstrayendo por completo los detalles de cómo y dónde se almacenan los datos. Su objetivo principal es proporcionar una interfaz coherente que permita a la capa de dominio interactuar con los datos sin preocuparse por las particularidades de la persistencia.
Para lograr este aislamiento, los repositorios no deberían implementar directamente las operaciones específicas de almacenamiento o recuperación. Aquí es donde entra en juego la introducción de los DAOs, que encapsulan los detalles de persistencia. Por ejemplo, en un sistema que combine una base de datos y un sistema de caché, los repositorios podrían delegar en un DAO el acceso a la caché para recuperar datos rápidamente o almacenar temporalmente resultados, manteniendo así su enfoque en los modelos de dominio.
En este tema, vamos a crear una aplicación que representará la capa de persistencia de nuestra aplicación de gestión de libros.
Nuestra capa de persistencia tiene que implementar los repositorios que están en la capa de dominio, por lo que tendremos que añadir esa dependencia en nuestro pom:
<dependency>
<groupId>es.cesguiro</groupId>
<artifactId>daw2-bookstore-domain</artifactId>
<version>${es.cesguiro.daw2-bookstore-domain.version}</version>
</dependency>
El patrón DAO (Data Access Object) se utiliza para separar y encapsular el acceso a los datos, proporcionando una capa de abstracción que aísla la lógica de acceso a datos del resto de la aplicación. Esto permite que el resto de la aplicación, y en particular la capa de dominio, interactúe con los datos sin preocuparse por los detalles específicos de persistencia. Los DAOs facilitan la delegación de las operaciones de acceso a los datos, de modo que los repositorios se puedan enfocar únicamente en trabajar con los modelos de dominio.
Por ejemplo, supongamos que queremos implementar en nuestra aplicación un sistema de caché con Redis para recuperar libros por su ISBN. En este caso, podemos hacer que el repositorio BookRepositoryImpl utilice dos DAOs: BookDao, que maneja el acceso a la base de datos, y BookRedisDao, que gestiona el almacenamiento en caché. De esta manera, el método findByIsbn intenta primero recuperar el libro de la caché a través de BookRedisDao. Si no se encuentra en la caché, BookDao realiza la consulta en la base de datos, y si el libro es encontrado, se guarda en la caché para futuras solicitudes.
Los datos se guardan en memoria RAM, por lo que la lectura y escritura es mucho más rápida que en una base de datos tradicional.
Redis soporta distintos tipos de estructuras de datos: strings, hashes, listas, conjuntos y sorted sets, lo que lo hace muy versátil.
Aunque es principalmente un almacenamiento en memoria, Redis permite persistir los datos en disco para mantenerlos entre reinicios si es necesario.
Es común usar Redis como nivel de cache entre la aplicación y la base de datos (patrón cache-aside), lo que reduce la carga sobre la base de datos y mejora el rendimiento de lectura.
En el contexto de nuestra aplicación, BookRedisDao actúa como un DAO específico de Redis: guarda y recupera libros usando su ISBN como clave, proporcionando un acceso mucho más rápido que la consulta directa a la base de datos.
Comenzaremos creando una interfaz genérica que servirá como base para nuestros DAOs: GenericDaoDb. Esta interfaz define los métodos básicos para interactuar con una base de datos.
En Java, las clases genéricas nos permiten escribir código flexible y reutilizable. Definir una clase o interfaz genérica significa que podemos trabajar con distintos tipos de datos sin necesidad de escribir código duplicado para cada tipo de entidad. En el caso de los DAOs, esto es útil porque muchas operaciones, como obtener todos los registros o buscar por ID, son comunes para todas las entidades.
Por ejemplo, consideremos el método findAll en la interfaz GenericDao. Este método devuelve una lista de entidades, pero al ser genérico, no podemos saber, a priori, de qué tipo. En lugar de definir un método que solo funcione con, por ejemplo, libros (Page<Book>), usamos un parámetro genérico (Page<T>) para que pueda adaptarse a cualquier tipo de entidad, ya sea Book, Author o Publisher:
public interface GenericDao<T> {
Page<T> findAll(int page, int size);
Optional<T> findById(Long id);
T insert(T entity);
T update(T entity);
void deleteById(Long id);
long count();
}
Al utilizar T, le estamos indicando al compilador que el tipo de la entidad que se va a utilizar será determinado cuando implementemos o instanciemos la interfaz. Esto hace que la interfaz sea flexible y reutilizable para cualquier entidad.
Cuando necesitamos operaciones específicas para ciertas entidades, podemos crear interfaces que extienden GenericDao y agregar los métodos adicionales que correspondan a esas entidades. Por ejemplo, PublisherDao puede extender GenericDao<Publisher> y sólo tendríamos que añadir el método findBySlug, ya que no será común a todos nuestros Daos.
public interface PublisherDao extends GenericDao<PublisherEntity> {
Optional<PublisherEntity> findBySlug(String slug);
}
De esta forma, podemos crear el resto de DAOs:
public interface BookDao extends GenericDao<BookEntity> {
Optional<BookEntity> findByIsbn(String isbn);
void deleteByIsbn(String isbn);
}
public interface AuthorDao extends GenericDao<AuthorEntity> {
Optional<AuthorEntity> findBySlug(String slug);
}
La persistencia de datos en aplicaciones es un aspecto fundamental para el desarrollo de sistemas robustos y escalables. En el entorno de Java, Jakarta Persistence ha surgido como la evolución moderna de Java Persistence API (JPA), ofreciendo una solución estándar para el mapeo objeto-relacional (ORM). Jakarta Persistence permite a los desarrolladores interactuar con bases de datos relacionales utilizando objetos Java de manera eficiente y coherente.
Originalmente, JPA se definía bajo el paquete javax.persistence. Tras la migración de Java EE a la Eclipse Foundation, la especificación se renombró a Jakarta Persistence y los paquetes pasaron a ser jakarta.persistence. La funcionalidad es prácticamente la misma: definir entidades, relaciones y operaciones CRUD de manera orientada a objetos, pero con el namespace actualizado. Esta transición es importante para la compatibilidad con versiones modernas de Spring Boot y Hibernate.
Jakarta Persistence facilita la representación de entidades de persistencia como objetos Java, permitiendo su almacenamiento y recuperación en bases de datos de manera transparente. Al abstraer la lógica de persistencia, simplifica el manejo de la capa de acceso a datos, promoviendo un código más limpio, mantenible y portable entre diferentes proveedores de bases de datos.
JPA define la gestión de datos en aplicaciones mediante la representación de objetos en una base de datos relacional. JPA proporciona un conjunto de interfaces y clases abstractas que permiten a los desarrolladores interactuar con la base de datos de una manera orientada a objetos, sin tener que preocuparse por detalles específicos de la implementación subyacente de la base de datos.
Existen diferentes implementaciones de JPA, entre las que destacan:
Estas implementaciones ofrecen funcionalidades similares en términos de mapeo objeto-relacional y operaciones CRUD, pero pueden diferir en sus características específicas, herramientas adicionales y rendimiento en ciertos contextos.
Al utilizar JPA, los desarrolladores pueden escribir código independiente de la base de datos subyacente, lo que les permite cambiar de proveedor de bases de datos sin tener que modificar considerablemente el código de la aplicación, lo que resulta en sistemas más flexibles y mantenibles a largo plazo.
En Spring Boot tenemos opciones para trabajar con JPA. Si bien Spring Data JPA es una forma común y conveniente de comenzar, podemos personalizar el enfoque dependiendo de las necesidades específicas del proyecto.
Por ejemplo, además de Spring Data JPA, podríamos optar por configurar JPA manualmente agregando las dependencias necesarias de forma individual. Esto nos ofrece un mayor control sobre cada componente que utilizamos.
Además, podemos elegir implementaciones específicas de JPA según nuestras preferencias o requisitos del proyecto. Aunque Hibernate es la implementación JPA predeterminada en Spring Boot, podemos optar por otras implementaciones como EclipseLink o Apache OpenJPA.
Como siempre, añadimos jakarta.persitence a nuestro pom:
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>${jakarta.persistence-api.version}</version>
</dependency>
Igual que con las validaciones, tenemos que indicar en algún lugar qué implementación concreta vamos a usar en nuestro proyecto. Por ahora, definiremos esa dependencia para que esté disponible en los tests.
En nuestro caso, vamos a utilizar la configuración común de JPA en un proyecto Spring. Al incluir Spring Data JPA en un proyecto Spring Boot, obtenemos una configuración predeterminada que facilita la interacción con bases de datos utilizando JPA.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${org.spring.boot.version}</version>
<scope>test</scope>
</dependency>
Este starter no solo proporciona Spring Data JPA, sino que también configura automáticamente otros componentes esenciales como Spring JDBC, Spring Transactions, Spring AOP y Spring Aspects, si se requieren para el contexto de persistencia de datos.
Además, añadiremos otra dependencia de Spring en los tests que lleva incorporado JUnit y Mockito, entre otras cosas:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${org.spring.boot.version}</version>
<scope>test</scope>
</dependency>
En nuestro archivo application.properties de test tendremos que configurar de la conexión a la bbdd. Además, vamos a añadir un par de opciones que nos serán útiles:
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MYSQL spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect spring.datasource.username=root spring.datasource.password=root spring.jpa.show-sql=true
La propiedad spring.jpa.hibernate.ddl-auto es una configuración en aplicaciones Spring que determina cómo Hibernate maneja la creación y actualización de la estructura de la base de datos. Tiene varios valores que permiten definir cómo se realiza esta gestión:
La última línea (spring.jpa.show-sql=true) es una configuración que se utiliza en aplicaciones Spring con Hibernate como implementación JPA. Cuando se establece a true, esta propiedad le indica a Hibernate que imprima las consultas SQL generadas por la aplicación en la consola, lo que nos puede ser útil mientras estamos en desarrollo.
JPA utiliza sus propias entidades que mapean una tabla de la bbdd. Para ello, debemos añadir la anotación @Entity a nuestras entidades.
@Entity
public class PublisherJpaEntity implements Serializable {
Una entidad de JPA debe tener un constructor vacío. Si no defines ningún otro constructor, el compilador de Java generará automáticamente uno vacío, por lo que JPA podrá instanciar la entidad sin problema.
Sin embargo, si añades un constructor con parámetros y no defines explícitamente un constructor vacío, JPA dará error al intentar instanciar la entidad.
JPA intentará conectar la entidad con la tabla correspondiente. El problema es que nuestras entidades llevan el sufijo JpaEntity, con lo que no encontrará ninguna tabla llamada PublisherJpaEntity. Por suerte, la solución es muy sencilla: indicarle el nombre de la tabla mediante la anotación @Table:
@Entity
@Table(name = "publishers")
public class PublisherJpaEntity implements Serializable {
Una entidad debe tener definida una clave primaria. La forma más sencilla de definir la clave primaria es mediante la anotación @Id.
Ademas, la anotación @GeneratedValue en JPA, junto con @Id, se utiliza para especificar cómo se generan los valores para una clave primaria en una entidad persistente. En particular, @GeneratedValue se usa para indicar la estrategia que se utilizará para generar los valores de las claves primarias de manera automática por parte de la base de datos.
La estrategia GenerationType.AUTO especifica que JPA deja que el persistence provider (por ejemplo, Hibernate) elija automáticamente la mejor estrategia según la base de datos que se esté usando. :
@Entity
@Table(name = "publishers")
@RedisHash("Publisher")
public class PublisherJpaEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private String slug;
//constructores, getters y setters
¿Qué pasa si los nombres de los campos de la tabla no coinciden con los de los atributos? Por ejemplo, en BookJpaEntity tenemos los campos titleEs, titleEn… mientras que en nuestra tabla los nombres son title_es, title_en… JPA no es capaz de asociar el atributo a un campo de la tabla. Por suerte, contamos con la anotación @Column para indicarle el nombre de la columna en la bbdd:
@Entity
@Table(name = "books")
public class BookJpaEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String isbn;
@Column(name = "title_es")
private String titleEs;
...
En JPA podemos representar relaciones entre entidades para facilitar el tratamiento de datos. Para hacerlo, contamos con una serie de anotaciones según el tipo de relación que queramos representar:
Las relaciones pueden ser bidireccionales o unidireccionales. Una relación bidireccional tiene dos lados:
En cambio, una relación unidireccional tiene solo el lado propietario.
El lado propietario de una relación es el que controla las actualizaciones que se reflejan en la base de datos.
Por ejemplo, BookJpaEntity tiene dos relaciones con PublisherJpaEntity y AuthorJpaEntity:
@Entity
@Table(name = "books")
public class BookJpaEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
@ManyToOne(fetch = FetchType.LAZY)
private PublisherJpaEntity publisher;
@ManyToMany
@JoinTable(
name = "books_authors",
joinColumns = @JoinColumn(name = "book_id"),
inverseJoinColumns = @JoinColumn(name = "author_id")
)
private List<AuthorJpaEntity> authors;
...
El primer tipo de relación (con PublisherJpaEntity) será ManyToOne. El atributo fetch indica cómo queremos recuperar los datos asociados. En este caso, indicamos que queremos Lazy Loading (carga perezosa).
El segundo tipo sería @ManyToMany. En este tipo de relaciones tenemos que indicarle la tabla de enganche y las claves ajenas de dicha tabla. Para eso, utilizamos la anotación @JoinTable.
Aunque podríamos dejarlo así, vamos a romper la relación ManyToMany y convertirla en una relación OneToMany/ManyToOne con la entidad intermedia BookAuthorJpaEntity. Este enfoque presenta varias ventajas frente al uso de una relación ManyToMany directa:
Convertir la relación ManyToMany en una estructura OneToMany/ManyToOne aporta una mayor robustez y flexibilidad al modelo de datos, lo que resulta especialmente útil en proyectos que pueden evolucionar o requerir información adicional en las asociaciones:
@Entity
@Table(name = "books")
public class BookJpaEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String isbn;
@Column(name = "title_es")
private String titleEs;
@Column(name = "title_en")
private String titleEn;
@Column(name = "synopsis_es", length = 2000)
private String synopsisEs;
@Column(name = "synopsis_en", length = 2000)
private String synopsisEn;
@Column(name = "base_price")
private BigDecimal basePrice;
@Column(name = "discount_percentage")
private Double discountPercentage;
private String cover;
@Column(name = "publication_date")
private String publicationDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "publisher_id")
private PublisherJpaEntity publisher;
@OneToMany(mappedBy = "book", cascade = CascadeType.ALL, orphanRemoval = true)
private List<BookAuthorJpaEntity> bookAuthors = new ArrayList<>();
@Entity
@Table(name = "book_author")
public class BookAuthorJpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "book_id")
private BookJpaEntity book;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id")
private AuthorJpaEntity author;
Además, podemos crear dos métodos en BookJpaEntity para leer/escribir los autores:
public List<AuthorJpaEntity> getAuthors() {
return bookAuthors.stream().map(BookAuthorJpaEntity::getAuthor).collect(Collectors.toList());
}
public void setAuthors(List<AuthorJpaEntity> authors) {
this.bookAuthors.clear();
for (AuthorJpaEntity author : authors) {
BookAuthorJpaEntity bookAuthor = new BookAuthorJpaEntity(this, author);
this.bookAuthors.add(bookAuthor);
}
}
Como es obvio, necesitamos poder convertir los DTOs que nos llegan de dominio (BookEntity) a entidades de JPA (BookJpaEntity). Podríamos hacer los mapeadores a mano, como hicimos en dominio, pero vamos a utilizar una librería que nos ayudará en la tarea: MapStruct.
MapStruct es una librería de mapeo de Java que te permite convertir automáticamente entre distintos tipos de objetos (DTOs, entidades, JPA entities, etc.) sin escribir manualmente los métodos de copia de propiedades.
Ventajas principales:
Como siempre, añadimos la dependencia correspondiente:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
Además, en el plugin de compilación de Maven debemos indicar el procesador de anotaciones:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${org.apache.maven.compiler.plugin.version}</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<parameters>true</parameters>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
Con ésto, ya podemos crear mapeadores con MapStruct, usando la anotación @Mapper:
@Mapper
public interface PublisherMapper {
PublisherMapper INSTANCE = Mappers.getMapper(PublisherMapper.class);
PublisherJpaEntity publisherEntityToPublisherJpaEntity(PublisherEntity publisherEntity);
PublisherEntity publisherJpaEntityToPublisherEntity(PublisherJpaEntity publisherJpaEntity);
}
Si los campos de las dos clases involucradas (en este caso PublisherEntity y PublisherJpaEntity) son del mismo tipo y tienen el mismo nombre, no hace falta añadir nada más. MapStruct creará la implementación de forma automática.
En general, un DAO JPA tendrá:
EntityManager actúa como el puente entre nuestras entidades Java y la base de datos. Permite realizar operaciones CRUD (crear, leer, actualizar, borrar) de manera transparente, sin tener que escribir SQL manualmente, y también gestionar el ciclo de vida de las entidades dentro del contexto de persistencia.
Con EntityManager podemos:
| Operación | Método | Descripción |
|---|---|---|
| Crear | entityManager.persist(entity) | Inserta un nuevo objeto en la base de datos (cuando se sincronice el contexto). |
| Leer | entityManager.find(BookJpaEntity.class, id) | Recupera la entidad con la clave primaria indicada. |
| Actualizar | entityManager.merge(entity) | Sincroniza los cambios de un objeto con la base de datos. |
| Borrar | entityManager.remove(entity) | Elimina la entidad de la base de datos. |
| Consultar | entityManager.createQuery(“SELECT b FROM BookJpaEntity b”, BookJpaEntity.class) | Ejecuta una consulta JPQL sobre entidades |
| Sincronizar | entityManager.flush() | Fuerza la escritura inmediata de los cambios pendientes en la base de datos. |
EntityManager mantiene un persistence context, es decir, un conjunto de entidades gestionadas en memoria.
Si obtenemos la misma entidad varias veces, JPA nos devolverá la misma instancia en memoria, y cualquier cambio realizado se sincronizará automáticamente con la base de datos cuando se complete la transacción.
En Jakarta Persistence (JPA), la anotación @PersistenceContext es la forma estándar de inyectar un EntityManager gestionado por el contenedor.
Esto significa que el contenedor se encarga del ciclo de vida del EntityManager: abrirlo, cerrarlo y sincronizarlo con las transacciones activas.
public class PublisherDaoJpa implements PublisherDao {
@PersistenceContext
private EntityManager entityManager;
De esta manera, cada DAO tiene un control completo sobre cómo se interactúa con la base de datos, permitiendo personalizar consultas, optimizar el rendimiento y combinar operaciones con otros sistemas como Redis, siguiendo el patrón de repositorios que hemos definido en la capa de dominio.
Con JPA disponemos de tres formas principales de consultar y operar con la base de datos:
JPQL / HQL (JPA Query Language)
JPQL (Java Persistence Query Language) es un lenguaje de consultas orientado a entidades, no a tablas. Permite escribir consultas similares a SQL, pero utilizando los nombres de las clases y sus atributos, en lugar de tablas y columnas.
List<BookJpaEntity> books = entityManager
.createQuery("SELECT b FROM BookJpaEntity b WHERE b.author = :author", BookJpaEntity.class)
.setParameter("author", "Isaac Asimov")
.getResultList();
| Ventajas | Inconvenientes |
|---|---|
| Más legible y cercano al modelo de dominio. | No soporta funciones específicas de cada motor SQL. |
| Independiente del motor de base de datos. | |
| Permite aprovechar relaciones (joins, fetch) entre entidades. |
Native SQL
Permite ejecutar consultas SQL crudas directamente sobre la base de datos, usando createNativeQuery. Se utiliza cuando JPQL no es suficiente o se necesita aprovechar funciones específicas del motor SQL.
List<Object[]> result = entityManager
.createNativeQuery("SELECT id, title FROM books WHERE author = ?")
.setParameter(1, "Isaac Asimov")
.getResultList();
También se pueden mapear los resultados a entidades o DTOs:
List<BookJpaEntity> books = entityManager
.createNativeQuery("SELECT * FROM books", BookJpaEntity.class)
.getResultList();
| Ventajas | Inconvenientes |
|---|---|
| Control total sobre la consulta. | Dependiente del dialecto de la base de datos. |
| Permite usar funciones y optimizaciones específicas del motor SQL. | No se aprovecha la abstracción del modelo de entidades. |
| Más propenso a errores de mantenimiento. |
Criteria API (CriteriaBuilder / CriteriaQuery)
Es un query builder tipado y programático que permite construir consultas dinámicamente mediante código Java, sin concatenar strings. Ideal para filtros opcionales o búsquedas avanzadas.
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<BookJpaEntity> cq = cb.createQuery(BookJpaEntity.class);
Root<BookJpaEntity> book = cq.from(BookJpaEntity.class);
cq.select(book)
.where(cb.equal(book.get("author"), "Isaac Asimov"));
List<BookJpaEntity> results = entityManager.createQuery(cq).getResultList();
| Ventajas | Inconvenientes |
|---|---|
| Totalmente tipado y seguro en tiempo de compilación. | Verboso y menos legible que JPQL. |
| Muy útil para construir consultas dinámicas. | Más complejo para consultas simples. |
| Evita errores de concatenación o typos en nombres de campos. |
Por ejemplo, en nuestro BookJpaDaoImpl, podemos añadir los métodos necesarios para recuperar datos:
public class BookJpaDaoJpaImpl implements BookJpaDao {
@PersistenceContext
private EntityManager entityManager;
@Override
public long count() {
return entityManager.createQuery("SELECT COUNT(b) FROM BookJpaEntity b", Long.class)
.getSingleResult();
}
@Override
public List<BookJpaEntity> findAll(int page, int size) {
int pageIndex = Math.max(page - 1, 0);
String sql = "SELECT b FROM BookJpaEntity b ORDER BY b.id";
TypedQuery<BookJpaEntity> bookJpaEntityPage = entityManager
.createQuery(sql, BookJpaEntity.class)
.setFirstResult(pageIndex * size)
.setMaxResults(size);
return bookJpaEntityPage.getResultList();
}
@Override
public Optional<BookJpaEntity> findById(Long id) {
return Optional.ofNullable(entityManager.find(BookJpaEntity.class, id));
}
@Override
public Optional<BookJpaEntity> findByIsbn(String isbn) {
String sql = "SELECT b FROM BookJpaEntity b WHERE b.isbn = :isbn";
try {
return Optional.of(entityManager.createQuery(sql, BookJpaEntity.class)
.setParameter("isbn", isbn)
.getSingleResult());
} catch (Exception e) {
return Optional.empty();
}
}
Y nuestro repositorio:
public class BookRepositoryImpl implements BookRepository {
private final BookJpaDao bookJpaDao;
public BookRepositoryImpl(BookJpaDao bookJpaDao) {
this.bookJpaDao = bookJpaDao;
}
@Override
public Page<BookEntity> findAll(int page, int size) {
List<BookEntity> content = bookJpaDao.findAll(page, size).stream()
.map(BookMapper.INSTANCE::fromBookJpaEntityToBookEntity)
.toList();
long totalElements = bookJpaDao.count();
return new Page<>(content, page, size, totalElements);
}
@Override
public Optional<BookEntity> findByIsbn(String isbn) {
return bookJpaDao.findByIsbn(isbn)
.map(BookMapper.INSTANCE::fromBookJpaEntityToBookEntity);
}
@Override
public Optional<BookEntity> findById(Long id) {
return bookJpaDao.findById(id)
.map(BookMapper.INSTANCE::fromBookJpaEntityToBookEntity);
}
Para las inserciones y actualizaciones, podemos crear el método sav() en el repositorio, y saber si es una actualización o inserción según si tiene o no id:
@Override
public BookEntity save(BookEntity bookEntity) {
BookJpaEntity bookJpaEntity = BookMapper.INSTANCE.fromBookEntityToBookJpaEntity(bookEntity);
if(bookEntity.id() == null) {
return BookMapper.INSTANCE.fromBookJpaEntityToBookEntity(bookJpaDao.insert(bookJpaEntity));
}
return BookMapper.INSTANCE.fromBookJpaEntityToBookEntity(bookJpaDao.update(bookJpaEntity));
}
En cuanto al borrado, lo hacemos por isbn:
@Override
public void deleteByIsbn(String isbn) {
bookJpaDao.deleteByIsbn(isbn);
}
En nuestro DAO, los métodos insert y deleteByIsbn son sencillos:
@Override
public BookJpaEntity insert(BookJpaEntity bookJpaEntity) {
entityManager.persist(bookJpaEntity);
return bookJpaEntity;
}
@Override
public void deleteById(Long id) {
entityManager.remove(entityManager.find(BookJpaEntity.class, id));
}
En cuanto al update, al hacer mapeos entre BookJpaEntity y BookEntity y haber roto la relación ManyToMany con Authors, primero debemos asegurarnos que eliminamos los autores anteriores:
@Override
public BookJpaEntity update(BookJpaEntity bookJpaEntity) {
BookJpaEntity managed = entityManager.find(BookJpaEntity.class, bookJpaEntity.getId());
if(managed == null) {
throw new ResourceNotFoundException("Book with id " + bookJpaEntity.getId() + " not found");
}
managed.getBookAuthors().clear();
entityManager.flush();
return entityManager.merge(bookJpaEntity);
}