1. Información general
El patrón Data Access Object (DAO) es un patrón estructural que nos permite aislar la capa de aplicación / negocio de la capa de persistencia (generalmente una base de datos relacional, pero podría ser cualquier otro mecanismo de persistencia) utilizando una API abstracta .
La funcionalidad de esta API es ocultar a la aplicación todas las complejidades involucradas en la realización de operaciones CRUD en el mecanismo de almacenamiento subyacente. Esto permite que ambas capas evolucionen por separado sin saber nada entre sí.
En este tutorial, profundizaremos en la implementación del patrón y aprenderemos cómo usarlo para abstraer llamadas a un administrador de entidades JPA.
2. Una implementación simple
Para comprender cómo funciona el patrón DAO, creemos un ejemplo básico.
Digamos que queremos desarrollar una aplicación que gestione usuarios. Para mantener el modelo de dominio de la aplicación completamente independiente de la base de datos, crearemos una clase DAO simple que se encargará de mantener estos componentes perfectamente desacoplados entre sí .
2.1. La clase de dominio
Como nuestra aplicación funcionará con los usuarios, necesitamos definir solo una clase para implementar su modelo de dominio:
public class User { private String name; private String email; // constructors / standard setters / getters }
La clase User es solo un contenedor simple para los datos del usuario, por lo que no implementa ningún otro comportamiento que valga la pena destacar.
Por supuesto, la elección de diseño más relevante que debemos tomar aquí es cómo mantener la aplicación que usa esta clase aislada de cualquier mecanismo de persistencia que pueda implementarse en algún momento.
Bueno, ese es exactamente el problema que el patrón DAO intenta abordar.
2.2. La API de DAO
Definamos una capa DAO básica, para que podamos ver cómo puede mantener el modelo de dominio completamente desacoplado de la capa de persistencia.
Aquí está la API de DAO:
public interface Dao { Optional get(long id); List getAll(); void save(T t); void update(T t, String[] params); void delete(T t); }
A vista de pájaro, es claro ver que el Dao interfaz define una API abstracta que lleva a cabo las operaciones CRUD en objetos de tipo T .
Debido al alto nivel de abstracción que proporciona la interfaz, es fácil crear una implementación concreta y detallada que funcione con objetos de usuario .
2.3. La clase UserDao
Definamos una implementación específica del usuario de la interfaz de Dao :
public class UserDao implements Dao { private List users = new ArrayList(); public UserDao() { users.add(new User("John", "[email protected]")); users.add(new User("Susan", "[email protected]")); } @Override public Optional get(long id) { return Optional.ofNullable(users.get((int) id)); } @Override public List 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 de usuario .
En aras de la simplicidad, la lista de usuarios actúa como una base de datos en memoria, que se llena con un par de objetos de usuario en el constructor .
Por supuesto, es fácil refactorizar los otros métodos para que puedan trabajar, por ejemplo, con una base de datos relacional.
Si bien las clases User y UserDao coexisten de forma independiente dentro de la misma aplicación, aún necesitamos ver cómo se puede usar esta última para mantener la capa de persistencia oculta a la lógica de la aplicación:
public class UserApplication { private static Dao userDao; public static void main(String[] args) { userDao = new UserDao(); User user1 = getUser(0); System.out.println(user1); userDao.update(user1, new String[]{"Jake", "[email protected]"}); User user2 = getUser(1); userDao.delete(user2); userDao.save(new User("Julie", "[email protected]")); userDao.getAll().forEach(user -> System.out.println(user.getName())); } private static User getUser(long id) { Optional user = userDao.get(id); return user.orElseGet( () -> new User("non-existing user", "no-email")); } }
El ejemplo es artificial, pero muestra, en pocas palabras, las motivaciones detrás del patrón DAO. En este caso, el método principal solo usa una instancia de UserDao para realizar operaciones CRUD en algunos objetos de usuario .
La faceta más relevante de este proceso es cómo UserDao oculta de la aplicación todos los detalles de bajo nivel sobre cómo se conservan, actualizan y eliminan los objetos .
3. Uso del patrón con JPA
Existe una tendencia general entre los desarrolladores a pensar que el lanzamiento de JPA degradó a cero la funcionalidad del patrón DAO, ya que el patrón se convierte en una capa más de abstracción y complejidad implementada además de la proporcionada por el administrador de entidades de JPA.
Sin duda, en algunos escenarios esto es cierto. Aun así , a veces solo queremos exponer a nuestra aplicación solo algunos métodos específicos de dominio de la API del administrador de la entidad. En tales casos, el patrón DAO tiene su lugar.
3.1. La clase JpaUserDao
Dicho esto, creemos una nueva implementación de la interfaz de Dao , para que podamos ver cómo puede encapsular la funcionalidad que el administrador de entidades de JPA proporciona de inmediato:
public class JpaUserDao implements Dao { private EntityManager entityManager; // standard constructors @Override public Optional get(long id) { return Optional.ofNullable(entityManager.find(User.class, id)); } @Override public List 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 action) { EntityTransaction tx = entityManager.getTransaction(); try { tx.begin(); action.accept(entityManager); tx.commit(); } catch (RuntimeException e) { tx.rollback(); throw e; } } }
La clase JpaUserDao es capaz de trabajar con cualquier base de datos relacional soportada por la implementación de JPA.
Además, si miramos de cerca la clase, nos daremos cuenta de cómo el uso de Inyección de composición y dependencia nos permite llamar solo a los métodos de administrador de entidades requeridos por nuestra aplicación.
En pocas palabras, tenemos una API personalizada para un dominio específico, en lugar de la API del administrador de la entidad completa.
3.2. Refactorización de la clase de usuario
En este caso, usaremos Hibernate como la implementación predeterminada de JPA, por lo que refactorizaremos la clase User en consecuencia:
@Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; private String name; private String email; // standard constructors / setters / getters }
3.3. Bootstrapping a JPA Entity Manager mediante programación
Assuming that we already have a working instance of MySQL running either locally or remotely and a database table “users” populated with some user records, we need to get a JPA entity manager, so we can use the JpaUserDao class for performing CRUD operations in the database.
In most cases, we accomplish this via the typical “persistence.xml” file, which is the standard approach.
In this case, we'll take an “xml-less” approach and get the entity manager with plain Java through Hibernate's handy EntityManagerFactoryBuilderImpl class.
For a detailed explanation on how to bootstrap a JPA implementation with Java, please check this article.
3.4. The UserApplication Class
Finally, let's refactor the initial UserApplication class, so it can work with a JpaUserDao instance and execute CRUD operations on the User entities:
public class UserApplication { private static Dao jpaUserDao; // standard constructors public static void main(String[] args) { User user1 = getUser(1); System.out.println(user1); updateUser(user1, new String[]{"Jake", "[email protected]"}); saveUser(new User("Monica", "[email protected]")); deleteUser(getUser(2)); getAllUsers().forEach(user -> System.out.println(user.getName())); } public static User getUser(long id) { Optional user = jpaUserDao.get(id); return user.orElseGet( () -> new User("non-existing user", "no-email")); } public static List 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); } }
Even when the example is pretty limited indeed, it remains useful for demonstrating how to integrate the DAO pattern's functionality with the one that the entity manager provides.
In most applications, there's a DI framework, which is responsible for injecting a JpaUserDao instance into the UserApplication class. For simplicity's sake, we've omitted the details of this process.
El punto más relevante a enfatizar aquí es cómo la clase JpaUserDao ayuda a mantener la clase UserApplication completamente independiente sobre cómo la capa de persistencia realiza operaciones CRUD .
Además, podríamos intercambiar MySQL por cualquier otro RDBMS (e incluso por una base de datos plana) más adelante, y aún así, nuestra aplicación continuaría funcionando como se esperaba, gracias al nivel de abstracción proporcionado por la interfaz Dao y el administrador de entidades. .
4. Conclusión
En este artículo, analizamos en profundidad los conceptos clave del patrón DAO, cómo implementarlo en Java y cómo usarlo en la parte superior del administrador de entidades de JPA.
Como de costumbre, todos los ejemplos de código que se muestran en este artículo están disponibles en GitHub.