Apuntes DAM

Patrón Repository

Una interfaz que se usa como una colección de objetos del dominio (guardar, buscar, consultas del negocio) y esconde dónde y cómo se guardan. Es lo que dan Spring Data, Room o Entity Framework.

nivel intermedioTambién: repositorio, Spring Data, JpaRepository

Visualízalo paso a paso

Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.

Repository

Escribe las consultas al repositorio separadas por comas: pendientes cliente, caros importe o enviar id (que guarda el pedido cambiado).

Paso 1

El servicio pregunta en términos del negocio: pendientesDe("Ana"). No hay ni SQL ni tablas a la vista.

1default List<Pedido> pendientesDe(String cliente) {  // consulta = pendientesDe("Ana")
2    return buscar(p -> p.cliente().equals(cliente)
3                    && p.estado() == Estado.PENDIENTE);
4}
5
6public List<Pedido> buscar(Predicate<Pedido> criterio) {
7    List<Pedido> r = new ArrayList<>();
8    for (Pedido p : pedidos.values())
9        if (criterio.test(p)) r.add(p);
10    return r;
11}
12
13public void guardar(Pedido p) { pedidos.put(p.id(), p); }

Variables

consulta
pendientesDe("Ana")

Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.

La idea

El DAO piensa en tablas: una operación por cada cosa que se hace con las filas. El repositorio piensa en el dominio: se comporta como una colección de pedidos o de reservas a la que se le añaden objetos y se le piden los que cumplen algo que tiene sentido en el negocio, como «los pedidos pendientes de Ana» o «las habitaciones ocupadas esta noche».

Un repositorio suele tener una parte genérica, igual para todas las entidades (guardar, porId, buscar con un criterio), y una parte propia de cada una, con consultas que se llaman como las nombraría un usuario (pendientesDe(cliente), masCarosQue(importe)). El servicio usa esas consultas y no sabe si debajo hay SQL, JPA, un fichero o una lista en memoria.

En los frameworks modernos el repositorio ya viene hecho: en Spring Data JPA basta con declarar la interfaz PedidoRepository extends JpaRepository<Pedido, Long> y Spring escribe la implementación, incluidas las consultas que se deducen del nombre del método (findByClienteAndEstado). Room en Android y Entity Framework en .NET siguen la misma idea.

Cuándo usarlo

  • Aplicaciones con un dominio rico (pedidos, reservas, matrículas) donde las consultas tienen significado de negocio.
  • Proyectos con un ORM (JPA, Hibernate, Room, Entity Framework, Doctrine), que ya trabajan con objetos y no con filas.
  • Quieres probar los servicios con un repositorio en memoria.

Cuándo no

  • Si la aplicación es poco más que un CRUD de tablas: un DAO o el acceso directo del framework es más simple.
  • Si las consultas son informes complejos con muchas tablas: mejor una consulta específica que forzarlas a parecer una colección.

Participantes

  1. Entidad. El objeto del dominio con su identidad (Pedido con su id).
  2. Repositorio genérico. Las operaciones de colección comunes a todas las entidades (Repositorio<T, ID>).
  3. Repositorio de la entidad. Añade las consultas del negocio (RepositorioPedidos.pendientesDe).
  4. Implementación. En memoria, con JDBC o generada por el framework (RepositorioPedidosMemoria, Spring Data).
  5. Servicio. Usa el repositorio para aplicar las reglas del negocio.

Diagrama de clases

0..*pedidos1repo«interface»Repositorio+guardar(entidad : T) : void+porId(id : ID) : Optional<T>+buscar(criterio : Predicate<T>) : List<T>«interface»RepositorioPedidos+pendientesDe(cliente : String) : List<Pedido>+masCarosQue(importe : double) : List<Pedido>RepositorioPedidosMemoria-pedidos : Map<Integer, Pedido>Pedido+id : int+cliente : String+total : double+estado : EstadoServicioPedidos
Arrastra las clases para colocarlas a tu gusto.
Ver el diagrama en PlantUML
text
1@startuml
2interface Repositorio {
3  +guardar(entidad : T) : void
4  +porId(id : ID) : Optional<T>
5  +buscar(criterio : Predicate<T>) : List<T>
6}
7interface RepositorioPedidos {
8  +pendientesDe(cliente : String) : List<Pedido>
9  +masCarosQue(importe : double) : List<Pedido>
10}
11class RepositorioPedidosMemoria {
12  -pedidos : Map<Integer, Pedido>
13}
14class Pedido {
15  +id : int
16  +cliente : String
17  +total : double
18  +estado : Estado
19}
20class ServicioPedidos
21Repositorio <|-- RepositorioPedidos
22RepositorioPedidos <|.. RepositorioPedidosMemoria
23RepositorioPedidosMemoria o-- "0..*" Pedido : pedidos
24ServicioPedidos --> "1" RepositorioPedidos : repo
25@enduml

