Apuntes DAM

Patrón DAO (Data Access Object)

Una interfaz con las operaciones de guardar, buscar, actualizar y borrar de una entidad, y una clase que las implementa con JDBC, PDO o un fichero: el SQL queda en un solo sitio.

nivel intermedioTambién: objeto de acceso a datos, data access object, capa de persistencia

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.

DAO

Escribe las operaciones de la secretaría separadas por comas: matricular id nombre ciclo, o listar ciclo.

Paso 1

La secretaría matricula a Ana: crea un objeto Alumno y llama a dao.insertar. No escribe SQL ni sabe qué base de datos hay.

1dao.insertar(new Alumno(id, nombre, ciclo));         // Secretaria  // filas en la tabla = 0
2
3public void insertar(Alumno a) {                     // AlumnoDaoJdbc
4    try (PreparedStatement ps = con.prepareStatement("INSERT INTO alumno VALUES (?, ?, ?)")) {
5        ps.setInt(1, a.id()); ps.setString(2, a.nombre()); ps.setString(3, a.ciclo());
6        ps.executeUpdate();
7    } catch (SQLIntegrityConstraintViolationException e) {
8        throw new IllegalStateException("ya existe el alumno " + a.id());
9    }
10}
11
12public List<Alumno> listarPorCiclo(String ciclo) {
13    ResultSet rs = ps.executeQuery();
14    while (rs.next()) r.add(new Alumno(rs.getInt("id"), rs.getString("nombre"), rs.getString("ciclo")));
15    return r;
16}

Variables

filas en la tabla
0

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

La idea

Si cada pantalla o cada servicio abre su conexión y escribe su SELECT, el SQL acaba repartido por toda la aplicación: cambiar el nombre de una columna obliga a buscarla en veinte sitios, y es imposible probar la lógica sin una base de datos de verdad.

El patrón DAO reúne todo el acceso a los datos de una entidad en una interfaz (AlumnoDao, con insertar, buscarPorId, listarPorCiclo, borrar) y una o varias implementaciones (AlumnoDaoJdbc con SQL, AlumnoDaoMemoria para las pruebas). El resto del programa trabaja con objetos Alumno y llama al DAO por su interfaz: no sabe si hay MySQL, un fichero CSV o un HashMap detrás.

El DAO también traduce los errores del almacenamiento a errores de la aplicación: una SQLException de clave duplicada se convierte en «ya existe el alumno», y quien llama no tiene que saber nada de JDBC. Es el patrón que se pide en casi todos los proyectos con base de datos de Acceso a Datos y de Entorno Servidor.

Cuándo usarlo

  • Cualquier aplicación que guarda datos en una base de datos, un fichero o un servicio externo.
  • Quieres poder cambiar el almacenamiento (de un fichero a MySQL, de MySQL a PostgreSQL) sin tocar la lógica.
  • Quieres probar la lógica con un DAO en memoria, sin levantar una base de datos.

Cuándo no

  • Si usas un ORM o un framework que ya da el acceso a datos (JPA con Spring Data, Eloquent, Entity Framework): normalmente se usa su repositorio.
  • En un script de un solo uso que lanza una consulta y termina.

Participantes

  1. Entidad. La clase con los datos (Alumno), sin lógica de acceso.
  2. Interfaz DAO. Las operaciones de acceso de esa entidad (AlumnoDao).
  3. Implementaciones. Una por almacenamiento: AlumnoDaoJdbc con SQL y PreparedStatement, AlumnoDaoMemoria para las pruebas.
  4. Cliente. La lógica de la aplicación (Secretaria), que recibe el DAO por su interfaz.

Diagrama de clases

