Entendiendo el Patrón Data Access Object en Java

¿Qué es el Patrón DAO?

El Patrón Data Access Object (DAO) es un patrón estructural que permite aislar la capa de aplicación/negocios de la capa de persistencia (generalmente una base de datos relacional, aunque puede ser cualquier otro mecanismo de persistencia) utilizando una API abstracta. Esta API oculta de la aplicación toda la complejidad de realizar operaciones CRUD en el mecanismo de almacenamiento subyacente. Esto permite que ambas capas evolucionen de forma independiente, sin tener conocimiento mutuo.

En esta entrada de blog, realizaremos un análisis exhaustivo sobre la implementación del patrón DAO y aprenderemos cómo utilizarlo para abstraer las llamadas a un JPA entity manager.

1. Visión General

El patrón DAO proporciona una manera de estructurar el acceso a los datos que ayuda a mantener una separación clara entre la lógica de negocio y el manejo de datos. Este patrón promueve el uso de interfaces para las operaciones de acceso a datos, permitiendo que la lógica de la aplicación no conozca los detalles de cómo se almacenan sus datos.

El siguiente ejemplo nos proporcionará un caso práctico de cómo implementar este patrón en Java.

2. Una Implementación Sencilla

Para entender cómo funciona el patrón DAO, vamos a crear un ejemplo básico. Supongamos que queremos desarrollar una aplicación que gestione usuarios. Queremos mantener completamente agnóstico el modelo de dominio de la aplicación sobre la base de datos. Por ello, crearemos una simple clase DAO que se encargará de mantener este desacoplamiento entre los componentes.

2.1 La Clase de Dominio

Dado que nuestra aplicación trabajará con usuarios, necesitamos definir solo una clase para implementar su modelo de dominio. A continuación se muestra la implementación:

public class User {
    private String name;
    private String email;

    // constructores / setters / getters estándar
}

La clase User es solo un contenedor simple para los datos del usuario, sin implicar ningún comportamiento adicional que merezca ser destacado. La decisión más importante aquí es cómo mantener la aplicación usando esta clase aislada de cualquier mecanismo de persistencia que pudiera implementarse.

2.2 La API DAO

Procedamos a definir una capa DAO básica para observar cómo mantiene completamente desacoplado el modelo de dominio de la capa de persistencia.

public interface Dao<T> {
    Optional<T> get(long id);
    List<T> getAll();
    void save(T t);
    void update(T t, String[] params);
    void delete(T t);
}

Desde una perspectiva general, es evidente que la interfaz Dao define una API abstracta que realiza operaciones CRUD sobre objetos del tipo T. Debido al alto nivel de abstracción que proporciona la interfaz, es fácil crear una implementación concreta que funcione con objetos User.

2.3 La Clase UserDao

Ahora, definiremos una implementación específica de Dao para la clase User:

public class UserDao implements Dao<User> {
    private List<User> users = new ArrayList<>();

    public UserDao() {
        users.add(new User("John", "john@example.com"));
        users.add(new User("Susan", "susan@example.com"));
    }

    @Override
    public Optional<User> get(long id) {
        return Optional.ofNullable(users.get((int) id));
    }

    @Override
    public List<User> getAll() {
        return users;
    }

    @Override
    public void save(User user) {
        users.add(user);
    }

    @Override
    public void update(User user, String[] params) {
        user.setName(Objects.requireNonNull(params[0], "Name cannot be null"));
        user.setEmail(Objects.requireNonNull(params[1], "Email cannot be null"));
        users.add(user);
    }

    @Override
    public void delete(User user) {
        users.remove(user);
    }
}

La clase UserDao implementa toda la funcionalidad necesaria para recuperar, actualizar y eliminar objetos User. Por simplicidad, la List users actúa como una base de datos en memoria, que se pobla con un par de objetos User en el constructor.

2.4 La Clase UserApplication

Aquí tienes cómo podemos utilizar esta clase en nuestra aplicación principal para realizar operaciones CRUD:

public class UserApplication {
    private static Dao<User> userDao;

    public static void main(String[] args) {
        userDao = new UserDao();
        
        User user1 = getUser(0);
        System.out.println(user1);
        userDao.update(user1, new String[]{"Jake", "jake@example.com"});
        
        User user2 = getUser(1);
        userDao.delete(user2);
        userDao.save(new User("Julie", "julie@example.com"));
        
        userDao.getAll().forEach(user -> System.out.println(user.getName()));
    }

    private static User getUser(long id) {
        Optional<User> user = userDao.get(id);
        
        return user.orElseGet(() -> new User("non-existing user", "no-email"));
    }
}

En este ejemplo, el método main utiliza una instancia de UserDao para realizar operaciones CRUD en unos pocos objetos User. La parte más relevante de este proceso es cómo UserDao oculta a la aplicación todos los detalles de bajo nivel sobre cómo se persisten, actualizan y eliminan los objetos.

3. Usando el Patrón con JPA

Recientemente, ha surgido la tendencia entre los desarrolladores a pensar que la llegada de JPA ha disminuido a cero la funcionalidad del patrón DAO. Esto se debe a que el patrón puede convertirse en una capa adicional de abstracción y complejidad sobre la que JPA proporciona a su entity manager.

