Una vez decidida la tecnología de persistencia, surge un problema de diseño recurrente: ¿cómo conectamos la lógica de negocio con la base de datos sin que el código se llene de sentencias SQL desperdigadas y dependencias del motor concreto? Si cada parte de la aplicación habla directamente con la base, acabamos con un acoplamiento brutal, código imposible de probar y reglas de negocio mezcladas con detalles de infraestructura. Los patrones de acceso a datos resuelven exactamente esto: introducen una capa que aísla cómo se guardan los datos del qué se hace con ellos. En esta lección estudiaremos los cuatro patrones más importantes (DAO, Repository, Unit of Work) y la dicotomía Data Mapper frente a Active Record, con ejemplos en Java para que veas las diferencias en código real.
Contenido
- El problema: acoplamiento a la persistencia
- El patrón DAO (Data Access Object)
- El patrón Repository
- DAO vs Repository: ¿en qué se diferencian?
- El patrón Unit of Work
- Data Mapper vs Active Record
- Cómo encajan todos juntos
- El problema: acoplamiento a la persistencia
Imagina un servicio que mezcla lógica de negocio con acceso directo a la base:
public class ServicioPedidos {
public void confirmar(long pedidoId) throws SQLException {
Connection con = DriverManager.getConnection("jdbc:postgresql://...");
PreparedStatement ps = con.prepareStatement(
"UPDATE pedidos SET estado = 'CONFIRMADO' WHERE id = ?");
ps.setLong(1, pedidoId);
ps.executeUpdate(); // lógica de negocio y SQL mezclados
con.close();
}
}Esto tiene problemas graves: el servicio depende de JDBC y de PostgreSQL, es imposible de probar sin una base real, y la sentencia SQL se repetirá en cada sitio que necesite tocar pedidos. Los patrones siguientes separan responsabilidades para evitarlo.
- El patrón DAO (Data Access Object)
Un DAO encapsula todo el acceso a una fuente de datos concreta (una tabla, normalmente) y expone operaciones orientadas a la persistencia. Su vocabulario es el de la base de datos: insertar, actualizar, borrar, buscar por clave.
Primero definimos la interfaz, que oculta los detalles de implementación:
public interface PedidoDao {
void insertar(Pedido pedido);
void actualizar(Pedido pedido);
void eliminar(long id);
Pedido buscarPorId(long id);
List<Pedido> buscarPorClienteId(long clienteId);
}Cada método refleja una operación contra la tabla. Ahora una implementación con JDBC:
public class PedidoDaoJdbc implements PedidoDao {
private final DataSource dataSource; // pool de conexiones inyectado
public PedidoDaoJdbc(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Pedido buscarPorId(long id) {
String sql = "SELECT id, cliente_id, total, estado FROM pedidos WHERE id = ?";
try (Connection con = dataSource.getConnection();
PreparedStatement ps = con.prepareStatement(sql)) {
ps.setLong(1, id); // sustituye el ? por el id
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return new Pedido(
rs.getLong("id"),
rs.getLong("cliente_id"),
rs.getBigDecimal("total"),
rs.getString("estado"));
}
return null;
}
} catch (SQLException e) {
throw new AccesoDatosException("Error al buscar pedido " + id, e);
}
}
// ... resto de métodos
}Puntos clave de este código:
DataSourceinyectado: el DAO no crea la conexión, la recibe. Esto permite cambiar el origen de datos (incluido uno simulado en pruebas).try-with-resources: cierra automáticamenteConnection,PreparedStatementyResultSetaunque haya excepción, evitando fugas de recursos.PreparedStatementcon?: previene inyección SQL y permite reutilizar el plan de ejecución.- Traducción de excepciones: convierte la
SQLException(de bajo nivel) en una excepción propia, para que el resto de la aplicación no dependa de JDBC.
El DAO centraliza todo el SQL de la tabla pedidos en un único lugar.
- El patrón Repository
Un Repository opera a un nivel de abstracción más alto: simula ser una colección en memoria de objetos de dominio. Forma parte del vocabulario del Diseño Dirigido por el Dominio (visto en el Módulo 6) y se asocia a un agregado, no a una tabla. Su intención es que el código de negocio crea estar trabajando con una lista de objetos, ajeno a que detrás hay una base de datos.
public interface RepositorioPedidos {
void guardar(Pedido pedido); // "añadir a la colección"
Optional<Pedido> obtener(PedidoId id);
List<Pedido> pendientesDe(ClienteId cliente); // consulta con lenguaje del dominio
}Diferencias de matiz frente al DAO:
- Usa identificadores del dominio (
PedidoId,ClienteId), nolongcrudos. - Devuelve
Optionalen lugar denull, expresando explícitamente la ausencia. - Los métodos hablan el lenguaje ubicuo del negocio (
pendientesDe), no el de SQL. - Un único método
guardardecide internamente si hay que insertar o actualizar; quien lo llama no lo sabe ni le importa.
Una implementación con Spring Data JPA reduce el código a casi nada:
public interface RepositorioPedidos extends JpaRepository<Pedido, Long> {
// Spring genera la consulta a partir del nombre del método
List<Pedido> findByClienteIdAndEstado(Long clienteId, String estado);
}Aquí JpaRepository ya aporta save, findById, etc., y Spring deriva la consulta del nombre findByClienteIdAndEstado. La intención de negocio queda en el nombre, sin SQL manual.
- DAO vs Repository: ¿en qué se diferencian?
Es la confusión más habitual. Ambos abstraen el acceso a datos, pero su intención y nivel difieren:
| Aspecto | DAO | Repository |
|---|---|---|
| Orientación | A la tabla / fuente de datos | Al agregado del dominio |
| Vocabulario | Persistencia (insertar, actualizar) | Negocio (colección de objetos) |
| Granularidad | Una por tabla, normalmente | Una por agregado raíz |
| Procedencia | Patrón de capa de datos clásico | Patrón táctico de DDD |
| Conoce SQL | Sí, lo expone conceptualmente | No, lo oculta tras la "colección" |
En la práctica, un Repository suele apoyarse en uno o varios DAO o en un ORM. No son excluyentes: el Repository es la cara que ve el dominio, el DAO es la maquinaria interna.
- El patrón Unit of Work
¿Qué pasa cuando una operación de negocio modifica varios objetos y todos deben confirmarse juntos? Si llamamos a guardar uno por uno, podríamos confirmar la mitad y fallar en la otra mitad. El Unit of Work resuelve esto: mantiene una lista de objetos afectados durante una operación de negocio y coordina la escritura y la gestión transaccional como una sola unidad atómica.
public class ServicioTransferenciaStock {
private final RepositorioPedidos repoPedidos;
private final RepositorioStock repoStock;
@Transactional // delimita la Unit of Work: todo o nada
public void confirmarPedido(PedidoId id) {
Pedido pedido = repoPedidos.obtener(id)
.orElseThrow(() -> new PedidoNoEncontradoException(id));
pedido.confirmar(); // cambia el estado en memoria
repoStock.descontar(pedido.lineas()); // descuenta stock en memoria
repoPedidos.guardar(pedido); // marca para persistir
// Al salir del método, @Transactional hace COMMIT de TODO junto;
// si algo lanza excepción, hace ROLLBACK de TODO.
}
}Cómo funciona:
- La anotación
@Transactionalde Spring implementa el Unit of Work: abre una transacción al entrar y la confirma (COMMIT) al salir sin error, o la deshace (ROLLBACK) si se lanza una excepción. - Las modificaciones a
pedidoy astockse acumulan y se aplican atómicamente. No es posible confirmar el pedido sin descontar el stock. - En JPA/Hibernate, el
EntityManageres en sí mismo una Unit of Work: rastrea los cambios de las entidades cargadas (dirty checking) y los vuelca en elflush.
El Unit of Work aporta dos beneficios: atomicidad (la garantía A de ACID a nivel de aplicación) y eficiencia (agrupa escrituras en un solo viaje a la base en vez de muchos).
- Data Mapper vs Active Record
Estos dos patrones describen cómo se relaciona un objeto con su representación en la base.
Active Record: el objeto de dominio contiene su propia lógica de persistencia. La fila y el objeto son la misma cosa.
// Estilo Active Record (típico de frameworks como Ruby on Rails o algunos en Java)
Pedido pedido = Pedido.buscarPorId(42);
pedido.setEstado("CONFIRMADO");
pedido.guardar(); // el propio objeto sabe escribirse en la baseVentaja: rápido y directo para CRUD sencillo. Inconveniente: mezcla negocio y persistencia en la misma clase, dificultando las pruebas y violando la separación de responsabilidades.
Data Mapper: una capa separada (el "mapeador") traslada entre los objetos de dominio y la base. El objeto de dominio no sabe nada de la persistencia.
// Estilo Data Mapper: el objeto es "ignorante" de la base Pedido pedido = new Pedido(/* datos puros de negocio */); pedido.confirmar(); // solo lógica de negocio, sin guardar() entityManager.persist(pedido); // el mapper (JPA) se encarga de escribir
Hibernate/JPA son implementaciones de Data Mapper. Comparativa:
| Criterio | Active Record | Data Mapper |
|---|---|---|
| Acoplamiento dominio-BD | Alto (en la misma clase) | Bajo (separados) |
| Curva de aprendizaje | Baja | Media |
| Testabilidad del dominio | Limitada | Alta (dominio puro) |
| Idóneo para | CRUD simple, prototipos | Dominios complejos, DDD |
Errores Comunes y Consejos
- Filtrar excepciones de infraestructura hacia el dominio. Si una
SQLExceptiono unaJpaExceptionsube hasta el servicio de negocio, has roto el aislamiento. Tradúcelas en el DAO/Repository. - Repository "gordo" con cientos de métodos. Si un repositorio crece sin control, probablemente el agregado está mal definido. Un repositorio por agregado raíz.
- Devolver entidades JPA gestionadas fuera de la transacción. Provoca el temido
LazyInitializationException. Devuelve DTO o carga lo necesario dentro de la transacción. - Poner
@Transactionalen métodos privados o llamadas internas de la misma clase. Spring usa proxies; la transacción no se activa en autollamadas. Colócala en el punto de entrada público. - Consejo: no abstraigas por abstraer. En aplicaciones pequeñas, Spring Data JPA ya te da Repository + Unit of Work sin escribir DAO a mano. Añade capas solo cuando aporten valor.
Ejercicios
Ejercicio 1. Explica con tus palabras por qué un Repository devuelve Optional<Pedido> en lugar de null, y qué riesgo evita.
Ejercicio 2. Tienes que registrar un nuevo cliente y crear su primer pedido en una sola operación que debe ser atómica. Esboza el método de servicio indicando dónde colocarías la frontera transaccional (Unit of Work).
Ejercicio 3. Clasifica cada situación como Active Record o Data Mapper: (a) la clase Factura tiene un método guardar(); (b) un EntityManager persiste un objeto Factura que solo contiene reglas de negocio.
Soluciones
Solución 1. Optional<Pedido> expresa de forma explícita en el tipo que el pedido puede no existir. Obliga a quien llama a manejar la ausencia (con orElseThrow, map, etc.) y evita los NullPointerException que surgen al usar un null no comprobado. La intención de "puede que no haya resultado" queda documentada en la firma del método.
Solución 2.
@Transactional // frontera del Unit of Work: ambos guardados se confirman juntos
public void altaClienteConPrimerPedido(DatosCliente datos, DatosPedido linea) {
Cliente cliente = new Cliente(datos.nombre(), datos.email());
repoClientes.guardar(cliente);
Pedido pedido = cliente.crearPedido(linea);
repoPedidos.guardar(pedido);
// COMMIT al salir; si falla el segundo guardado, ROLLBACK del cliente también.
}La frontera transaccional se coloca en el método de servicio (@Transactional), de modo que la creación del cliente y la del pedido formen una única unidad atómica.
Solución 3. (a) Active Record: el objeto Factura contiene su propia lógica de persistencia (guardar()). (b) Data Mapper: la persistencia vive fuera (en el EntityManager) y la Factura solo tiene reglas de negocio.
Conclusión
Has visto cómo aislar la lógica de negocio de los detalles de persistencia mediante cuatro patrones complementarios: el DAO centraliza el SQL por fuente de datos, el Repository ofrece al dominio una colección de objetos en su propio lenguaje, el Unit of Work garantiza la atomicidad de operaciones que tocan varios objetos, y la dicotomía Data Mapper vs Active Record decide cuánto sabe el objeto de dominio sobre su propia persistencia. Frameworks como Spring Data JPA combinan varios de estos patrones por ti. Hasta ahora hemos supuesto una única base de datos; pero en arquitecturas de microservicios cada servicio tiene la suya, lo que abre retos nuevos de consultas y transacciones repartidas. Eso es lo que abordaremos en la siguiente lección: Base de Datos por Servicio y Gestión de Datos Distribuidos.
Curso de Arquitectura de Aplicaciones
Módulo 1: Fundamentos de la Arquitectura de Aplicaciones
- ¿Qué es la Arquitectura de Aplicaciones?
- El Rol del Arquitecto de Software
- Atributos de Calidad y Requisitos No Funcionales
- Decisiones Arquitectónicas y Compromisos (Trade-offs)
- Documentación de Arquitectura: Vistas y el Modelo C4
Módulo 2: Principios y Tácticas de Diseño
- Acoplamiento, Cohesión y Separación de Responsabilidades
- Principios SOLID Aplicados a la Arquitectura
- DRY, KISS, YAGNI y Otros Principios de Diseño
- Tácticas Arquitectónicas para los Atributos de Calidad
- Gestión de la Deuda Técnica
Módulo 3: Estilos y Patrones Arquitectónicos
- Arquitectura Monolítica
- Arquitectura en Capas (N-Tier)
- Arquitectura Cliente-Servidor
- Arquitectura Hexagonal (Puertos y Adaptadores)
- Arquitectura Limpia y Cebolla (Clean & Onion)
Módulo 4: Arquitecturas Distribuidas y Microservicios
- Introducción a los Sistemas Distribuidos
- Arquitectura de Microservicios
- Descomposición de Servicios y Bounded Contexts
- API Gateway, Service Discovery y Comunicación entre Servicios
- Patrones de Resiliencia: Circuit Breaker, Retry y Bulkhead
- El Teorema CAP y la Consistencia de Datos
Módulo 5: Arquitecturas Dirigidas por Eventos y Mensajería
- Fundamentos de la Arquitectura Orientada a Eventos
- Mensajería Asíncrona: Colas y Brokers
- Patrones de Eventos: Event Sourcing y CQRS
- Gestión de Transacciones Distribuidas: Patrón Saga
- Streaming de Datos en Tiempo Real
Módulo 6: Diseño Dirigido por el Dominio (DDD)
- Conceptos Fundamentales del DDD
- Diseño Estratégico: Bounded Contexts y Lenguaje Ubicuo
- Diseño Táctico: Entidades, Agregados y Repositorios
- Mapeo de Contextos (Context Mapping)
Módulo 7: Datos y Persistencia
- Estrategias de Persistencia: SQL vs NoSQL
- Patrones de Acceso a Datos: Repository, Unit of Work y DAO
- Base de Datos por Servicio y Gestión de Datos Distribuidos
- Caché y Estrategias de Invalidación
Módulo 8: Arquitectura en la Nube y Despliegue
- Fundamentos del Cloud Computing (IaaS, PaaS, SaaS)
- Contenedores y Orquestación con Docker y Kubernetes
- Arquitectura Serverless
- Patrones de Diseño Cloud-Native
- Infraestructura como Código (IaC)
Módulo 9: Calidad, Seguridad y Observabilidad
- Escalabilidad: Horizontal vs Vertical y Balanceo de Carga
- Alta Disponibilidad y Tolerancia a Fallos
- Seguridad por Diseño y Autenticación/Autorización
- Observabilidad: Logging, Métricas y Trazabilidad
- Rendimiento y Pruebas de Carga