Puedes copiarlo en el editor de diagramas UML y modificarlo.

El código

Un repositorio de pedidos

Las consultas del negocio son métodos por defecto de la interfaz que se apoyan en buscar: cualquier implementación las tiene gratis.

Java
1import java.util.*;
2import java.util.function.Predicate;
3
4enum Estado { PENDIENTE, ENVIADO, ENTREGADO }
5
6record Pedido(int id, String cliente, double total, Estado estado) {}
7
8/** Un repositorio genérico: se usa como una colección de objetos del dominio. */
9interface Repositorio<T, ID> {
10    void guardar(T entidad);                      // inserta o, si ya existe, sustituye
11    Optional<T> porId(ID id);
12    List<T> buscar(Predicate<T> criterio);
13}
14
15/** El de pedidos añade consultas con nombre del negocio, no de tablas ni de SQL. */
16interface RepositorioPedidos extends Repositorio<Pedido, Integer> {
17    default List<Pedido> pendientesDe(String cliente) {
18        return buscar(p -> p.cliente().equals(cliente) && p.estado() == Estado.PENDIENTE);
19    }
20
21    default List<Pedido> masCarosQue(double importe) {
22        return buscar(p -> p.total() > importe);
23    }
24}
25
26/** En memoria para el ejemplo; con JPA, Spring Data escribiría esta clase por ti. */
27class RepositorioPedidosMemoria implements RepositorioPedidos {
28    private final Map<Integer, Pedido> pedidos = new TreeMap<>();
29
30    public void guardar(Pedido p) { pedidos.put(p.id(), p); }
31    public Optional<Pedido> porId(Integer id) { return Optional.ofNullable(pedidos.get(id)); }
32
33    public List<Pedido> buscar(Predicate<Pedido> criterio) {
34        List<Pedido> r = new ArrayList<>();
35        for (Pedido p : pedidos.values()) if (criterio.test(p)) r.add(p);
36        return r;
37    }
38}
39
40public class Main {
41    static String ids(List<Pedido> pedidos) {
42        List<String> r = new ArrayList<>();
43        for (Pedido p : pedidos) r.add("#" + p.id());
44        return r.isEmpty() ? "ninguno" : String.join(", ", r);
45    }
46
47    public static void main(String[] args) {
48        RepositorioPedidos repo = new RepositorioPedidosMemoria();
49        repo.guardar(new Pedido(1, "Ana", 45.0, Estado.PENDIENTE));
50        repo.guardar(new Pedido(2, "Luis", 120.5, Estado.ENVIADO));
51        repo.guardar(new Pedido(3, "Ana", 230.0, Estado.PENDIENTE));
52        repo.guardar(new Pedido(4, "Ana", 15.0, Estado.ENTREGADO));
53        System.out.println("Pendientes de Ana: " + ids(repo.pendientesDe("Ana")));
54        System.out.println("Más de 100 €: " + ids(repo.masCarosQue(100)));
55        repo.guardar(new Pedido(1, "Ana", 45.0, Estado.ENVIADO));          // mismo id: se actualiza
56        System.out.println("Pendientes de Ana tras enviar el #1: " + ids(repo.pendientesDe("Ana")));
57        System.out.println("Pedido #2: " + repo.porId(2).map(p -> p.cliente() + ", " + p.estado()).orElse("no existe"));
58    }
59}

Salida al ejecutarlo (la misma en los 5 lenguajes)

Pendientes de Ana: #1, #3
Más de 100 €: #2, #3
Pendientes de Ana tras enviar el #1: #3
Pedido #2: Luis, ENVIADO

Spring Data JPA

Se declara la interfaz y Spring escribe la clase, incluidas las consultas que deduce del nombre de cada método.

Java
1// Con Spring Data JPA se declara la interfaz y Spring escribe la implementación:
2// las consultas salen del nombre de los métodos.
3public interface PedidoRepository extends JpaRepository<Pedido, Long> {
4    List<Pedido> findByClienteAndEstado(String cliente, Estado estado);
5    List<Pedido> findByTotalGreaterThanOrderByTotalDesc(BigDecimal importe);
6
7    @Query("SELECT p FROM Pedido p WHERE p.fecha >= :desde")
8    List<Pedido> recientes(@Param("desde") LocalDate desde);
9}
10
11// Y el servicio lo recibe ya hecho:
12@Service
13public class PedidoService {
14    private final PedidoRepository repo;
15    public PedidoService(PedidoRepository repo) { this.repo = repo; }
16
17    public List<Pedido> pendientes(String cliente) {
18        return repo.findByClienteAndEstado(cliente, Estado.PENDIENTE);
19    }
20}