Sin embargo, en ciertos escenarios, deseamos exponer a nuestra aplicación solo unos pocos métodos específicos de dominio de la API del entity manager. En tales casos, el patrón DAO sigue teniendo su lugar.

3.1 La Clase JpaUserDao

Vamos a crear una nueva implementación de la interfaz Dao para ver cómo puede encapsular la funcionalidad que proporciona el entity manager de JPA:

public class JpaUserDao implements Dao<User> {
    private EntityManager entityManager;

    // constructores estándar

    @Override
    public Optional<User> get(long id) {
        return Optional.ofNullable(entityManager.find(User.class, id));
    }

    @Override
    public List<User> getAll() {
        Query query = entityManager.createQuery("SELECT e FROM User e");
        return query.getResultList();
    }

    @Override
    public void save(User user) {
        executeInsideTransaction(entityManager -> entityManager.persist(user));
    }

    @Override
    public void update(User user, String[] params) {
        user.setName(Objects.requireNonNull(params[0], "Name cannot be null"));
        user.setEmail(Objects.requireNonNull(params[1], "Email cannot be null"));
        executeInsideTransaction(entityManager -> entityManager.merge(user));
    }

    @Override
    public void delete(User user) {
        executeInsideTransaction(entityManager -> entityManager.remove(user));
    }

    private void executeInsideTransaction(Consumer<EntityManager> action) {
        EntityTransaction tx = entityManager.getTransaction();
        try {
            tx.begin();
            action.accept(entityManager);
            tx.commit();
        } catch (RuntimeException e) {
            tx.rollback();
            throw e;
        }
    }
}

La clase JpaUserDao puede trabajar con cualquier base de datos relacional soportada por la implementación de JPA. A través del uso de Composición y Inyección de Dependencias, podemos llamar solo a los métodos necesarios del entity manager que requiere nuestra aplicación.

3.2 Refactorizando la Clase User

Vamos a refactorizar la clase User para que se ajuste a la estrategia de implementación de JPA con Hibernate:

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    
    private String name;
    private String email;

    // constructores estándar / setters / getters
}

3.3 Inicializando un Entity Manager de JPA

Asumiendo que tenemos una instancia de MySQL funcionando localmente o de forma remota y una tabla de base de datos “users” poblada con algunos registros de usuario, debemos obtener un entity manager de JPA:

// Ejemplo simplificado de cómo se obtiene un EntityManager
EntityManagerFactory emf = Persistence.createEntityManagerFactory("my-persistence-unit");
EntityManager em = emf.createEntityManager();

3.4 La Clase UserApplication con JpaUserDao

Finalmente, refactoricemos la clase inicial UserApplication para que trabaje con una instancia de JpaUserDao:

public class UserApplication {
    private static Dao<User> jpaUserDao;

    public static void main(String[] args) {
        jpaUserDao = new JpaUserDao(); // Injectar correctamente el EntityManager
        
        User user1 = getUser(1);
        System.out.println(user1);
        updateUser(user1, new String[]{"Jake", "jake@example.com"});
        saveUser(new User("Monica", "monica@example.com"));
        deleteUser(getUser(2));
        getAllUsers().forEach(user -> System.out.println(user.getName()));
    }

    public static User getUser(long id) {
        Optional<User> user = jpaUserDao.get(id);
        return user.orElseGet(() -> new User("non-existing user", "no-email"));
    }

    public static List<User> getAllUsers() {
        return jpaUserDao.getAll();
    }

    public static void updateUser(User user, String[] params) {
        jpaUserDao.update(user, params);
    }

    public static void saveUser(User user) {
        jpaUserDao.save(user);
    }

    public static void deleteUser(User user) {
        jpaUserDao.delete(user);
    }
}

El objetivo de este ejemplo es mostrar cómo integrar la funcionalidad del patrón DAO con la que proporciona el entity manager. En las aplicaciones más complejas, es común que haya un marco de DI responsable de inyectar una instancia de JpaUserDao en la clase UserApplication.

4. Conclusiones Prácticas

En este artículo, examinamos a fondo los conceptos clave del patrón DAO. Observamos cómo implementarlo en Java y cómo utilizarlo sobre el entity manager de JPA. A continuación se muestran algunos consejos prácticos para programadores especializados en Java:

  • Separa siempre tu lógica de negocio de la lógica de acceso a datos: Esto facilitará la prueba y el mantenimiento de tu aplicación.
  • Usa interfaces para definir tu API de acceso a datos: Esto permite intercambiar las implementaciones de acceso a datos sin afectar a la lógica de negocio.
  • Pensar en la escalabilidad: El patrón DAO es escalable, y podrás adaptarlo a cambios de tecnología en tu capa de persistencia.
  • Considera el uso de un marco de inyección de dependencias: Frameworks como Spring pueden ayudarte a gestionar tus instancias DAO, haciendo que tu código sea más limpio y manejable.

Siguiendo estos principios y utilizando el patrón DAO, podrás desarrollar aplicaciones Java robustas y mantenibles. ¡Feliz codificación!