Un cop decidida la tecnologia de persistència, sorgeix un problema de disseny recurrent: com connectem la lògica de negoci amb la base de dades sense que el codi s'ompli de sentències SQL escampades i dependències del motor concret? Si cada part de l'aplicació parla directament amb la base, acabem amb un acoblament brutal, codi impossible de provar i regles de negoci barrejades amb detalls d'infraestructura. Els patrons d'accés a dades resolen exactament això: introdueixen una capa que aïlla com es desen les dades del què s'hi fa. En aquesta lliçó estudiarem els quatre patrons més importants (DAO, Repository, Unit of Work) i la dicotomia Data Mapper enfront d'Active Record, amb exemples en Java perquè vegis les diferències en codi real.
Contingut
- El problema: acoblament a la persistència
- El patró DAO (Data Access Object)
- El patró Repository
- DAO vs Repository: en què es diferencien?
- El patró Unit of Work
- Data Mapper vs Active Record
- Com encaixen tots junts
- El problema: acoblament a la persistència
Imagina un servei que barreja lògica de negoci amb accés directe 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 negoci i SQL barrejats
con.close();
}
}Això té problemes greus: el servei depèn de JDBC i de PostgreSQL, és impossible de provar sense una base real, i la sentència SQL es repetirà a cada lloc que necessiti tocar comandes. Els patrons següents separen responsabilitats per evitar-ho.
- El patró DAO (Data Access Object)
Un DAO encapsula tot l'accés a una font de dades concreta (una taula, normalment) i exposa operacions orientades a la persistència. El seu vocabulari és el de la base de dades: inserir, actualitzar, esborrar, cercar per clau.
Primer definim la interfície, que oculta els detalls d'implementació:
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ètode reflecteix una operació contra la taula. Ara una implementació amb JDBC:
public class PedidoDaoJdbc implements PedidoDao {
private final DataSource dataSource; // pool de connexions injectat
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); // substitueix el ? per l'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);
}
}
// ... resta de mètodes
}Punts clau d'aquest codi:
DataSourceinjectat: el DAO no crea la connexió, la rep. Això permet canviar l'origen de dades (inclòs un de simulat en proves).try-with-resources: tanca automàticamentConnection,PreparedStatementiResultSetencara que hi hagi excepció, evitant fuites de recursos.PreparedStatementamb?: prevé la injecció SQL i permet reutilitzar el pla d'execució.- Traducció d'excepcions: converteix la
SQLException(de baix nivell) en una excepció pròpia, perquè la resta de l'aplicació no depengui de JDBC.
El DAO centralitza tot el SQL de la taula pedidos en un únic lloc.
- El patró Repository
Un Repository opera a un nivell d'abstracció més alt: simula ser una col·lecció en memòria d'objectes de domini. Forma part del vocabulari del Disseny Dirigit pel Domini (vist al Mòdul 6) i s'associa a un agregat, no a una taula. La seva intenció és que el codi de negoci cregui estar treballant amb una llista d'objectes, aliè al fet que al darrere hi ha una base de dades.
public interface RepositorioPedidos {
void guardar(Pedido pedido); // "afegir a la col·lecció"
Optional<Pedido> obtener(PedidoId id);
List<Pedido> pendientesDe(ClienteId cliente); // consulta amb llenguatge del domini
}Diferències de matís respecte al DAO:
- Fa servir identificadors del domini (
PedidoId,ClienteId), nolongcrus. - Retorna
Optionalen lloc denull, expressant explícitament l'absència. - Els mètodes parlen el llenguatge ubic del negoci (
pendientesDe), no el de SQL. - Un únic mètode
guardardecideix internament si cal inserir o actualitzar; qui el crida no ho sap ni li importa.
Una implementació amb Spring Data JPA redueix el codi a gairebé res:
public interface RepositorioPedidos extends JpaRepository<Pedido, Long> {
// Spring genera la consulta a partir del nom del mètode
List<Pedido> findByClienteIdAndEstado(Long clienteId, String estado);
}Aquí JpaRepository ja aporta save, findById, etc., i Spring deriva la consulta del nom findByClienteIdAndEstado. La intenció de negoci queda al nom, sense SQL manual.
- DAO vs Repository: en què es diferencien?
És la confusió més habitual. Tots dos abstreuen l'accés a dades, però la seva intenció i nivell difereixen:
| Aspecte | DAO | Repository |
|---|---|---|
| Orientació | A la taula / font de dades | A l'agregat del domini |
| Vocabulari | Persistència (inserir, actualitzar) | Negoci (col·lecció d'objectes) |
| Granularitat | Un per taula, normalment | Un per agregat arrel |
| Procedència | Patró de capa de dades clàssic | Patró tàctic de DDD |
| Coneix SQL | Sí, l'exposa conceptualment | No, l'oculta rere la "col·lecció" |
A la pràctica, un Repository sol recolzar-se en un o diversos DAO o en un ORM. No són excloents: el Repository és la cara que veu el domini, el DAO és la maquinària interna.
- El patró Unit of Work
Què passa quan una operació de negoci modifica diversos objectes i tots han de confirmar-se junts? Si cridem guardar un per un, podríem confirmar-ne la meitat i fallar en l'altra meitat. El Unit of Work resol això: manté una llista d'objectes afectats durant una operació de negoci i coordina l'escriptura i la gestió transaccional com una sola unitat atòmica.
public class ServicioTransferenciaStock {
private final RepositorioPedidos repoPedidos;
private final RepositorioStock repoStock;
@Transactional // delimita el Unit of Work: tot o res
public void confirmarPedido(PedidoId id) {
Pedido pedido = repoPedidos.obtener(id)
.orElseThrow(() -> new PedidoNoEncontradoException(id));
pedido.confirmar(); // canvia l'estat en memòria
repoStock.descontar(pedido.lineas()); // descompta estoc en memòria
repoPedidos.guardar(pedido); // marca per persistir
// En sortir del mètode, @Transactional fa COMMIT de TOT alhora;
// si alguna cosa llança excepció, fa ROLLBACK de TOT.
}
}Com funciona:
- L'anotació
@Transactionalde Spring implementa el Unit of Work: obre una transacció en entrar i la confirma (COMMIT) en sortir sense error, o la desfà (ROLLBACK) si es llança una excepció. - Les modificacions a
pedidoi astocks'acumulen i s'apliquen atòmicament. No és possible confirmar la comanda sense descomptar l'estoc. - En JPA/Hibernate, l'
EntityManagerés en si mateix una Unit of Work: rastreja els canvis de les entitats carregades (dirty checking) i els bolca alflush.
El Unit of Work aporta dos beneficis: atomicitat (la garantia A d'ACID a nivell d'aplicació) i eficiència (agrupa escriptures en un sol viatge a la base en lloc de molts).
- Data Mapper vs Active Record
Aquests dos patrons descriuen com es relaciona un objecte amb la seva representació a la base.
Active Record: l'objecte de domini conté la seva pròpia lògica de persistència. La fila i l'objecte són la mateixa cosa.
// Estil Active Record (típic de frameworks com Ruby on Rails o alguns en Java)
Pedido pedido = Pedido.buscarPorId(42);
pedido.setEstado("CONFIRMADO");
pedido.guardar(); // el mateix objecte sap escriure's a la baseAvantatge: ràpid i directe per a CRUD senzill. Inconvenient: barreja negoci i persistència a la mateixa classe, dificultant les proves i violant la separació de responsabilitats.
Data Mapper: una capa separada (el "mapejador") trasllada entre els objectes de domini i la base. L'objecte de domini no sap res de la persistència.
// Estil Data Mapper: l'objecte és "ignorant" de la base Pedido pedido = new Pedido(/* dades pures de negoci */); pedido.confirmar(); // només lògica de negoci, sense guardar() entityManager.persist(pedido); // el mapper (JPA) s'encarrega d'escriure
Hibernate/JPA són implementacions de Data Mapper. Comparativa:
| Criteri | Active Record | Data Mapper |
|---|---|---|
| Acoblament domini-BD | Alt (a la mateixa classe) | Baix (separats) |
| Corba d'aprenentatge | Baixa | Mitjana |
| Testabilitat del domini | Limitada | Alta (domini pur) |
| Idoni per a | CRUD simple, prototips | Dominis complexos, DDD |
Errors habituals i consells
- Filtrar excepcions d'infraestructura cap al domini. Si una
SQLExceptiono unaJpaExceptionpuja fins al servei de negoci, has trencat l'aïllament. Tradueix-les al DAO/Repository. - Repository "gras" amb centenars de mètodes. Si un repositori creix sense control, probablement l'agregat està mal definit. Un repositori per agregat arrel.
- Retornar entitats JPA gestionades fora de la transacció. Provoca la temuda
LazyInitializationException. Retorna DTO o carrega el necessari dins de la transacció. - Posar
@Transactionalen mètodes privats o crides internes de la mateixa classe. Spring fa servir proxies; la transacció no s'activa en autocrides. Col·loca-la al punt d'entrada públic. - Consell: no abstreguis per abstreure. En aplicacions petites, Spring Data JPA ja et dóna Repository + Unit of Work sense escriure DAO a mà. Afegeix capes només quan aportin valor.
Exercicis
Exercici 1. Explica amb les teves paraules per què un Repository retorna Optional<Pedido> en lloc de null, i quin risc evita.
Exercici 2. Has de registrar un nou client i crear la seva primera comanda en una sola operació que ha de ser atòmica. Esbossa el mètode de servei indicant on col·locaries la frontera transaccional (Unit of Work).
Exercici 3. Classifica cada situació com a Active Record o Data Mapper: (a) la classe Factura té un mètode guardar(); (b) un EntityManager persisteix un objecte Factura que només conté regles de negoci.
Solucions
Solució 1. Optional<Pedido> expressa de manera explícita al tipus que la comanda pot no existir. Obliga qui la crida a gestionar l'absència (amb orElseThrow, map, etc.) i evita els NullPointerException que sorgeixen en fer servir un null no comprovat. La intenció de "potser no hi ha resultat" queda documentada a la signatura del mètode.
Solució 2.
@Transactional // frontera del Unit of Work: tots dos desats es confirmen junts
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 en sortir; si falla el segon desat, ROLLBACK del client també.
}La frontera transaccional es col·loca al mètode de servei (@Transactional), de manera que la creació del client i la de la comanda formin una única unitat atòmica.
Solució 3. (a) Active Record: l'objecte Factura conté la seva pròpia lògica de persistència (guardar()). (b) Data Mapper: la persistència viu fora (a l'EntityManager) i la Factura només té regles de negoci.
Conclusió
Has vist com aïllar la lògica de negoci dels detalls de persistència mitjançant quatre patrons complementaris: el DAO centralitza el SQL per font de dades, el Repository ofereix al domini una col·lecció d'objectes en el seu propi llenguatge, el Unit of Work garanteix l'atomicitat d'operacions que toquen diversos objectes, i la dicotomia Data Mapper vs Active Record decideix quant sap l'objecte de domini sobre la seva pròpia persistència. Frameworks com Spring Data JPA combinen diversos d'aquests patrons per tu. Fins ara hem suposat una única base de dades; però en arquitectures de microserveis cada servei té la seva, cosa que obre reptes nous de consultes i transaccions repartides. Això és el que abordarem a la següent lliçó: Base de dades per servei i gestió de dades distribuïdes.
Curs d'Arquitectura d'Aplicacions
Mòdul 1: Fonaments de l'Arquitectura d'Aplicacions
- Què és l'Arquitectura d'Aplicacions?
- El Rol de l'Arquitecte de Programari
- Atributs de Qualitat i Requisits No Funcionals
- Decisions Arquitectòniques i Compromisos (Trade-offs)
- Documentació d'Arquitectura: Vistes i el Model C4
Mòdul 2: Principis i Tàctiques de Disseny
- Acoblament, Cohesió i Separació de Responsabilitats
- Principis SOLID Aplicats a l'Arquitectura
- DRY, KISS, YAGNI i Altres Principis de Disseny
- Tàctiques Arquitectòniques per als Atributs de Qualitat
- Gestió del Deute Tècnic
Mòdul 3: Estils i Patrons Arquitectònics
- Arquitectura Monolítica
- Arquitectura en Capes (N-Tier)
- Arquitectura Client-Servidor
- Arquitectura Hexagonal (Ports i Adaptadors)
- Arquitectura Neta i Ceba (Clean & Onion)
Mòdul 4: Arquitectures Distribuïdes i Microserveis
- Introducció als Sistemes Distribuïts
- Arquitectura de Microserveis
- Descomposició de Serveis i Bounded Contexts
- API Gateway, Service Discovery i Comunicació entre Serveis
- Patrons de Resiliència: Circuit Breaker, Retry i Bulkhead
- El Teorema CAP i la Consistència de Dades
Mòdul 5: Arquitectures Dirigides per Esdeveniments i Missatgeria
- Fonaments de l'Arquitectura Orientada a Esdeveniments
- Missatgeria Asíncrona: Cues i Brokers
- Patrons d'Esdeveniments: Event Sourcing i CQRS
- Gestió de Transaccions Distribuïdes: Patró Saga
- Streaming de Dades en Temps Real
Mòdul 6: Disseny Dirigit pel Domini (DDD)
- Conceptes Fonamentals del DDD
- Disseny Estratègic: Bounded Contexts i Llenguatge Ubic
- Disseny Tàctic: Entitats, Agregats i Repositoris
- Mapatge de Contextos (Context Mapping)
Mòdul 7: Dades i Persistència
- Estratègies de Persistència: SQL vs NoSQL
- Patrons d'Accés a Dades: Repository, Unit of Work i DAO
- Base de Dades per Servei i Gestió de Dades Distribuïdes
- Cau i Estratègies d'Invalidació
Mòdul 8: Arquitectura al Núvol i Desplegament
- Fonaments del Cloud Computing (IaaS, PaaS, SaaS)
- Contenidors i Orquestració amb Docker i Kubernetes
- Arquitectura Serverless
- Patrons de Disseny Cloud-Native
- Infraestructura com a Codi (IaC)
Mòdul 9: Qualitat, Seguretat i Observabilitat
- Escalabilitat: Horitzontal vs Vertical i Balanceig de Càrrega
- Alta Disponibilitat i Tolerància a Fallades
- Seguretat per Disseny i Autenticació/Autorització
- Observabilitat: Logging, Mètriques i Traçabilitat
- Rendiment i Proves de Càrrega