En la práctica

  • Spring Data JPA (JpaRepository, CrudRepository) y Spring Data MongoDB.
  • Room en Android (con @Dao) y los repositorios de la arquitectura recomendada por Google, que eligen entre la red y la base de datos local.
  • Entity Framework (DbSet<T> hace de repositorio), Doctrine en Symfony y los repositorios de Laravel.
  • Los repositorios en memoria para probar los servicios sin base de datos.

Errores típicos

  • Un repositorio que devuelve filas, ResultSet o arrays sueltos en lugar de objetos del dominio.
  • Un método de consulta por cada combinación posible de filtros: mejor un buscar con criterio o especificaciones.
  • Poner reglas del negocio en el repositorio (no se puede reservar una habitación ocupada): eso es del servicio, que usa las consultas del repositorio.
  • Confundirlo con el DAO hasta el punto de tener los dos haciendo lo mismo: en una aplicación con ORM, normalmente basta con el repositorio.

Ejercicios

Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.

1. Un repositorio genérico

Escribe RepositorioMemoria<T extends Entidad>, un repositorio en memoria que sirve para cualquier entidad con id. El main lo usa con libros y le pide consultas con criterios distintos sin que el repositorio sepa nada de libros.

  • Órdenes: libro id;título;autor;año (escribe Nuevo: título o Actualizado: título si el id ya existía), ver id, autor texto, antes año, quitar id y todos.
  • Las listas salen ordenadas por id. buscar devuelve las entidades que cumplen el Predicate que recibe.
JavaUn repositorio genéricoFácil

Ejemplo

Entrada (lo que se escribe por teclado)
libro 3;Fortunata y Jacinta;Benito Pérez Galdós;1887
libro 1;La Regenta;Leopoldo Alas;1884
libro 2;Niebla;Miguel de Unamuno;1914
autor galdós
antes 1900
todos
Salida esperada
Nuevo: Fortunata y Jacinta
Nuevo: La Regenta
Nuevo: Niebla
De galdós:
  3. Fortunata y Jacinta — Benito Pérez Galdós (1887)
Anteriores a 1900:
  1. La Regenta — Leopoldo Alas (1884)
  3. Fortunata y Jacinta — Benito Pérez Galdós (1887)
Todos:
  1. La Regenta — Leopoldo Alas (1884)
  2. Niebla — Miguel de Unamuno (1914)
  3. Fortunata y Jacinta — Benito Pérez Galdós (1887)
Test oculto #3
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
1import java.util.*;
2import java.util.function.Predicate;
3
4/** Lo que tiene cualquier objeto que se guarda en un repositorio: un id. */
5interface Entidad {
6    int id();
7}
8
9record Libro(int id, String titulo, String autor, int anio) implements Entidad {}
10
11/** Un repositorio en memoria que vale para cualquier entidad. */
12class RepositorioMemoria<T extends Entidad> {
13    private final Map<Integer, T> datos = new TreeMap<>();
14
15    /** Inserta o, si ya hay una con ese id, la sustituye. Devuelve true si era nueva. */
16    boolean guardar(T e) { return datos.put(e.id(), e) == null; }
17
18    Optional<T> porId(int id) { return Optional.ofNullable(datos.get(id)); }
19
20    /** Todas, ordenadas por id. */
21    List<T> todos() { return new ArrayList<>(datos.values()); }
22
23    boolean borrar(int id) { return datos.remove(id) != null; }
24
25    /** Las que cumplen el criterio, ordenadas por id. */
26    List<T> buscar(Predicate<T> criterio) {
27        List<T> r = new ArrayList<>();
28        for (T e : datos.values()) if (criterio.test(e)) r.add(e);
29        return r;
30    }
31}
32
33public class Main {
34    static void escribir(List<Libro> libros) {
35        if (libros.isEmpty()) System.out.println("  (ninguno)");
36        for (Libro l : libros) System.out.println("  " + l.id() + ". " + l.titulo() + " — " + l.autor() + " (" + l.anio() + ")");
37    }
38
39    public static void main(String[] args) {
40        RepositorioMemoria<Libro> repo = new RepositorioMemoria<>();
41        Scanner sc = new Scanner(System.in);
42        while (sc.hasNextLine()) {
43            String linea = sc.nextLine().trim();
44            if (linea.isEmpty()) continue;
45            String[] p = linea.split("\\s+", 2);
46            String arg = p.length > 1 ? p[1].trim() : "";
47            try {
48                switch (p[0]) {
49                    case "libro" -> {
50                        String[] c = arg.split(";");
51                        if (c.length != 4) throw new NumberFormatException();
52                        Libro l = new Libro(Integer.parseInt(c[0].trim()), c[1].trim(), c[2].trim(), Integer.parseInt(c[3].trim()));
53                        System.out.println((repo.guardar(l) ? "Nuevo: " : "Actualizado: ") + l.titulo());
54                    }
55                    case "ver" -> System.out.println(repo.porId(Integer.parseInt(arg)).map(l -> l.titulo() + " — " + l.autor()).orElse("No existe el libro " + arg));
56                    case "autor" -> {
57                        System.out.println("De " + arg + ":");
58                        escribir(repo.buscar(l -> l.autor().toLowerCase().contains(arg.toLowerCase())));
59                    }
60                    case "antes" -> {
61                        int anio = Integer.parseInt(arg);
62                        System.out.println("Anteriores a " + anio + ":");
63                        escribir(repo.buscar(l -> l.anio() < anio));
64                    }
65                    case "quitar" -> System.out.println(repo.borrar(Integer.parseInt(arg)) ? "Quitado" : "No existe el libro " + arg);
66                    case "todos" -> {
67                        System.out.println("Todos:");
68                        escribir(repo.todos());
69                    }
70                    default -> System.out.println("Orden no válida: " + linea);
71                }
72            } catch (NumberFormatException e) {
73                System.out.println("Orden no válida: " + linea);
74            }
75        }
76    }
77}