1daoguardaAlumno+id : int+nombre : String+ciclo : String«interface»AlumnoDao+insertar(a : Alumno) : void+buscarPorId(id : int) : Optional<Alumno>+listarPorCiclo(ciclo : String) : List<Alumno>+borrar(id : int) : booleanAlumnoDaoMemoria-tabla : Map<Integer, Alumno>AlumnoDaoJdbc-conexion : ConnectionSecretaria-dao : AlumnoDao+matricular(id : int, nombre : String, ciclo : String) : void
Arrastra las clases para colocarlas a tu gusto.
Ver el diagrama en PlantUML
text
1@startuml
2class Alumno {
3  +id : int
4  +nombre : String
5  +ciclo : String
6}
7interface AlumnoDao {
8  +insertar(a : Alumno) : void
9  +buscarPorId(id : int) : Optional<Alumno>
10  +listarPorCiclo(ciclo : String) : List<Alumno>
11  +borrar(id : int) : boolean
12}
13class AlumnoDaoMemoria {
14  -tabla : Map<Integer, Alumno>
15}
16class AlumnoDaoJdbc {
17  -conexion : Connection
18}
19class Secretaria {
20  -dao : AlumnoDao
21  +matricular(id : int, nombre : String, ciclo : String) : void
22}
23AlumnoDao <|.. AlumnoDaoMemoria
24AlumnoDao <|.. AlumnoDaoJdbc
25Secretaria --> "1" AlumnoDao : dao
26AlumnoDao ..> Alumno : guarda
27@enduml

Puedes copiarlo en el editor de diagramas UML y modificarlo.

El código

Alumnos con un DAO

Secretaria solo conoce la interfaz AlumnoDao. Aquí recibe la versión en memoria; con la de JDBC funcionaría igual.

Java
1import java.util.*;
2
3record Alumno(int id, String nombre, String ciclo) {}
4
5/** El DAO: todas las operaciones con los alumnos guardados, sin decir dónde se guardan. */
6interface AlumnoDao {
7    void insertar(Alumno a);
8    Optional<Alumno> buscarPorId(int id);
9    List<Alumno> listarPorCiclo(String ciclo);
10    boolean borrar(int id);
11}
12
13/** Una implementación en memoria, para probar. La de la base de datos cumple la misma interfaz. */
14class AlumnoDaoMemoria implements AlumnoDao {
15    private final Map<Integer, Alumno> tabla = new TreeMap<>();
16
17    public void insertar(Alumno a) {
18        if (tabla.containsKey(a.id())) throw new IllegalStateException("ya existe el alumno " + a.id());
19        tabla.put(a.id(), a);
20    }
21
22    public Optional<Alumno> buscarPorId(int id) { return Optional.ofNullable(tabla.get(id)); }
23
24    public List<Alumno> listarPorCiclo(String ciclo) {
25        List<Alumno> r = new ArrayList<>();
26        for (Alumno a : tabla.values()) if (a.ciclo().equals(ciclo)) r.add(a);
27        return r;
28    }
29
30    public boolean borrar(int id) { return tabla.remove(id) != null; }
31}
32
33/** La lógica de la aplicación usa el DAO por su interfaz: aquí no hay ni una línea de SQL. */
34class Secretaria {
35    private final AlumnoDao dao;
36
37    Secretaria(AlumnoDao dao) { this.dao = dao; }
38
39    void matricular(int id, String nombre, String ciclo) {
40        try {
41            dao.insertar(new Alumno(id, nombre, ciclo));
42            System.out.println("Matriculado: " + nombre + " en " + ciclo);
43        } catch (IllegalStateException e) {
44            System.out.println("No se puede matricular a " + nombre + ": " + e.getMessage());
45        }
46    }
47
48    void listar(String ciclo) {
49        List<String> nombres = new ArrayList<>();
50        for (Alumno a : dao.listarPorCiclo(ciclo)) nombres.add(a.nombre());
51        System.out.println("Alumnos de " + ciclo + ": " + String.join(", ", nombres));
52    }
53
54    void ficha(int id) {
55        System.out.println(dao.buscarPorId(id).map(a -> "#" + a.id() + " " + a.nombre() + " (" + a.ciclo() + ")").orElse("No existe el alumno " + id));
56    }
57}
58
59public class Main {
60    public static void main(String[] args) {
61        Secretaria s = new Secretaria(new AlumnoDaoMemoria());   // con AlumnoDaoJdbc funcionaría igual
62        s.matricular(1, "Ana", "DAM");
63        s.matricular(2, "Luis", "DAW");
64        s.matricular(3, "Marta", "DAM");
65        s.matricular(2, "Pablo", "DAM");
66        s.listar("DAM");
67        s.ficha(2);
68        s.ficha(9);
69    }
70}

