Curso Jakarta EE 9 (30). JPA con Hibernate (13): clases DAO

logo Jakarta EE

Antes de proseguir con el estudio de las consultas JPQL, hagamos un pequeño paréntesis para ver cómo estructurar de forma práctica y sencilla el código relacionado con la persistencia.

>>>> ÍNDICE <<<<

El patrón de diseño DAO

Si bien es posible inyectar tanto el DataSource como el gestor de entidades donde los necesitemos, no es una buena idea -de hecho resulta horrible- ir repartiendo su uso por el código de nuestro proyecto sin seguir criterio alguno. Crearemos un caos que complicará sobremanera la comprensión y el mantenimiento del proyecto.

La solución habitual consiste en recurrir al patrón de diseño Data Access Object (objeto de acceso a datos), más conocido por las siglas DAO. Las clases DAO son las responsables de implementar todas las operaciones con una fuente y\o almacenamiento de datos. Fuera de ellas, el código no tiene conocimiento sobre cómo se realiza la persistencia; puede ser una base de datos relacional o «no SQL», ficheros de texto, etcétera. Exponemos una API y todo lo demás queda encapsulado y abstraído en los DAOs, los cuales, generalmente, obtendremos con una factoría. Por lo común, cuando la fuente de datos es una base de datos relacional, una clase DAO contiene todas las operaciones centradas en una tabla.

El patrón es aplicable a cualquier lenguaje orientado a objetos. Con JPA y un servidor de aplicaciones su empleo resulta sencillo. Tengamos en cuenta que:

  • Los objetos DAO serán creados e inyectados por CDI. En general, no tendremos que hacer nada, salvo, quizás, escribir algún método productor.
  • Crearemos un DAO para cada cada entidad que lo requiera.
  • En lugar de una fuente de datos -podríamos usarla tal y como vimos en su momento– dentro de nuestros DAOs trabajaremos con un gestor de entidades que recibiremos mediante inyección.
  • La gestión de las transacciones la delegaremos en JTA.

A continuación se muestra un primer ejemplo.

package com.danielme.jakartaee.jpa.dao;

import com.danielme.jakartaee.jpa.entities.Expense;
import jakarta.enterprise.context.ApplicationScoped;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.Optional;

@ApplicationScoped
public class ExpenseDAOImpl implements ExpenseDAO {

    @PersistenceContext
    private EntityManager em;

    @Override
    public Optional<Expense> findById(Long id) {
        return Optional.ofNullable(em.find(Expense.class, id));
    }

    @Override
    public void create(Expense expense) {
        em.persist(expense);
    }

    @Override
    public Expense save(Expense expense) {
        return em.merge(expense);
    }

    @Override
    public void deleteById(Long id) {
        em.remove(em.find(Expense.class, id));
    }

    @Override
    public void delete(Expense expense) {
        em.remove(expense);
    }

}

La clase ExpenseDAOImpl contendrá las operaciones individuales, centradas en la entidad Expense, que necesitemos hacer con el gestor de entidades, el cual nunca debería usarse fuera de los DAOs. Se trata de un CDI bean de ámbito aplicación porque es una clase que no guarda estados internos y puede reutilizarse con seguridad. La inyectaremos a través de una interfaz que expone sus operaciones.

public class ExpenseDAOTest {

    @Inject
    private ExpenseDAO expenseDAO;

    @Test
    void testFindById() {
        assertThat(expenseDAO.findById(Datasets.EXPENSE_ID_1)).isPresent();
    }

}

La transaccionalidad podemos definirla en el mismo DAO, pues las transacciones podrán ser reutilizadas, si así lo deseamos, en aquellas iniciadas por los métodos que invoquen al DAO. Sin embargo, prefiero no hacerlo porque me parece más natural declarar las transacciones solo en las clases que contienen los distintos procesos de negocio o casos de uso, los cuales suelen apoyarse en los DAOs.

Asimismo, este criterio facilita la detección de errores: si olvidamos definir un método de negocio como transaccional y usa métodos de DAOs que debieran serlo y no lo son, se lanzará la excepción correspondiente. En este escenario, si el método del DAO estuviera definido como transaccional, no sería tan evidente que nos hemos olvidado de hacer transaccional todo el método de negocio. En cualquier caso, la decisión final queda en manos del lector.

DAO genérico

Pasemos a implementar un DAO equivalente para la entidad Category.

@ApplicationScoped
public class CategoryDAOImpl implements CategoryDAO {

    @PersistenceContext
    private EntityManager em;