Con genéricos, la misma clase sirve para libros, socios o préstamos: es lo que hace JpaRepository<T, ID>.

Pasar el criterio como un Predicate evita escribir un método por cada consulta.

2. Las reservas de un hotel

La recepción del hotel usa el repositorio de reservas para no dar dos veces la misma habitación. Escribe las tres consultas del negocio de RepositorioReservas como métodos por defecto que se apoyan en buscar: solapadas, ocupadas y delCliente.

  • Órdenes: reservar habitación cliente entrada salida (fechas AAAA-MM-DD), cancelar id, ocupadas fecha y cliente nombre. La recepción ya escribe los mensajes.
  • Una reserva ocupa las noches de entrada a salida, sin incluir la de salida: dos reservas se solapan si una empieza antes de que termine la otra y al revés (a.entrada < b.salida y b.entrada < a.salida). Así, entrar el día que otro sale no es un choque.
  • ocupadas devuelve los números de habitación sin repetir y de menor a mayor; delCliente, las reservas ordenadas por entrada (y por id si empatan).
JavaLas reservas de un hotelMedio

Ejemplo

Entrada (lo que se escribe por teclado)
reservar 101 Ana 2026-10-10 2026-10-13
reservar 101 Luis 2026-10-12 2026-10-14
reservar 101 Luis 2026-10-13 2026-10-15
reservar 102 Marta 2026-10-11 2026-10-12
ocupadas 2026-10-11
ocupadas 2026-10-13
cliente luis
Salida esperada
Reserva #1: Ana en la 101 del 2026-10-10 al 2026-10-13
La 101 está ocupada (reserva #1)
Reserva #2: Luis en la 101 del 2026-10-13 al 2026-10-15
Reserva #3: Marta en la 102 del 2026-10-11 al 2026-10-12
Ocupadas el 2026-10-11: 101, 102
Ocupadas el 2026-10-13: 101
Reservas de luis:
  #2 habitación 101: 2026-10-13 → 2026-10-15
Test oculto #3
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
1import java.time.LocalDate;
2import java.util.*;
3import java.util.function.Predicate;
4
5/** Una reserva: de la noche de entrada a la de salida, sin incluir la salida. */
6record Reserva(int id, int habitacion, String cliente, LocalDate entrada, LocalDate salida) {}
7
8interface RepositorioReservas {
9    void guardar(Reserva r);
10    boolean borrar(int id);
11    List<Reserva> buscar(Predicate<Reserva> criterio);
12
13    /** Las reservas de esa habitación que se cruzan con [desde, hasta). */
14    default List<Reserva> solapadas(int habitacion, LocalDate desde, LocalDate hasta) {
15        return buscar(r -> r.habitacion() == habitacion && r.entrada().isBefore(hasta) && desde.isBefore(r.salida()));
16    }
17
18    /** Las habitaciones ocupadas la noche de esa fecha, de menor a mayor. */
19    default List<Integer> ocupadas(LocalDate fecha) {
20        TreeSet<Integer> h = new TreeSet<>();
21        for (Reserva r : buscar(r -> !fecha.isBefore(r.entrada()) && fecha.isBefore(r.salida()))) h.add(r.habitacion());
22        return new ArrayList<>(h);
23    }
24
25    /** Las reservas de un cliente (sin distinguir mayúsculas), por fecha de entrada. */
26    default List<Reserva> delCliente(String cliente) {
27        List<Reserva> r = buscar(x -> x.cliente().equalsIgnoreCase(cliente));
28        r.sort(Comparator.comparing(Reserva::entrada).thenComparingInt(Reserva::id));
29        return r;
30    }
31}
32
33class RepositorioReservasMemoria implements RepositorioReservas {
34    private final Map<Integer, Reserva> datos = new TreeMap<>();
35    public void guardar(Reserva r) { datos.put(r.id(), r); }
36    public boolean borrar(int id) { return datos.remove(id) != null; }
37    public List<Reserva> buscar(Predicate<Reserva> criterio) {
38        List<Reserva> r = new ArrayList<>();
39        for (Reserva x : datos.values()) if (criterio.test(x)) r.add(x);
40        return r;
41    }
42}
43
44/** El servicio usa las consultas del repositorio para aplicar las reglas del hotel. */
45class Recepcion {
46    private final RepositorioReservas repo;
47    private int siguiente = 1;
48    Recepcion(RepositorioReservas repo) { this.repo = repo; }
49
50    String reservar(int habitacion, String cliente, LocalDate entrada, LocalDate salida) {
51        if (!entrada.isBefore(salida)) return "Fechas no válidas";
52        List<Reserva> choques = repo.solapadas(habitacion, entrada, salida);
53        if (!choques.isEmpty()) return "La " + habitacion + " está ocupada (reserva #" + choques.get(0).id() + ")";
54        Reserva r = new Reserva(siguiente++, habitacion, cliente, entrada, salida);
55        repo.guardar(r);
56        return "Reserva #" + r.id() + ": " + cliente + " en la " + habitacion + " del " + entrada + " al " + salida;
57    }
58}
59
60public class Main {
61    public static void main(String[] args) {
62        RepositorioReservas repo = new RepositorioReservasMemoria();
63        Recepcion recepcion = new Recepcion(repo);
64        Scanner sc = new Scanner(System.in);
65        while (sc.hasNextLine()) {
66            String linea = sc.nextLine().trim();
67            if (linea.isEmpty()) continue;
68            String[] p = linea.split("\\s+");
69            try {
70                switch (p[0] + "/" + (p.length - 1)) {
71                    case "reservar/4" -> System.out.println(recepcion.reservar(Integer.parseInt(p[1]), p[2], LocalDate.parse(p[3]), LocalDate.parse(p[4])));
72                    case "cancelar/1" -> System.out.println(repo.borrar(Integer.parseInt(p[1])) ? "Cancelada la #" + p[1] : "No existe la reserva #" + p[1]);
73                    case "ocupadas/1" -> {
74                        List<Integer> h = repo.ocupadas(LocalDate.parse(p[1]));
75                        System.out.println("Ocupadas el " + p[1] + ": " + (h.isEmpty() ? "ninguna" : h.toString().replaceAll("[\\[\\]]", "")));
76                    }
77                    case "cliente/1" -> {
78                        List<Reserva> r = repo.delCliente(p[1]);
79                        System.out.println("Reservas de " + p[1] + ":" + (r.isEmpty() ? " ninguna" : ""));
80                        for (Reserva x : r) System.out.println("  #" + x.id() + " habitación " + x.habitacion() + ": " + x.entrada() + " → " + x.salida());
81                    }
82                    default -> System.out.println("Orden no válida: " + linea);
83                }
84            } catch (RuntimeException e) {
85                System.out.println("Orden no válida: " + linea);
86            }
87        }
88    }
89}

La regla («no se puede reservar una habitación ocupada») está en el servicio; el repositorio solo responde preguntas del dominio («¿qué reservas se cruzan con estas fechas?»).

Los intervalos cerrados por la izquierda y abiertos por la derecha hacen que el día de salida de uno sea el de entrada de otro sin choques.

Test

Test: Repository

0/5 respondidas · 0 aciertos

Elige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.

  1. 1.¿En qué se diferencia un repositorio de un DAO?

  2. 2.¿Qué hace Spring Data JPA con una interfaz que extiende JpaRepository?

  3. 3.¿Dónde debería estar la regla «no se puede reservar una habitación ocupada»?

  4. 4.¿Qué ventaja tiene un método buscar(Predicate<T>)?

  5. 5.¿Qué devuelve un repositorio?

Relacionado