Salida al ejecutarlo (la misma en los 5 lenguajes)

Matriculado: Ana en DAM
Matriculado: Luis en DAW
Matriculado: Marta en DAM
No se puede matricular a Pablo: ya existe el alumno 2
Alumnos de DAM: Ana, Marta
#2 Luis (DAW)
No existe el alumno 9

La implementación con JDBC

El SQL, los PreparedStatement y las SQLException se quedan dentro del DAO.

Java
1/** La implementación de verdad: el SQL vive aquí y solo aquí. */
2public class AlumnoDaoJdbc implements AlumnoDao {
3    private final DataSource datos;
4
5    public AlumnoDaoJdbc(DataSource datos) { this.datos = datos; }
6
7    @Override
8    public void insertar(Alumno a) {
9        String sql = "INSERT INTO alumno (id, nombre, ciclo) VALUES (?, ?, ?)";
10        try (Connection con = datos.getConnection();
11             PreparedStatement ps = con.prepareStatement(sql)) {
12            ps.setInt(1, a.id());
13            ps.setString(2, a.nombre());
14            ps.setString(3, a.ciclo());
15            ps.executeUpdate();
16        } catch (SQLIntegrityConstraintViolationException e) {
17            throw new IllegalStateException("ya existe el alumno " + a.id());
18        } catch (SQLException e) {
19            throw new RuntimeException("Error al insertar el alumno", e);   // el resto no ve SQLException
20        }
21    }
22
23    @Override
24    public List<Alumno> listarPorCiclo(String ciclo) {
25        String sql = "SELECT id, nombre, ciclo FROM alumno WHERE ciclo = ? ORDER BY id";
26        List<Alumno> r = new ArrayList<>();
27        try (Connection con = datos.getConnection();
28             PreparedStatement ps = con.prepareStatement(sql)) {
29            ps.setString(1, ciclo);
30            try (ResultSet rs = ps.executeQuery()) {
31                while (rs.next()) r.add(new Alumno(rs.getInt("id"), rs.getString("nombre"), rs.getString("ciclo")));
32            }
33        } catch (SQLException e) {
34            throw new RuntimeException("Error al listar los alumnos", e);
35        }
36        return r;
37    }
38    // buscarPorId y borrar siguen la misma forma
39}

En la práctica

  • Los proyectos con JDBC de Acceso a Datos y de Programación (AlumnoDao, ProductoDao…), y con PDO en Entorno Servidor.
  • Spring JdbcTemplate dentro de clases DAO; los @Dao de Room en Android son interfaces DAO de verdad.
  • Las capas de persistencia de cualquier aplicación por capas: controlador → servicio → DAO → base de datos.
  • Los DAO en memoria para las pruebas unitarias de los servicios.

Errores típicos

  • Dejar SQLException y ResultSet salir del DAO: el resto de la aplicación vuelve a depender de JDBC.
  • Concatenar los valores en el SQL en vez de usar PreparedStatement con ?: inyección SQL.
  • Meter lógica de negocio en el DAO (calcular la nota media del curso, decidir si un alumno aprueba): el DAO solo guarda y recupera.
  • No cerrar conexiones y sentencias: usa try con recursos.

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 DAO de productos en memoria

La interfaz ProductoDao ya está definida y el main la usa. Escribe ProductoDaoMemoria, que guarda los productos en un mapa y les da un id autoincremental, como haría una tabla con AUTO_INCREMENT.

  • Órdenes: alta nombre precio, ver id, precio id nuevo, baja id y lista.
  • Los ids empiezan en 1 y no se reutilizan aunque se borre un producto. listar devuelve los productos ordenados por nombre, sin distinguir mayúsculas, y por id si empatan.