    @Override
    public Optional<Category> findById(Long id) {
        return Optional.ofNullable(em.find(Category.class, id));
    }

    @Override
    public void create(Category category) {
        em.persist(category);
    }

    @Override
    public Category save(Category category) {
        return em.merge(category);
    }

    @Override
    public void deleteById(Long id) {
        em.remove(em.find(Category.class, id));
    }

    @Override
    public void delete(Category category) {
        em.remove(category);
    }

}

¿No se parece demasiado a ExpenseDAO? ¡Solo cambia la entidad! Sequemos partido al tipado genérico de Java y procedamos a crear una DAO general. Vamos a contemplar que, además de la entidad (T), también puede cambiar el tipo de la clave primaria (K).

public interface GenericDAO<T, K> {

    Optional<T> findById(K id);

    void create(T entity);

    T save(T entity);

    void deleteById(Long id);

    void delete(T entity);

}

GenericDAO es el «contrato» que cumplirán todos los DAOs, lo que hará consistente a la capa de persistencia. En el ejemplo he declarado unos pocos métodos básicos, pero, naturalmente, podemos ir añadiendo los que queramos. Vamos a implementarlo para cualquier entidad.

public class GenericDAOImpl<T, K> implements GenericDAO<T, K> {

    private final EntityManager em;
    private final Class<T> entityClass;

    public GenericDAOImpl(EntityManager em, Class<T> entityClass) {
        this.em = em;
        this.entityClass = entityClass;
    }

    @Override
    public Optional<T> findById(K id) {
        return Optional.ofNullable(em.find(entityClass, id));
    }

    @Override
    public void create(T entity) {
        em.persist(entity);
    }

    @Override
    public T save(T entity) {
        return em.merge(entity);
    }

    @Override
    public void deleteById(Long id) {
        em.remove(em.find(entityClass, id));
    }

    @Override
    public void delete(T entity) {
        em.remove(entity);
    }

}

Obsérvese que la clase no es un CDI Bean inyectable y que tenemos que crearla con su constructor. Los DAOs concretos se instancian mediante productores. Hay uno por entidad y los distinguimos por su tipado.

class DaoProducer {

    @PersistenceContext
    private EntityManager em;

    @Produces
    @ApplicationScoped
    private GenericDAO<Expense, Long> produceExpenseDAO() {
        return new GenericDAOImpl<>(em, Expense.class);
    }

    @Produces
    @ApplicationScoped
    private GenericDAO<Category, Long> produceCategoryDAO() {
        return new GenericDAOImpl<>(em, Category.class);
    }

}
@Inject
GenericDAO<Expense, Long> expenseDAO;
@Inject
GenericDAO<Category, Long> categoryDAO;

@Test
void testExpenseGenericFindById() {
    assertThat(expenseGenericDAO.findById(Datasets.EXPENSE_ID_1)).isPresent();
}

@Test
void testCategoryGenericFindById() {
    assertThat(categoryDAO.findById(Datasets.CATEGORY_ID_FOOD)).isPresent();
}

Definir DAOs para otras entidades es tan fácil como añadir productores. No obstante, en la práctica rara vez tendremos entidades para las que sean suficientes las operaciones genéricas. En estos casos, desarrollaremos un DAO específico que herede del general y contenga las operaciones específicas de la entidad. Es lo que se ha hecho en el siguiente código: en ExpenseDAO disponemos de las funcionalidades de GenericDAO y un método que, por ejemplo, cuente todos los gastos de cierta categoría indicada por su identificador.

public interface ExpenseDAO extends GenericDAO<Expense, Long> {

    long countByCategoryId(Long categoryId);

}

Es el único método que contiene su implementación, declarada como un CDI Bean. El resto se hereda.

@ApplicationScoped
public class ExpenseDAOImpl extends GenericDAOImpl2<Expense, Long> implements ExpenseDAO {

    public ExpenseDAOImpl() {
        super(Expense.class);
    }

    @Override
    public long findAllByCategoryId(Long categoryId) {
        return em.createQuery("SELECT count(e) FROM Expense e WHERE e.category.id=:categoryId", Long.class)
                .setParameter("categoryId", categoryId)
                .getSingleResult();
    }
    
}

Por comodidad, en el ejemplo se usa GenericDAOImpl2, una nueva implementación de GenericDao que recibe el gestor de entidades para evitar inyectarlo desde las clases hijas.

    @PersistenceContext
    protected EntityManager em;
    protected final Class<T> entityClass;

    public GenericDAOImpl2(Class<T> entityClass) {
        this.entityClass = entityClass;
    }

