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
- Entidad. La clase con los datos (
Alumno), sin lógica de acceso. - Interfaz DAO. Las operaciones de acceso de esa entidad (
AlumnoDao). - Implementaciones. Una por almacenamiento:
AlumnoDaoJdbccon SQL yPreparedStatement,AlumnoDaoMemoriapara las pruebas. - Cliente. La lógica de la aplicación (
Secretaria), que recibe el DAO por su interfaz.
Diagrama de clases
Ver el diagrama en PlantUML
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@endumlPuedes 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.
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}from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass(frozen=True)
class Alumno:
id: int
nombre: str
ciclo: str
class AlumnoDao(ABC):
"""El DAO: todas las operaciones con los alumnos guardados, sin decir dónde se guardan."""
@abstractmethod
def insertar(self, a): ...
@abstractmethod
def buscar_por_id(self, id): ...
@abstractmethod
def listar_por_ciclo(self, ciclo): ...
@abstractmethod
def borrar(self, id): ...
class AlumnoDaoMemoria(AlumnoDao):
"""Una implementación en memoria, para probar. La de la base de datos cumple la misma interfaz."""
def __init__(self):
self._tabla = {}
def insertar(self, a):
if a.id in self._tabla:
raise KeyError(f"ya existe el alumno {a.id}")
self._tabla[a.id] = a
def buscar_por_id(self, id):
return self._tabla.get(id)
def listar_por_ciclo(self, ciclo):
return [a for _, a in sorted(self._tabla.items()) if a.ciclo == ciclo]
def borrar(self, id):
return self._tabla.pop(id, None) is not None
class Secretaria:
"""La lógica de la aplicación usa el DAO por su interfaz: aquí no hay ni una línea de SQL."""
def __init__(self, dao):
self._dao = dao
def matricular(self, id, nombre, ciclo):
try:
self._dao.insertar(Alumno(id, nombre, ciclo))
print(f"Matriculado: {nombre} en {ciclo}")
except KeyError as e:
print(f"No se puede matricular a {nombre}: {e.args[0]}")
def listar(self, ciclo):
print(f"Alumnos de {ciclo}: {', '.join(a.nombre for a in self._dao.listar_por_ciclo(ciclo))}")
def ficha(self, id):
a = self._dao.buscar_por_id(id)
print(f"#{a.id} {a.nombre} ({a.ciclo})" if a else f"No existe el alumno {id}")
s = Secretaria(AlumnoDaoMemoria()) # con un DAO de sqlite3 funcionaría igual
s.matricular(1, "Ana", "DAM")
s.matricular(2, "Luis", "DAW")
s.matricular(3, "Marta", "DAM")
s.matricular(2, "Pablo", "DAM")
s.listar("DAM")
s.ficha(2)
s.ficha(9)/** Una implementación en memoria del DAO de alumnos. La de la base de datos tendría los mismos métodos. */
class AlumnoDaoMemoria {
#tabla = new Map();
insertar(a) {
if (this.#tabla.has(a.id)) throw new Error(`ya existe el alumno ${a.id}`);
this.#tabla.set(a.id, a);
}
buscarPorId(id) { return this.#tabla.get(id) ?? null; }
listarPorCiclo(ciclo) {
return [...this.#tabla.values()].sort((x, y) => x.id - y.id).filter((a) => a.ciclo === ciclo);
}
borrar(id) { return this.#tabla.delete(id); }
}
/** La lógica de la aplicación usa el DAO: aquí no hay ni una línea de SQL. */
class Secretaria {
constructor(dao) { this.dao = dao; }
matricular(id, nombre, ciclo) {
try {
this.dao.insertar({ id, nombre, ciclo });
console.log(`Matriculado: ${nombre} en ${ciclo}`);
} catch (e) {
console.log(`No se puede matricular a ${nombre}: ${e.message}`);
}
}
listar(ciclo) {
console.log(`Alumnos de ${ciclo}: ${this.dao.listarPorCiclo(ciclo).map((a) => a.nombre).join(", ")}`);
}
ficha(id) {
const a = this.dao.buscarPorId(id);
console.log(a ? `#${a.id} ${a.nombre} (${a.ciclo})` : `No existe el alumno ${id}`);
}
}
const s = new Secretaria(new AlumnoDaoMemoria()); // con un DAO de MySQL funcionaría igual
s.matricular(1, "Ana", "DAM");
s.matricular(2, "Luis", "DAW");
s.matricular(3, "Marta", "DAM");
s.matricular(2, "Pablo", "DAM");
s.listar("DAM");
s.ficha(2);
s.ficha(9);using System;
using System.Collections.Generic;
using System.Linq;
record Alumno(int Id, string Nombre, string Ciclo);
// El DAO: todas las operaciones con los alumnos guardados, sin decir dónde se guardan.
interface IAlumnoDao {
void Insertar(Alumno a);
Alumno? BuscarPorId(int id);
List<Alumno> ListarPorCiclo(string ciclo);
bool Borrar(int id);
}
// Una implementación en memoria, para probar. La de la base de datos cumple la misma interfaz.
class AlumnoDaoMemoria : IAlumnoDao {
private readonly SortedDictionary<int, Alumno> tabla = new();
public void Insertar(Alumno a) {
if (tabla.ContainsKey(a.Id)) throw new InvalidOperationException(quot;ya existe el alumno {a.Id}");
tabla[a.Id] = a;
}
public Alumno? BuscarPorId(int id) => tabla.GetValueOrDefault(id);
public List<Alumno> ListarPorCiclo(string ciclo) => tabla.Values.Where(a => a.Ciclo == ciclo).ToList();
public bool Borrar(int id) => tabla.Remove(id);
}
// La lógica de la aplicación usa el DAO por su interfaz: aquí no hay ni una línea de SQL.
class Secretaria {
private readonly IAlumnoDao dao;
public Secretaria(IAlumnoDao dao) => this.dao = dao;
public void Matricular(int id, string nombre, string ciclo) {
try {
dao.Insertar(new Alumno(id, nombre, ciclo));
Console.WriteLine(quot;Matriculado: {nombre} en {ciclo}");
} catch (InvalidOperationException e) {
Console.WriteLine(quot;No se puede matricular a {nombre}: {e.Message}");
}
}
public void Listar(string ciclo) =>
Console.WriteLine(quot;Alumnos de {ciclo}: {string.Join(", ", dao.ListarPorCiclo(ciclo).Select(a => a.Nombre))}");
public void Ficha(int id) {
var a = dao.BuscarPorId(id);
Console.WriteLine(a != null ? quot;#{a.Id} {a.Nombre} ({a.Ciclo})" : quot;No existe el alumno {id}");
}
}
class Program {
static void Main() {
var s = new Secretaria(new AlumnoDaoMemoria()); // con un DAO de ADO.NET funcionaría igual
s.Matricular(1, "Ana", "DAM");
s.Matricular(2, "Luis", "DAW");
s.Matricular(3, "Marta", "DAM");
s.Matricular(2, "Pablo", "DAM");
s.Listar("DAM");
s.Ficha(2);
s.Ficha(9);
}
}<?php
final class Alumno {
public function __construct(public int $id, public string $nombre, public string $ciclo) {}
}
/** El DAO: todas las operaciones con los alumnos guardados, sin decir dónde se guardan. */
interface AlumnoDao {
public function insertar(Alumno $a): void;
public function buscarPorId(int $id): ?Alumno;
public function listarPorCiclo(string $ciclo): array;
public function borrar(int $id): bool;
}
/** Una implementación en memoria, para probar. La de PDO cumple la misma interfaz. */
class AlumnoDaoMemoria implements AlumnoDao {
private array $tabla = [];
public function insertar(Alumno $a): void {
if (isset($this->tabla[$a->id])) throw new RuntimeException("ya existe el alumno {$a->id}");
$this->tabla[$a->id] = $a;
ksort($this->tabla);
}
public function buscarPorId(int $id): ?Alumno { return $this->tabla[$id] ?? null; }
public function listarPorCiclo(string $ciclo): array {
return array_values(array_filter($this->tabla, fn (Alumno $a) => $a->ciclo === $ciclo));
}
public function borrar(int $id): bool {
if (!isset($this->tabla[$id])) return false;
unset($this->tabla[$id]);
return true;
}
}
/** La lógica de la aplicación usa el DAO por su interfaz: aquí no hay ni una línea de SQL. */
class Secretaria {
public function __construct(private AlumnoDao $dao) {}
public function matricular(int $id, string $nombre, string $ciclo): void {
try {
$this->dao->insertar(new Alumno($id, $nombre, $ciclo));
echo "Matriculado: $nombre en $ciclo\n";
} catch (RuntimeException $e) {
echo "No se puede matricular a $nombre: {$e->getMessage()}\n";
}
}
public function listar(string $ciclo): void {
$nombres = array_map(fn (Alumno $a) => $a->nombre, $this->dao->listarPorCiclo($ciclo));
echo "Alumnos de $ciclo: ", implode(", ", $nombres), "\n";
}
public function ficha(int $id): void {
$a = $this->dao->buscarPorId($id);
echo $a ? "#{$a->id} {$a->nombre} ({$a->ciclo})" : "No existe el alumno $id", "\n";
}
}
$s = new Secretaria(new AlumnoDaoMemoria()); // con un DAO de PDO funcionaría igual
$s->matricular(1, "Ana", "DAM");
$s->matricular(2, "Luis", "DAW");
$s->matricular(3, "Marta", "DAM");
$s->matricular(2, "Pablo", "DAM");
$s->listar("DAM");
$s->ficha(2);
$s->ficha(9);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.
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}import sqlite3
class AlumnoDaoSqlite(AlumnoDao):
"""La implementación de verdad: el SQL vive aquí y solo aquí."""
def __init__(self, ruta):
self._con = sqlite3.connect(ruta)
def insertar(self, a):
try:
with self._con:
self._con.execute("INSERT INTO alumno (id, nombre, ciclo) VALUES (?, ?, ?)", (a.id, a.nombre, a.ciclo))
except sqlite3.IntegrityError:
raise KeyError(f"ya existe el alumno {a.id}") # el resto no ve sqlite3
def listar_por_ciclo(self, ciclo):
filas = self._con.execute("SELECT id, nombre, ciclo FROM alumno WHERE ciclo = ? ORDER BY id", (ciclo,))
return [Alumno(*f) for f in filas]// Con Node y mysql2: el SQL vive aquí y solo aquí
class AlumnoDaoMysql {
constructor(pool) { this.pool = pool; }
async insertar(a) {
try {
await this.pool.execute("INSERT INTO alumno (id, nombre, ciclo) VALUES (?, ?, ?)", [a.id, a.nombre, a.ciclo]);
} catch (e) {
if (e.code === "ER_DUP_ENTRY") throw new Error(`ya existe el alumno ${a.id}`);
throw e;
}
}
async listarPorCiclo(ciclo) {
const [filas] = await this.pool.execute("SELECT id, nombre, ciclo FROM alumno WHERE ciclo = ? ORDER BY id", [ciclo]);
return filas;
}
}// ADO.NET: el SQL y los parámetros viven aquí y solo aquí
public class AlumnoDaoSqlite : IAlumnoDao {
private readonly string cadena;
public AlumnoDaoSqlite(string cadena) => this.cadena = cadena;
public List<Alumno> ListarPorCiclo(string ciclo) {
using var con = new SqliteConnection(cadena);
con.Open();
using var cmd = con.CreateCommand();
cmd.CommandText = "SELECT id, nombre, ciclo FROM alumno WHERE ciclo = $ciclo ORDER BY id";
cmd.Parameters.AddWithValue("$ciclo", ciclo);
using var lector = cmd.ExecuteReader();
var r = new List<Alumno>();
while (lector.Read()) r.Add(new Alumno(lector.GetInt32(0), lector.GetString(1), lector.GetString(2)));
return r;
}
// Insertar, BuscarPorId y Borrar siguen la misma forma
}<?php
/** La implementación con PDO: el SQL vive aquí y solo aquí. */
class AlumnoDaoPdo implements AlumnoDao {
public function __construct(private PDO $pdo) {}
public function insertar(Alumno $a): void {
try {
$st = $this->pdo->prepare("INSERT INTO alumno (id, nombre, ciclo) VALUES (?, ?, ?)");
$st->execute([$a->id, $a->nombre, $a->ciclo]);
} catch (PDOException $e) {
if ($e->getCode() === "23000") throw new RuntimeException("ya existe el alumno {$a->id}");
throw $e;
}
}
public function listarPorCiclo(string $ciclo): array {
$st = $this->pdo->prepare("SELECT id, nombre, ciclo FROM alumno WHERE ciclo = ? ORDER BY id");
$st->execute([$ciclo]);
return array_map(fn ($f) => new Alumno($f["id"], $f["nombre"], $f["ciclo"]), $st->fetchAll(PDO::FETCH_ASSOC));
}
// buscarPorId y borrar siguen la misma forma
}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
JdbcTemplatedentro de clases DAO; los@Daode 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
SQLExceptionyResultSetsalir del DAO: el resto de la aplicación vuelve a depender de JDBC. - Concatenar los valores en el SQL en vez de usar
PreparedStatementcon?: 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
trycon 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 idylista. - Los ids empiezan en 1 y no se reutilizan aunque se borre un producto.
listardevuelve los productos ordenados por nombre, sin distinguir mayúsculas, y por id si empatan.
Ejemplo
alta teclado 25.5 alta Altavoz 40 alta raton 12 lista precio 3 9.99 ver 3 baja 1 lista
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 €
Ver la solución explicada
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 idylista. 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).insertarlanzaIllegalStateExceptionconya existe el alumno idsi el id está repetido, yIllegalArgumentExceptionconel nombre no puede llevar ;.
Ejemplo
alta 1 Ana 8.5 alta 2 Luis 6 alta 3 Marta 9.25 nota 2 7.5 baja 1 lista ver 3
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
Ver la solución explicada
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 aciertosElige 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.¿Qué contiene un DAO?
2.¿Por qué se define el DAO como una interfaz?
3.Una consulta del DAO lanza SQLException. ¿Qué es lo más adecuado?
4.¿Cómo se evita la inyección SQL en un DAO con JDBC?
5.¿Qué no debería hacer un DAO?