JavaUn DAO de productos en memoriaFácil

Ejemplo

Entrada (lo que se escribe por teclado)
alta teclado 25.5
alta Altavoz 40
alta raton 12
lista
precio 3 9.99
ver 3
baja 1
lista
Salida esperada
Alta con id 1
Alta con id 2
Alta con id 3
2. Altavoz: 40,00 €
3. raton: 12,00 €
1. teclado: 25,50 €
Precio cambiado
3. raton: 9,99 €
Borrado
2. Altavoz: 40,00 €
3. raton: 9,99 €
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.math.*;
2import java.util.*;
3
4/** Un producto. El id lo asigna el DAO al insertar (como un AUTO_INCREMENT). */
5record Producto(int id, String nombre, double precio) {}
6
7interface ProductoDao {
8    /** Guarda un producto nuevo y devuelve el id que le ha dado (1, 2, 3…). */
9    int insertar(String nombre, double precio);
10    Optional<Producto> buscar(int id);
11    /** Todos, ordenados por nombre (sin distinguir mayúsculas) y, si empatan, por id. */
12    List<Producto> listar();
13    /** false si no existe. */
14    boolean actualizarPrecio(int id, double precio);
15    boolean borrar(int id);
16}
17
18class ProductoDaoMemoria implements ProductoDao {
19    private final Map<Integer, Producto> tabla = new HashMap<>();
20    private int siguiente = 1;
21
22    public int insertar(String nombre, double precio) {
23        int id = siguiente++;                            // los ids no se reutilizan aunque se borre
24        tabla.put(id, new Producto(id, nombre, precio));
25        return id;
26    }
27
28    public Optional<Producto> buscar(int id) { return Optional.ofNullable(tabla.get(id)); }
29
30    public List<Producto> listar() {
31        List<Producto> r = new ArrayList<>(tabla.values());
32        r.sort(Comparator.comparing((Producto p) -> p.nombre().toLowerCase()).thenComparingInt(Producto::id));
33        return r;
34    }
35
36    public boolean actualizarPrecio(int id, double precio) {
37        Producto p = tabla.get(id);
38        if (p == null) return false;
39        tabla.put(id, new Producto(id, p.nombre(), precio));   // el record es inmutable: se sustituye
40        return true;
41    }
42
43    public boolean borrar(int id) { return tabla.remove(id) != null; }
44}
45
46public class Main {
47    static String euros(double x) {
48        return BigDecimal.valueOf(x).setScale(2, RoundingMode.HALF_UP).toPlainString().replace('.', ',') + " €";
49    }
50
51    public static void main(String[] args) {
52        ProductoDao dao = new ProductoDaoMemoria();
53        Scanner sc = new Scanner(System.in);
54        while (sc.hasNextLine()) {
55            String linea = sc.nextLine().trim();
56            if (linea.isEmpty()) continue;
57            String[] p = linea.split("\\s+");
58            try {
59                switch (p[0] + "/" + (p.length - 1)) {
60                    case "alta/2" -> System.out.println("Alta con id " + dao.insertar(p[1], Double.parseDouble(p[2])));
61                    case "ver/1" -> System.out.println(dao.buscar(Integer.parseInt(p[1])).map(x -> x.id() + ". " + x.nombre() + ": " + euros(x.precio())).orElse("No existe el producto " + p[1]));
62                    case "precio/2" -> System.out.println(dao.actualizarPrecio(Integer.parseInt(p[1]), Double.parseDouble(p[2])) ? "Precio cambiado" : "No existe el producto " + p[1]);
63                    case "baja/1" -> System.out.println(dao.borrar(Integer.parseInt(p[1])) ? "Borrado" : "No existe el producto " + p[1]);
64                    case "lista/0" -> {
65                        List<Producto> l = dao.listar();
66                        if (l.isEmpty()) System.out.println("Sin productos");
67                        for (Producto x : l) System.out.println(x.id() + ". " + x.nombre() + ": " + euros(x.precio()));
68                    }
69                    default -> System.out.println("Orden no válida: " + linea);
70                }
71            } catch (NumberFormatException e) {
72                System.out.println("Orden no válida: " + linea);
73            }
74        }
75    }
76}