Verifiquemos el DAO en ExpenseDAOTest. Ahora tenemos dos objetos candidatos a ser inyectados para satisfacer la dependencia GenericDAO<Expense, Long>: ExpenseDAOImpl y el creado en el productor. Empleamos un calificador en el segundo de ellos para resolver la ambigüedad. Con ExpenseDAO no hay dificultad alguna: solo hay una opción.

    @Inject
    @Named("expenseDaoGeneric")
    GenericDAO<Expense, Long> expenseGenericDAO;
    @Inject
    GenericDAO<Category, Long> categoryDAO;
    @Inject
    ExpenseDAO expenseDAO;


    @Test
    void testExpenseGenericFindById() {
        assertThat(expenseGenericDAO.findById(Datasets.EXPENSE_ID_1)).isPresent();
    }

    @Test
    void testCategoryGenericFindById() {
        assertThat(categoryDAO.findById(Datasets.CATEGORY_ID_FOOD)).isPresent();
    }

    @Test
    void testCountByCategory() {
        long count = expenseDAO.countByCategoryId(CATEGORY_ID_FOOD);

        assertThat(count).isEqualTo(2);
    }

Esta es la estrategia que seguiremos a partir de ahora en los proyectos de ejemplo.

Apache DeltaSpike

Explotar una base de datos es el fundamento de la mayoría de aplicaciones de gestión. De igual forma que con los ORM abstraemos la interacción directa con ellas, cabría preguntarse si una tarea tan común cuenta con alguna librería que contenga DAOs genéricos fáciles de integrar con JPA.

En el mundo Spring contamos con Spring Data JPA, imprescindible desde hace años en cualquier desarrollo basado en este popular ecosistema. En el blog le dediqué unos tutoriales básicos. En el caso de Jakarta, hay una librería llamada Apache DeltaSpike que contiene una serie de módulos basados en la especificación CDI que añaden nuevas capacidades. De todos ellos, el que buscamos se llama Data y nos permitirá, grosso modo, reemplazar los DAOs por interfaces de este estilo.

@Repository
public interface PersonRepository extends EntityRepository<Person, Long> {

    List<Person> findByAgeBetweenAndGender(int minAge, int maxAge, Gender gender);

    @Query("select p from Person p where p.ssn = ?1")
    Person findBySSN(String ssn);

    @Query(named=Person.BY_FULL_NAME)
    Person findByFullName(String firstName, String lastName);

    List<Person> findByNameLikeAndAgeBetweenAndGender(String name,
                              int minAge, int maxAge, Gender gender);

    long countByName(String name);

}

El ejemplo proviene de la documentación oficial y muestra un enorme parecido con Spring Data JPA. PersonRepository define el «repositorio» (*) de datos de la entidad Person y que interactúa con la base de datos empleando un gestor de entidades de JPA como mediador. Además de las operaciones que recibimos «gratis» al especializar EntityRepository, para usar una consulta JPQL estática basta con escribirla y asociarla a un método. DeltaSpike se encarga de reemplazar los posibles parámetros de entrada y devolver el resultado adecuado. Otra característica genial es que podemos definir consultas a partir del nombre del método, como findByNameLikeAndAgeBetweenAndGender y countByName.

(*) Repository es un patrón similar al DAO de más alto nivel y que puede estar internamente implementado con DAOs. Su descripción exacta depende del autor, siendo la más aceptada la propuesta por Erick Evans. En cualquier caso, nos centraremos en los DAO.

No estamos limitados a declarar métodos. Es posible crear el repositorio como una clase abstracta en la que implementar cualquier código, disponiendo del gestor de entidades. Por tanto, no perdemos la libertad absoluta que nos dan los DAOs que escribimos a medida, y ganamos una potente herramienta que nos simplificará el desarrollo de las capas de persistencia de nuestros proyectos. Eso sí, echaremos en falta alguna que otra característica de Spring Data JPA, más potente y con un desarrollo más activo.

Por los motivos anteriores, recomiendo al lector que evalúe el uso de Apache DeltaSpike Data. El problema es que actualmente (diciembre 2021) todavía no existe una versión totalmente compatible con Jakarta EE 9.

Código de ejemplo

El código de ejemplo se encuentra en GitHub (todos los proyectos son independientes pero están en un único repositorio). Para más información sobre cómo utilizar GitHub, consultar este artículo.

>>>> ÍNDICE <<<<

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.