El main solo usa la interfaz: cambiar el mapa por una tabla de MySQL sería escribir otra clase que la implemente.

Que los ids no se reutilicen evita que un enlace o una factura antigua apunte a otro producto.

2. Un DAO sobre un fichero CSV

Escribe AlumnoDaoCsv, que guarda cada alumno en una línea id;nombre;nota de un Fichero. El fichero es la única fuente de verdad: cada operación lee las líneas y, si cambia algo, las vuelve a escribir todas. El main cuenta las escrituras: no reescribas el fichero si no ha cambiado nada.

  • Órdenes: alta id nombre nota, ver id, nota id nueva, baja id y lista. Al final se escribe el contenido del fichero y cuántas escrituras se han hecho.
  • Las notas se guardan sin ceros de más (8.5, 7). insertar lanza IllegalStateException con ya existe el alumno id si el id está repetido, y IllegalArgumentException con el nombre no puede llevar ;.
JavaUn DAO sobre un fichero CSVMedio

Ejemplo

Entrada (lo que se escribe por teclado)
alta 1 Ana 8.5
alta 2 Luis 6
alta 3 Marta 9.25
nota 2 7.5
baja 1
lista
ver 3
Salida esperada
Alta: Ana
Alta: Luis
Alta: Marta
Nota cambiada
Baja hecha
Luis 7,5 · Marta 9,25
Marta: 9,25
--- fichero (5 escrituras) ---
2;Luis;7.5
3;Marta;9.25
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.math.*;
2import java.util.*;
3
4/** Hace de fichero de texto: el DAO solo puede leer todas sus líneas o reescribirlas todas. */
5class Fichero {
6    private List<String> lineas = new ArrayList<>();
7    int escrituras = 0;
8    List<String> leer() { return new ArrayList<>(lineas); }
9    void escribir(List<String> nuevas) { lineas = new ArrayList<>(nuevas); escrituras++; }
10}
11
12record Alumno(int id, String nombre, double nota) {}
13
14interface AlumnoDao {
15    void insertar(Alumno a);
16    Optional<Alumno> buscar(int id);
17    List<Alumno> listar();
18    boolean actualizarNota(int id, double nota);
19    boolean borrar(int id);
20}
21
22/** Guarda cada alumno en una línea «id;nombre;nota» del fichero, en el orden en que se insertó. */
23class AlumnoDaoCsv implements AlumnoDao {
24    private final Fichero fichero;
25
26    AlumnoDaoCsv(Fichero fichero) { this.fichero = fichero; }
27
28    private static Alumno leer(String linea) {
29        String[] p = linea.split(";");
30        return new Alumno(Integer.parseInt(p[0]), p[1], Double.parseDouble(p[2]));
31    }
32
33    private static String escribir(Alumno a) {
34        return a.id() + ";" + a.nombre() + ";" + BigDecimal.valueOf(a.nota()).stripTrailingZeros().toPlainString();
35    }
36
37    public void insertar(Alumno a) {
38        if (a.nombre().contains(";")) throw new IllegalArgumentException("el nombre no puede llevar ;");
39        if (buscar(a.id()).isPresent()) throw new IllegalStateException("ya existe el alumno " + a.id());
40        List<String> l = fichero.leer();
41        l.add(escribir(a));
42        fichero.escribir(l);
43    }
44
45    public Optional<Alumno> buscar(int id) {
46        for (String linea : fichero.leer()) {
47            Alumno a = leer(linea);
48            if (a.id() == id) return Optional.of(a);
49        }
50        return Optional.empty();
51    }
52
53    public List<Alumno> listar() {
54        List<Alumno> r = new ArrayList<>();
55        for (String linea : fichero.leer()) r.add(leer(linea));
56        return r;
57    }
58
59    public boolean actualizarNota(int id, double nota) {
60        List<String> l = fichero.leer();
61        for (int i = 0; i < l.size(); i++) {
62            Alumno a = leer(l.get(i));
63            if (a.id() == id) {
64                l.set(i, escribir(new Alumno(id, a.nombre(), nota)));
65                fichero.escribir(l);
66                return true;
67            }
68        }
69        return false;                                    // si no existe, no se reescribe el fichero
70    }
71
72    public boolean borrar(int id) {
73        List<String> l = fichero.leer();
74        boolean quitado = l.removeIf(linea -> leer(linea).id() == id);
75        if (quitado) fichero.escribir(l);
76        return quitado;
77    }
78}
79
80public class Main {
81    static String nota(double x) {
82        return BigDecimal.valueOf(x).stripTrailingZeros().toPlainString().replace('.', ',');
83    }
84
85    public static void main(String[] args) {
86        Fichero fichero = new Fichero();
87        AlumnoDao dao = new AlumnoDaoCsv(fichero);
88        Scanner sc = new Scanner(System.in);
89        while (sc.hasNextLine()) {
90            String linea = sc.nextLine().trim();
91            if (linea.isEmpty()) continue;
92            String[] p = linea.split("\\s+");
93            try {
94                switch (p[0] + "/" + (p.length - 1)) {
95                    case "alta/3" -> {
96                        double n = Double.parseDouble(p[3]);
97                        if (n < 0 || n > 10) throw new NumberFormatException();
98                        dao.insertar(new Alumno(Integer.parseInt(p[1]), p[2], n));
99                        System.out.println("Alta: " + p[2]);
100                    }
101                    case "ver/1" -> System.out.println(dao.buscar(Integer.parseInt(p[1])).map(a -> a.nombre() + ": " + nota(a.nota())).orElse("No existe el alumno " + p[1]));
102                    case "nota/2" -> {
103                        double n = Double.parseDouble(p[2]);
104                        if (n < 0 || n > 10) throw new NumberFormatException();
105                        System.out.println(dao.actualizarNota(Integer.parseInt(p[1]), n) ? "Nota cambiada" : "No existe el alumno " + p[1]);
106                    }
107                    case "baja/1" -> System.out.println(dao.borrar(Integer.parseInt(p[1])) ? "Baja hecha" : "No existe el alumno " + p[1]);
108                    case "lista/0" -> {
109                        List<String> l = new ArrayList<>();
110                        for (Alumno a : dao.listar()) l.add(a.nombre() + " " + nota(a.nota()));
111                        System.out.println(l.isEmpty() ? "Sin alumnos" : String.join(" · ", l));
112                    }
113                    default -> System.out.println("Orden no válida: " + linea);
114                }
115            } catch (NumberFormatException e) {
116                System.out.println("Orden no válida: " + linea);
117            } catch (IllegalStateException | IllegalArgumentException e) {
118                System.out.println("Error: " + e.getMessage());
119            }
120        }
121        System.out.println("--- fichero (" + fichero.escrituras + (fichero.escrituras == 1 ? " escritura" : " escrituras") + ") ---");
122        for (String l : fichero.leer()) System.out.println(l);
123    }
124}

El formato del fichero solo lo conoce el DAO: si mañana se pasa a JSON o a una base de datos, el main no cambia.

Leer y reescribir todo en cada operación es sencillo pero caro: por eso las bases de datos existen. Con pocos datos, un CSV es suficiente.

Test

Test: DAO (Data Access Object)

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.¿Qué contiene un DAO?

  2. 2.¿Por qué se define el DAO como una interfaz?

  3. 3.Una consulta del DAO lanza SQLException. ¿Qué es lo más adecuado?

  4. 4.¿Cómo se evita la inyección SQL en un DAO con JDBC?

  5. 5.¿Qué no debería hacer un DAO?

Relacionado