Apuntes DAM
Volver al inicio

Ejercicios resueltos de Acceso a Datos

Los 24 ejercicios de Acceso a Datos de la web, tema a tema: cada uno con su enunciado, los datos que necesitas (código de partida, ejemplos de entrada y salida o la base de datos) y, al final, la solución explicada.

Descargar el PDF

Manejo de Ficheros

1. Media de notas desde un fichero CSV

Fácil · Manejo de Ficheros · apuntesdam.com/subject/acceso-datos/topic/gestion-ficheros

El programa guarda en notas.csv las líneas recibidas por teclado (formato nombre;nota). Completa leerMedia() para que lea el fichero línea a línea con BufferedReader y devuelva la media de las notas. Se imprime con dos decimales.

Código de partida (java)
import java.io.*;import java.util.*; public class Main {     static double leerMedia(File fichero) throws IOException {        double suma = 0;        int contador = 0;        // TODO: lee el fichero línea a línea y acumula las notas         return contador == 0 ? 0 : suma / contador;    }     public static void main(String[] args) throws IOException {        // Guarda en notas.csv las líneas recibidas por teclado        File fichero = new File("notas.csv");        Scanner sc = new Scanner(System.in);        try (PrintWriter pw = new PrintWriter(new FileWriter(fichero))) {            while (sc.hasNextLine()) pw.println(sc.nextLine());        }         System.out.printf(Locale.US, "Media: %.2f%n", leerMedia(fichero));    }}

Ejemplo: Tres alumnos

Entrada

Ana;8
Luis;6
Marta;9.5

Salida esperada

Media: 7.83

Ejemplo: Un alumno

Entrada

Pedro;5

Salida esperada

Media: 5.00

2. Serializar una lista de productos

Medio · Manejo de Ficheros · apuntesdam.com/subject/acceso-datos/topic/gestion-ficheros

Completa guardar() y cargar() para escribir la lista de productos en productos.obj con ObjectOutputStream y volver a leerla con ObjectInputStream. El programa lee los productos por teclado (nombre;precio), los guarda, los carga de nuevo y muestra cuántos hay y el importe total.

Código de partida (java)
import java.io.*;import java.util.*; public class Main {     static class Producto implements Serializable {        private static final long serialVersionUID = 1L;        String nombre;        double precio;        Producto(String nombre, double precio) { this.nombre = nombre; this.precio = precio; }    }     static void guardar(List<Producto> productos, File fichero) throws IOException {        // TODO: escribe la lista en el fichero    }     @SuppressWarnings("unchecked")    static List<Producto> cargar(File fichero) throws IOException, ClassNotFoundException {        // TODO: lee la lista desde el fichero        return new ArrayList<>();    }     public static void main(String[] args) throws Exception {        List<Producto> productos = new ArrayList<>();        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String[] p = sc.nextLine().split(";");            if (p.length == 2) productos.add(new Producto(p[0], Double.parseDouble(p[1])));        }         File fichero = new File("productos.obj");        guardar(productos, fichero);        List<Producto> leidos = cargar(fichero);         double total = 0;        for (Producto p : leidos) total += p.precio;        System.out.printf(Locale.US, "%d productos, total %.2f%n", leidos.size(), total);    }}

Ejemplo: Tres productos

Entrada

Teclado;49.99
Ratón;19.5
Monitor;189

Salida esperada

3 productos, total 258.49

3. Leer un XML con DOM

Medio · Manejo de Ficheros · apuntesdam.com/subject/acceso-datos/topic/gestion-ficheros

El programa guarda en biblioteca.xml el documento recibido por teclado. Completa procesar() para cargarlo con DOM, recorrer los elementos <libro> y mostrar cuántos libros hay y cuántos están disponibles (atributo disponible="si").

Código de partida (java)
import java.io.*;import java.nio.file.*;import javax.xml.parsers.*;import org.w3c.dom.*; public class Main {     static void procesar(File fichero) throws Exception {        int total = 0, disponibles = 0;        // TODO: carga el XML con DOM y cuenta los libros         System.out.println("Libros: " + total + ", disponibles: " + disponibles);    }     public static void main(String[] args) throws Exception {        File fichero = new File("biblioteca.xml");        Files.write(fichero.toPath(), System.in.readAllBytes());        procesar(fichero);    }}

Ejemplo: Tres libros, dos disponibles

Entrada

<biblioteca><libro disponible="si"><titulo>Java</titulo></libro><libro disponible="no"><titulo>SQL</titulo></libro><libro disponible="si"><titulo>XML</titulo></libro></biblioteca>

Salida esperada

Libros: 3, disponibles: 2

4. Acceso aleatorio a registros

Difícil · Manejo de Ficheros · apuntesdam.com/subject/acceso-datos/topic/gestion-ficheros

El programa crea empleados.dat con registros de tamaño fijo: id (int, 4 bytes), nombre (10 caracteres = 20 bytes) y salario (double, 8 bytes). Completa leerRegistro() para que use seek() y lea directamente el registro número n (empezando en 1) sin recorrer los anteriores. Se lee n por teclado.

Código de partida (java)
import java.io.*;import java.util.*; public class Main {    static final int TAM = 4 + 20 + 8; // id + nombre(10 chars) + salario     static void escribir(RandomAccessFile raf, int id, String nombre, double salario) throws IOException {        raf.writeInt(id);        StringBuilder sb = new StringBuilder(nombre);        sb.setLength(10); // rellena o recorta a 10 caracteres        raf.writeChars(sb.toString());        raf.writeDouble(salario);    }     static String leerRegistro(RandomAccessFile raf, int n) throws IOException {        // TODO: sitúate en el registro n y lee sus tres campos        return "";    }     public static void main(String[] args) throws IOException {        try (RandomAccessFile raf = new RandomAccessFile("empleados.dat", "rw")) {            raf.setLength(0);            escribir(raf, 1, "Ana", 1850.5);            escribir(raf, 2, "Luis", 2100);            escribir(raf, 3, "Marta", 2475.25);            escribir(raf, 4, "Jorge", 1990);             int n = new Scanner(System.in).nextInt();            System.out.println(leerRegistro(raf, n));        }    }}

Ejemplo: Registro 1

Entrada

1

Salida esperada

1 Ana 1850.5

Ejemplo: Registro 3

Entrada

3

Salida esperada

3 Marta 2475.25

5. Un CSV de verdad: comillas, separadores y saltos de línea

Difícil · Manejo de Ficheros · apuntesdam.com/subject/acceso-datos/topic/gestion-ficheros

Partir cada línea por el separador falla con CSV reales: un campo entre comillas dobles puede contener el separador, saltos de línea y comillas (escritas dos veces: ""). Lee todo el texto (separador ; como el Excel en español) y analízalo carácter a carácter según el RFC 4180: fuera de comillas, ; separa campos y el salto de línea termina el registro (las líneas vacías se ignoran); una comilla al principio abre un campo entrecomillado, dentro del cual "" es una comilla y " cierra. La primera fila es la cabecera. Para cada registro de datos con tantos campos como la cabecera, muestra «Registro i:» y una línea « cabecera = valor» por campo (los saltos de línea dentro del valor se muestran como \n y un valor vacío como «(vacío)»); una comilla en mitad de un campo sin comillas es un carácter normal; si el número de campos no coincide, «Error en el registro i: tiene X campos y se esperaban M». Termina con «Registros válidos: V de T · Campos: M». Si el texto acaba con una comilla sin cerrar: «Error: comilla sin cerrar en el registro i» (i = registros completos antes de ese) y nada más.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in).useDelimiter("\\A");        String texto = sc.hasNext() ? sc.next().replace("\r", "") : "";        List<List<String>> registros = new ArrayList<>();        // TODO: analizar el CSV carácter a carácter (separador ; y comillas dobles)    }}

Ejemplo: Alumnos con campos difíciles

Entrada

nif;nombre;direccion;nota
12345678Z;"García; Ana";"Calle Mayor 3
2º B";8,5
87654321X;Luis "el rápido";Avenida Sol 1;7
X1234567L;"Pérez, ""Lolo""";;
00000000T;Falta;un campo

Salida esperada

Registro 1:
  nif = 12345678Z
  nombre = García; Ana
  direccion = Calle Mayor 3
2º B
  nota = 8,5
Registro 2:
  nif = 87654321X
  nombre = Luis "el rápido"
  direccion = Avenida Sol 1
  nota = 7
Registro 3:
  nif = X1234567L
  nombre = Pérez, "Lolo"
  direccion = (vacío)
  nota = (vacío)
Error en el registro 4: tiene 3 campos y se esperaban 4
Registros válidos: 3 de 4 · Campos: 4

6. Leer un formato binario propio con versión y CRC

Muy difícil · Manejo de Ficheros · apuntesdam.com/subject/acceso-datos/topic/gestion-ficheros

Muchas aplicaciones guardan sus datos en un formato binario propio. Este guarda alumnos así (todo en big-endian, como DataOutputStream): 4 bytes ASCII «ALUM»; un short con la versión (1 o 2); un int con el número de registros; por cada registro, un int id, un short con la longitud en bytes del nombre seguido del nombre en UTF-8, en la versión 2 un byte con el curso, y un float con la nota; y al final un int con el CRC32 de todos los bytes anteriores. La entrada es el fichero en hexadecimal. Comprueba en este orden: la firma (si no es ALUM, «Formato desconocido»), la versión («Versión X no soportada»), que no se acaben los bytes antes de tiempo («Fichero truncado») y el CRC («CRC incorrecto: esperado XXXXXXXX, calculado YYYYYYYY», en hexadecimal con 8 cifras). Si todo es correcto, muestra cada registro como «id · nombre · nota» (en la versión 2, «id · nombre · curso N · nota»), con la nota con dos decimales, y «Registros: n · media M · versión v · CRC OK».

Código de partida (java)
import java.nio.*;import java.util.*;import java.util.zip.CRC32; public class Main {    static byte[] desdeHex(String h) {        byte[] b = new byte[h.length() / 2];        for (int i = 0; i < b.length; i++) b[i] = (byte) Integer.parseInt(h.substring(2 * i, 2 * i + 2), 16);        return b;    }     public static void main(String[] args) {        byte[] datos = desdeHex(new Scanner(System.in).nextLine().trim());        ByteBuffer buf = ByteBuffer.wrap(datos);          // big-endian, como DataInputStream        // TODO: leer cabecera, registros y CRC    }}

Ejemplo: Versión 1 correcta

Entrada

414C554D00010000000300000001000B416E612047617263C3AD614108000000000002000B49C3B1616B69205275697A40C8000000000003000C5A6FC3AB204D617274C3AD6E411C000054FEC7DF

Salida esperada

1 · Ana García · 8.50
2 · Iñaki Ruiz · 6.25
3 · Zoë Martín · 9.75
Registros: 3 · media 8.17 · versión 1 · CRC OK

Ejemplo: Versión 2 con curso

Entrada

414C554D0002000000020000000A00064C7563C3AD610140F000000000000B000C4A6F73C3A9204D6172C3AD610240A0000027FC88E5

Salida esperada

10 · Lucía · curso 1 · 7.50
11 · José María · curso 2 · 5.00
Registros: 2 · media 6.25 · versión 2 · CRC OK

Ejemplo: CRC alterado

Entrada

414C554D00010000000300000001000B416E612047617263C3AD614108000000000002000B49C3B1616B69205275697A40C8000000000003000C5A6FC3AB204D617274C3AD6E411C000054FEC7DE

Salida esperada

CRC incorrecto: esperado 54FEC7DE, calculado 54FEC7DF

Conectores JDBC

7. Sentencias preparadas frente a concatenación

Medio · Conectores JDBC · apuntesdam.com/subject/acceso-datos/topic/jdbc

Un PreparedStatement envía la sentencia y los datos por separado, así que un dato nunca puede convertirse en código SQL. Para verlo, simula lo que pasaría al concatenar. La primera línea es la plantilla con ? como marcadores; cada línea siguiente es un parámetro «tipo:valor» con tipo s (texto), i (entero), d (decimal con punto), b (booleano), f (fecha) o n (nulo). Los ? dentro de un literal entre comillas simples de la plantilla no son marcadores. Muestra «Parámetros: N» y, si no coincide con los dados, «Error: la sentencia tiene N parámetros y se han dado M». Valida que los enteros y decimales lo sean («Error: el parámetro k no es un entero: x» / «no es un decimal»). Después muestra «Concatenando: …» (cada texto entre comillas tal cual) y «Preparada: …» (con las comillas simples del texto duplicadas, que es como queda el dato escapado); los booleanos son TRUE/FALSE, las fechas DATE '…' y los nulos NULL. Termina con «¡Inyección! Con la concatenación el texto del usuario cambia la estructura de la consulta» si algún texto contiene una comilla simple, o «Las dos versiones son equivalentes con estos datos».

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String plantilla = sc.nextLine();        List<String[]> params = new ArrayList<>();          // {tipo, valor}        while (sc.hasNextLine()) {            String l = sc.nextLine();            if (l.isBlank()) continue;            int dos = l.indexOf(':');            params.add(new String[] { l.substring(0, dos), l.substring(dos + 1) });        }        // TODO: localizar los ? fuera de los literales y montar las dos versiones    }}

Ejemplo: Inicio de sesión atacado

Entrada

SELECT * FROM usuario WHERE login = ? AND clave = ?
s:admin
s:' OR '1'='1

Salida esperada

Parámetros: 2
Concatenando: SELECT * FROM usuario WHERE login = 'admin' AND clave = '' OR '1'='1'
Preparada:    SELECT * FROM usuario WHERE login = 'admin' AND clave = ''' OR ''1''=''1'
¡Inyección! Con la concatenación el texto del usuario cambia la estructura de la consulta

Ejemplo: Tipos y ? en un literal

Entrada

UPDATE alumno SET nota = ?, aprobado = ?, nacimiento = ?, tutor = ? WHERE nombre = ? AND obs <> '¿Qué?'
d:7.5
b:true
f:2005-03-14
n:
s:O'Brien

Salida esperada

Parámetros: 5
Concatenando: UPDATE alumno SET nota = 7.5, aprobado = TRUE, nacimiento = DATE '2005-03-14', tutor = NULL WHERE nombre = 'O'Brien' AND obs <> '¿Qué?'
Preparada:    UPDATE alumno SET nota = 7.5, aprobado = TRUE, nacimiento = DATE '2005-03-14', tutor = NULL WHERE nombre = 'O''Brien' AND obs <> '¿Qué?'
¡Inyección! Con la concatenación el texto del usuario cambia la estructura de la consulta

8. Un pool de conexiones por dentro

Difícil · Conectores JDBC · apuntesdam.com/subject/acceso-datos/topic/jdbc

Abrir una conexión a la base de datos cuesta decenas de milisegundos, así que las aplicaciones usan un pool (HikariCP, el de Tomcat…) que las reutiliza. La primera línea es «max M espera W» y las siguientes, ordenadas por tiempo, «t pedir cliente» o «t devolver cliente». Al pedir: si hay una conexión libre se entrega la que lleva más tiempo libre («t: X recibe la conexión #k (reutilizada)»); si no, y hay menos de M creadas en total, se crea una nueva numerada desde 1 («(nueva)»); si no, el cliente espera en una cola FIFO («t: X espera (pool lleno)»). Al devolver: si alguien espera, la conexión pasa al primero de la cola («t: X devuelve #k → pasa a Y (esperó d ms)»); si no, queda libre («t: X devuelve #k (queda libre)»); si el cliente no tenía conexión, «t: Error: X no tiene conexión». Quien lleva esperando más de W ms (su inicio + W es menor que el instante del siguiente evento) se va antes de procesar ese evento: «inicio+W: X agota la espera (timeout)»; al terminar los eventos se agotan los que queden. Termina con «Conexiones creadas: c · Reutilizadas: r · Esperas: e · Timeouts: o · Espera media: m ms» (media entera de las esperas que sí recibieron conexión; 0 si ninguna) y «Al final: c abiertas, l libres, u en uso».

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        String[] conf = sc.nextLine().trim().split(" ");     // «max M espera W»        int max = Integer.parseInt(conf[1]), espera = Integer.parseInt(conf[3]);        List<String[]> eventos = new ArrayList<>();           // {tiempo, accion, cliente}        while (sc.hasNextLine()) {            String l = sc.nextLine().trim();            if (!l.isEmpty()) eventos.add(l.split(" "));        }        // TODO: simular el pool (conexiones libres, en uso, cola de espera y tiempos límite)    }}

Ejemplo: Aplicación web

Entrada

max 2 espera 100
0 pedir web1
5 pedir web2
10 pedir web3
40 devolver web1
45 pedir web4
50 devolver web3
60 pedir web5
70 devolver web2
80 devolver web5
90 pedir web7
200 pedir web6
210 devolver web9
320 devolver web4
330 pedir web8
340 pedir web10

Salida esperada

0: web1 recibe la conexión #1 (nueva)
5: web2 recibe la conexión #2 (nueva)
10: web3 espera (pool lleno)
40: web1 devuelve #1 → pasa a web3 (esperó 30 ms)
45: web4 espera (pool lleno)
50: web3 devuelve #1 → pasa a web4 (esperó 5 ms)
60: web5 espera (pool lleno)
70: web2 devuelve #2 → pasa a web5 (esperó 10 ms)
80: web5 devuelve #2 (queda libre)
90: web7 recibe la conexión #2 (reutilizada)
200: web6 espera (pool lleno)
210: Error: web9 no tiene conexión
300: web6 agota la espera (timeout)
320: web4 devuelve #1 (queda libre)
330: web8 recibe la conexión #1 (reutilizada)
340: web10 espera (pool lleno)
440: web10 agota la espera (timeout)
Conexiones creadas: 2 · Reutilizadas: 2 · Esperas: 5 · Timeouts: 2 · Espera media: 15 ms
Al final: 2 abiertas, 0 libres, 2 en uso

9. Transacciones con puntos de guardado y bloqueos

Muy difícil · Conectores JDBC · apuntesdam.com/subject/acceso-datos/topic/jdbc

Simula dos conexiones JDBC (sesiones A y B) sobre unas cuentas, como haría una base de datos con nivel de aislamiento READ COMMITTED. La primera línea es «cuentas C1=v1 C2=v2 …» y cada línea siguiente «S orden»: BEGIN (abre una transacción: setAutoCommit(false); si ya hay una, «S: ya hay una transacción abierta»); SELECT c («S ve c = v»: el valor propio pendiente si la sesión lo ha cambiado en su transacción y, si no, el confirmado: nunca ve lo no confirmado de la otra); UPDATE c ±n (si la otra sesión tiene c cambiada en una transacción abierta, «S: UPDATE c bloqueado por O (se cancela la sentencia)»; si el saldo quedaría negativo, «S: UPDATE c rechazado: el saldo quedaría en v»; si no, sin transacción se confirma al momento «S: c = v (confirmado)» y dentro de una, «S: c = v»); SAVEPOINT nombre; ROLLBACK TO nombre (vuelve al estado del punto y borra los puntos posteriores; «S: vuelta a nombre» o «S: no existe el punto de guardado nombre»); COMMIT («S: COMMIT (n cambios)», n = cuentas distintas cambiadas) y ROLLBACK («S: ROLLBACK (n cambios descartados)»). COMMIT, ROLLBACK y SAVEPOINT sin transacción: «S: no hay transacción». Un UPDATE rechazado no deshace la transacción. Al final, por cada sesión con una transacción abierta (A y luego B), «S tenía una transacción abierta: se deshace al cerrar la conexión», y «Confirmado: C1=v1 …» en el orden inicial.

Código de partida (java)
import java.util.*; public class Main {    public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Map<String, Integer> confirmado = new LinkedHashMap<>();   // lo que ve cualquiera        for (String par : sc.nextLine().trim().split(" ")) {            if (!par.contains("=")) continue;                     // «cuentas A1=100 A2=50 …»            String[] kv = par.split("=");            confirmado.put(kv[0], Integer.parseInt(kv[1]));        }        // Por sesión: cambios pendientes (null si no hay transacción) y puntos de guardado        Map<String, Map<String, Integer>> pendientes = new HashMap<>();        Map<String, LinkedHashMap<String, Map<String, Integer>>> puntos = new HashMap<>();        // TODO: procesar las órdenes de las sesiones A y B    }}

Ejemplo: Transferencia con comisión

Entrada

cuentas A1=100 A2=50 A3=0
A BEGIN
A UPDATE A1 -30
A UPDATE A2 +30
B SELECT A1
A SELECT A1
B UPDATE A1 +5
B UPDATE A3 +10
A SAVEPOINT antes_comision
A UPDATE A2 -100
A UPDATE A3 -2
A ROLLBACK TO antes_comision
A UPDATE A2 -1
A COMMIT
B SELECT A1
B UPDATE A1 +5
B BEGIN
B UPDATE A2 -79
B SELECT A2
A SELECT A2

Salida esperada

A: BEGIN
A: A1 = 70
A: A2 = 80
B ve A1 = 100
A ve A1 = 70
B: UPDATE A1 bloqueado por A (se cancela la sentencia)
B: A3 = 10 (confirmado)
A: SAVEPOINT antes_comision
A: UPDATE A2 rechazado: el saldo quedaría en -20
A: A3 = 8
A: vuelta a antes_comision
A: A2 = 79
A: COMMIT (2 cambios)
B ve A1 = 70
B: A1 = 75 (confirmado)
B: BEGIN
B: A2 = 0
B ve A2 = 0
A ve A2 = 79
B tenía una transacción abierta: se deshace al cerrar la conexión
Confirmado: A1=75 A2=79 A3=10

Mapeo Objeto-Relacional (Hibernate/JPA)

10. El problema N+1 y las estrategias de carga

Medio · Mapeo Objeto-Relacional (Hibernate/JPA) · apuntesdam.com/subject/acceso-datos/topic/hibernate

Con JPA, una consulta inocente como «SELECT a FROM Autor a» seguida de a.getLibros() en un bucle puede lanzar una consulta por autor: el problema N+1. Simula lo que hace Hibernate. La entrada tiene líneas «autor id nombre», «libro id idAutor título» y «estrategia E». Se ejecuta la consulta de autores (ordenados por id) y después, para cada autor, se accede a sus libros (ordenados por id) y se muestra «Nombre: n libros (Título, Título)» («1 libro», o «0 libros» sin paréntesis). Muestra cada SQL en el momento en que se lanza: «SQL: select * from autor order by id» al principio y, según la estrategia: LAZY, «SQL: select * from libro where autor_id = X» justo antes de usar los libros de cada autor; EAGER, esas mismas consultas pero todas seguidas tras la primera (con JPQL, EAGER no evita el N+1); JOIN FETCH, una única consulta «SQL: select * from autor a left join libro l on l.autor_id = a.id order by a.id» que lo trae todo; BATCH k, al necesitar los libros de un autor no cargado se cargan los de hasta k autores no cargados a partir de él: «SQL: select * from libro where autor_id in (1, 2)»; SUBSELECT, la primera vez se cargan los de todos: «SQL: select * from libro where autor_id in (select id from autor)». Termina con «Consultas: n (estrategia E)».

Código de partida (java)
import java.util.*; public class Main {    record Autor(int id, String nombre) { }    record Libro(int id, int autorId, String titulo) { }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        List<Autor> autores = new ArrayList<>();        List<Libro> libros = new ArrayList<>();        String estrategia = "LAZY";        while (sc.hasNextLine()) {            String l = sc.nextLine().trim();            if (l.startsWith("autor ")) {                String[] p = l.split(" ", 3);                autores.add(new Autor(Integer.parseInt(p[1]), p[2]));            } else if (l.startsWith("libro ")) {                String[] p = l.split(" ", 4);                libros.add(new Libro(Integer.parseInt(p[1]), Integer.parseInt(p[2]), p[3]));            } else if (l.startsWith("estrategia ")) {                estrategia = l.substring(11).trim();            }        }        autores.sort(Comparator.comparingInt(Autor::id));        libros.sort(Comparator.comparingInt(Libro::id));        // TODO: simular «SELECT a FROM Autor a» y el acceso a a.getLibros() de cada autor    }}

Ejemplo: Estrategia LAZY

Entrada

autor 1 Cervantes
autor 2 Galdós
autor 3 Emilia Pardo Bazán
autor 4 Machado
autor 5 Lorca
libro 10 1 Don Quijote
libro 11 2 Fortunata y Jacinta
libro 12 1 Novelas ejemplares
libro 13 3 Los pazos de Ulloa
libro 14 5 Bodas de sangre
libro 15 5 Yerma
libro 16 2 Misericordia
estrategia LAZY

Salida esperada

SQL: select * from autor order by id
SQL: select * from libro where autor_id = 1
Cervantes: 2 libros (Don Quijote, Novelas ejemplares)
SQL: select * from libro where autor_id = 2
Galdós: 2 libros (Fortunata y Jacinta, Misericordia)
SQL: select * from libro where autor_id = 3
Emilia Pardo Bazán: 1 libro (Los pazos de Ulloa)
SQL: select * from libro where autor_id = 4
Machado: 0 libros
SQL: select * from libro where autor_id = 5
Lorca: 2 libros (Bodas de sangre, Yerma)
Consultas: 6 (estrategia LAZY)

Ejemplo: Estrategia JOIN FETCH

Entrada

autor 1 Cervantes
autor 2 Galdós
autor 3 Emilia Pardo Bazán
autor 4 Machado
autor 5 Lorca
libro 10 1 Don Quijote
libro 11 2 Fortunata y Jacinta
libro 12 1 Novelas ejemplares
libro 13 3 Los pazos de Ulloa
libro 14 5 Bodas de sangre
libro 15 5 Yerma
libro 16 2 Misericordia
estrategia JOIN FETCH

Salida esperada

SQL: select * from autor a left join libro l on l.autor_id = a.id order by a.id
Cervantes: 2 libros (Don Quijote, Novelas ejemplares)
Galdós: 2 libros (Fortunata y Jacinta, Misericordia)
Emilia Pardo Bazán: 1 libro (Los pazos de Ulloa)
Machado: 0 libros
Lorca: 2 libros (Bodas de sangre, Yerma)
Consultas: 1 (estrategia JOIN FETCH)

Ejemplo: Estrategia BATCH 2

Entrada

autor 1 Cervantes
autor 2 Galdós
autor 3 Emilia Pardo Bazán
autor 4 Machado
autor 5 Lorca
libro 10 1 Don Quijote
libro 11 2 Fortunata y Jacinta
libro 12 1 Novelas ejemplares
libro 13 3 Los pazos de Ulloa
libro 14 5 Bodas de sangre
libro 15 5 Yerma
libro 16 2 Misericordia
estrategia BATCH 2

Salida esperada

SQL: select * from autor order by id
SQL: select * from libro where autor_id in (1, 2)
Cervantes: 2 libros (Don Quijote, Novelas ejemplares)
Galdós: 2 libros (Fortunata y Jacinta, Misericordia)
SQL: select * from libro where autor_id in (3, 4)
Emilia Pardo Bazán: 1 libro (Los pazos de Ulloa)
Machado: 0 libros
SQL: select * from libro where autor_id in (5)
Lorca: 2 libros (Bodas de sangre, Yerma)
Consultas: 4 (estrategia BATCH 2)

11. Los estados de una entidad JPA

Difícil · Mapeo Objeto-Relacional (Hibernate/JPA) · apuntesdam.com/subject/acceso-datos/topic/hibernate

En JPA una entidad puede estar NUEVA (transient), GESTIONADA (managed: el EntityManager vigila sus cambios), SEPARADA (detached) o ELIMINADA (removed), y el SQL no se lanza cuando llamas a los métodos sino en el flush. Simula un EntityManager con una única tabla por entidad e ids IDENTITY (1, 2, 3…). Órdenes: «new v Clase c1=v1,c2=v2» («v: NUEVA (transient)»); «persist v» (NUEVA → inserta al momento: «SQL: insert into clase (c1, c2) values ('v1', 'v2') → id N» y pasa a GESTIONADA; si ya lo está, «v ya está gestionada: no se hace nada»; si está separada, «PersistentObjectException: v está separada (usa merge)»); «set v campo=valor» (si no está gestionada, «v (estado): el cambio solo está en memoria»); «find v Clase id» (si esa fila ya está gestionada en el contexto, se devuelve la misma instancia sin SQL: «v es la misma instancia que w (caché de primer nivel, sin SQL)», con w la primera variable por orden alfabético que la apunta; si no, «SQL: select * from clase where id = N» y «v: GESTIONADA {campos}» o «v = null»); «detach v»; «merge v w» (copia el estado de w en la instancia gestionada con su id, cargándola con un select si no está en el contexto; w no cambia: «v: copia gestionada de w (w sigue separada)»); «remove v» (solo gestionadas: «v: ELIMINADA (se borrará en el flush)»; si no, «IllegalArgumentException: v no está gestionada»); «flush» y «commit» (recorren el contexto en orden: las eliminadas lanzan «SQL: delete …» y dejan de estar en el contexto; las gestionadas con campos distintos de su copia del último flush lanzan «SQL: update clase set c = 'v', … where id = N» solo con los campos cambiados; después «flush: n sentencias» o «COMMIT (n sentencias en el flush)», con «1 sentencia» en singular); «close» («Contexto cerrado: n entidades pasan a SEPARADA») y «estado v».

Código de partida (java)
import java.util.*; public class Main {    static class Entidad {        Long id;        String tabla;        Map<String, String> campos = new LinkedHashMap<>();        String estado = "NUEVA";                   // NUEVA, GESTIONADA, SEPARADA, ELIMINADA        Map<String, String> instantanea;           // copia al pasar a gestionada o tras el último flush    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        Map<String, Entidad> variables = new HashMap<>();        Map<Long, Map<String, String>> baseDatos = new TreeMap<>();        List<Entidad> contexto = new ArrayList<>();    // contexto de persistencia (en orden de entrada)        long[] siguienteId = { 1 };        while (sc.hasNextLine()) {            String linea = sc.nextLine().trim();            if (linea.isEmpty()) continue;            String[] p = linea.split(" ", 3);            // TODO: new, persist, set, find, detach, merge, remove, flush, commit, close y estado        }    }}

Ejemplo: Ciclo de vida completo

Entrada

new e1 Alumno nombre=Ana,nota=6
persist e1
set e1 nota=8
set e1 nota=6
flush
set e1 nota=9
find e2 Alumno 1
commit
detach e1
set e1 nombre=Ana María
commit
persist e1
merge e3 e1
estado e3
commit
remove e1
remove e3
commit
find e4 Alumno 1

Salida esperada

e1: NUEVA (transient)
SQL: insert into alumno (nombre, nota) values ('Ana', '6') → id 1
flush: 0 sentencias
e2 es la misma instancia que e1 (caché de primer nivel, sin SQL)
SQL: update alumno set nota = '9' where id = 1
COMMIT (1 sentencia en el flush)
e1: SEPARADA
e1 (separada): el cambio solo está en memoria
COMMIT (0 sentencias en el flush)
PersistentObjectException: e1 está separada (usa merge)
SQL: select * from alumno where id = 1
e3: copia gestionada de e1 (e1 sigue separada)
e3: GESTIONADA id=1 {nombre=Ana María, nota=9}
SQL: update alumno set nombre = 'Ana María' where id = 1
COMMIT (1 sentencia en el flush)
IllegalArgumentException: e1 no está gestionada
e3: ELIMINADA (se borrará en el flush)
SQL: delete from alumno where id = 1
COMMIT (1 sentencia en el flush)
SQL: select * from alumno where id = 1
e4 = null

BD Objeto-Relacionales y Orientadas a Objetos

12. Arrays y tipos compuestos de PostgreSQL

Medio · BD Objeto-Relacionales y Orientadas a Objetos · apuntesdam.com/subject/acceso-datos/topic/bd-objeto-relacionales

PostgreSQL permite columnas de tipo array (text[]) y tipos compuestos (CREATE TYPE direccion AS (calle text, cp text, ciudad text)). Si se leen por JDBC con getString, llegan en su formato de texto y hay que interpretarlo. Cada línea de la entrada es «array {…}» o «compuesto (…)»; implementa leerArray y leerCompuesto, que devuelven la lista de valores (null para los nulos). Array: elementos separados por comas; un elemento entre comillas dobles puede contener comas y espacios (los espacios de fuera de las comillas se ignoran) y dentro de él \\ escapa el carácter siguiente (\" es una comilla); sin comillas se quitan los espacios de alrededor y NULL (en cualquier combinación de mayúsculas) es nulo, pero "NULL" entre comillas es el texto NULL; {} es un array vacío. Compuesto: campos separados por comas; un campo vacío sin comillas es nulo y "" es una cadena vacía; entre comillas, "" o \" es una comilla; sin comillas el texto se toma tal cual. El main ya muestra cada resultado como «tipo ["a", null, …] · n valores · nulos: k».

Código de partida (java)
import java.util.*; public class Main {    static String json(List<String> valores) {        List<String> partes = new ArrayList<>();        for (String v : valores)            partes.add(v == null ? "null" : "\"" + v.replace("\\", "\\\\").replace("\"", "\\\"") + "\"");        return "[" + String.join(", ", partes) + "]";    }     static List<String> leerArray(String texto) {        // TODO: elementos separados por comas, comillas con escapes y NULL        return new ArrayList<>();    }     static List<String> leerCompuesto(String texto) {        // TODO: campos separados por comas, vacío = nulo y comillas dobladas        return new ArrayList<>();    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String l = sc.nextLine();            if (l.isBlank()) continue;            String tipo = l.substring(0, l.indexOf(' ')), texto = l.substring(l.indexOf(' ') + 1).trim();            List<String> v = tipo.equals("array") ? leerArray(texto) : leerCompuesto(texto);            System.out.println(tipo + " " + json(v) + " · " + v.size() + (v.size() == 1 ? " valor" : " valores")                + " · nulos: " + v.stream().filter(Objects::isNull).count());        }    }}

Ejemplo: Arrays y compuestos

Entrada

array {Lunes,Martes,"Fin de semana",NULL}
array {}
array {"NULL",null, 3 , "con \"comillas\" y \\barra"}
compuesto ("Calle Mayor, 3",28013,Madrid,)
compuesto (,"",abc)
compuesto ("dijo ""hola""",x)

Salida esperada

array ["Lunes", "Martes", "Fin de semana", null] · 4 valores · nulos: 1
array [] · 0 valores · nulos: 0
array ["NULL", null, "3", "con \"comillas\" y \\barra"] · 4 valores · nulos: 1
compuesto ["Calle Mayor, 3", "28013", "Madrid", null] · 4 valores · nulos: 1
compuesto [null, "", "abc"] · 3 valores · nulos: 1
compuesto ["dijo \"hola\"", "x"] · 2 valores · nulos: 0

13. Profundidad de activación en una base de objetos

Difícil · BD Objeto-Relacionales y Orientadas a Objetos · apuntesdam.com/subject/acceso-datos/topic/bd-objeto-relacionales

Las bases de datos orientadas a objetos (db4o, ObjectDB) guardan grafos de objetos con referencias entre ellos. Al recuperar un objeto no cargan todo el grafo: lo «activan» solo hasta cierta profundidad, y los objetos más lejanos quedan como referencias sin rellenar. Las líneas «objeto ID Clase campo=valor ref=@ID …» definen el almacén (los valores con @ son referencias a otros objetos, que pueden formar ciclos). Implementa activar para que «activar ID k» muestre el árbol: con profundidad ≥ 1 el objeto se activa y se muestra «Clase ID {campo=valor, …}» y después cada referencia, en orden, con dos espacios más de sangría y el prefijo «campo → », activada con profundidad k−1; con profundidad 0 se muestra «Clase ID (sin activar)»; si el id no existe, «null (ID no existe)». El main cuenta los objetos distintos activados.

Código de partida (java)
import java.util.*; public class Main {    static class Objeto {        String id, clase;        Map<String, String> datos = new LinkedHashMap<>();       // campos simples        Map<String, String> referencias = new LinkedHashMap<>(); // campo → id del objeto apuntado    }     static Map<String, Objeto> almacen = new LinkedHashMap<>();    static Set<String> activados = new HashSet<>();     static void activar(String id, int profundidad, String sangria, String prefijo) {        // TODO: mostrar el objeto y activar sus referencias con profundidad - 1    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String[] p = sc.nextLine().trim().split(" ");            if (p[0].equals("objeto")) {                     // objeto ID Clase campo=valor ref=@ID …                Objeto o = new Objeto();                o.id = p[1]; o.clase = p[2];                for (int i = 3; i < p.length; i++) {                    String[] kv = p[i].split("=");                    if (kv[1].startsWith("@")) o.referencias.put(kv[0], kv[1].substring(1));                    else o.datos.put(kv[0], kv[1]);                }                almacen.put(o.id, o);            } else if (p[0].equals("activar")) {           // activar ID profundidad                activados.clear();                System.out.println("== activar " + p[1] + " con profundidad " + p[2]);                activar(p[1], Integer.parseInt(p[2]), "", "");                System.out.println("Objetos leídos del disco: " + activados.size());            }        }    }}

Ejemplo: Profundidad 1

Entrada

objeto C1 Curso nombre=1DAM tutor=@P1 delegado=@A1
objeto P1 Profesor nombre=Ana departamento=@D1
objeto D1 Departamento nombre=Informática jefe=@P1
objeto A1 Alumno nombre=Luis grupo=@C1 mentor=@A9
activar C1 1

Salida esperada

== activar C1 con profundidad 1
Curso C1 {nombre=1DAM}
  tutor → Profesor P1 (sin activar)
  delegado → Alumno A1 (sin activar)
Objetos leídos del disco: 1

Ejemplo: Profundidad 3 con ciclo

Entrada

objeto C1 Curso nombre=1DAM tutor=@P1 delegado=@A1
objeto P1 Profesor nombre=Ana departamento=@D1
objeto D1 Departamento nombre=Informática jefe=@P1
objeto A1 Alumno nombre=Luis grupo=@C1 mentor=@A9
activar C1 3

Salida esperada

== activar C1 con profundidad 3
Curso C1 {nombre=1DAM}
  tutor → Profesor P1 {nombre=Ana}
    departamento → Departamento D1 {nombre=Informática}
      jefe → Profesor P1 (sin activar)
  delegado → Alumno A1 {nombre=Luis}
    grupo → Curso C1 {nombre=1DAM}
      tutor → Profesor P1 (sin activar)
      delegado → Alumno A1 (sin activar)
    mentor → null (A9 no existe)
Objetos leídos del disco: 4

14. Herencia de tablas en PostgreSQL

Muy difícil · BD Objeto-Relacionales y Orientadas a Objetos · apuntesdam.com/subject/acceso-datos/topic/bd-objeto-relacionales

PostgreSQL permite que una tabla herede de otra (CREATE TABLE alumno (curso text) INHERITS (persona)): la hija tiene las columnas del padre más las suyas y, al consultar el padre, también salen las filas de las hijas. Pero las restricciones UNIQUE (y las claves primarias) no se heredan entre tablas: cada tabla solo comprueba sus propias filas. Simúlalo. Órdenes: «tabla t columnas=a,b [hereda=padre] [unico=col]» (muestra «CREATE TABLE t (todas las columnas) [INHERITS (padre)] [UNIQUE (col)]»; las columnas heredadas van primero); «insertar t a=1,b=2» (columnas no indicadas a NULL; si una no existe, «ERROR: la columna x no existe en t»; si viola el único de esa tabla, comparando solo con las filas de esa misma tabla, «ERROR: valor duplicado col=v en t»; si no, «INSERT en t»); «consultar t» y «consultar solo t» (como SELECT * FROM t y SELECT * FROM ONLY t). La consulta muestra «SELECT * FROM [ONLY ]t (col | col …)», cada fila con dos espacios delante y sus valores para las columnas de t separados por « | » (NULL si no tiene), añadiendo « [hija]» a las filas que vienen de otra tabla, y al final « n filas» («1 fila»). Primero van las filas propias y después, sin ONLY, las de cada hija en orden de creación (y las de sus hijas, recursivamente).

Código de partida (java)
import java.util.*; public class Main {    static class Tabla {        String nombre, padre, unico;        List<String> columnas = new ArrayList<>();        List<Map<String, String>> filas = new ArrayList<>();        List<String> hijas = new ArrayList<>();    }     static Map<String, Tabla> tablas = new LinkedHashMap<>();     // TODO: static void recoger(Tabla consultada, Tabla t, boolean solo, List<String> salida)     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String l = sc.nextLine().trim();            if (l.isEmpty()) continue;            String[] p = l.split(" ");            // TODO: tabla, insertar, consultar y consultar solo        }    }}

Ejemplo: Personas, alumnos y profesores

Entrada

tabla persona columnas=dni,nombre unico=dni
tabla alumno columnas=curso hereda=persona unico=dni
tabla profesor columnas=departamento hereda=persona
tabla delegado columnas=desde hereda=alumno
insertar persona dni=111,nombre=Conserje
insertar alumno dni=222,nombre=Ana,curso=1DAM
insertar alumno dni=222,nombre=Otra,curso=2DAM
insertar alumno dni=111,nombre=Luis,curso=1DAM
insertar profesor dni=333,nombre=Marta,departamento=Informática
insertar delegado dni=444,nombre=Eva,curso=2DAM,desde=2025
insertar profesor dni=555,nombre=Pepe,curso=1DAM
insertar persona dni=111,nombre=Duplicado
consultar persona
consultar solo persona
consultar alumno

Salida esperada

CREATE TABLE persona (dni, nombre) UNIQUE (dni)
CREATE TABLE alumno (dni, nombre, curso) INHERITS (persona) UNIQUE (dni)
CREATE TABLE profesor (dni, nombre, departamento) INHERITS (persona)
CREATE TABLE delegado (dni, nombre, curso, desde) INHERITS (alumno)
INSERT en persona
INSERT en alumno
ERROR: valor duplicado dni=222 en alumno
INSERT en alumno
INSERT en profesor
INSERT en delegado
ERROR: la columna curso no existe en profesor
ERROR: valor duplicado dni=111 en persona
SELECT * FROM persona (dni | nombre)
  111 | Conserje
  222 | Ana   [alumno]
  111 | Luis   [alumno]
  444 | Eva   [delegado]
  333 | Marta   [profesor]
  5 filas
SELECT * FROM ONLY persona (dni | nombre)
  111 | Conserje
  1 fila
SELECT * FROM alumno (dni | nombre | curso)
  222 | Ana | 1DAM
  111 | Luis | 1DAM
  444 | Eva | 2DAM   [delegado]
  3 filas

Componentes de Acceso a Datos

15. Propiedad ligada (bound property)

Fácil · Componentes de Acceso a Datos · apuntesdam.com/subject/acceso-datos/topic/componentes-acceso-datos

Completa el JavaBean Termostato para que su propiedad temperatura sea ligada: cada vez que cambie de valor debe avisar a los oyentes con firePropertyChange. El programa lee temperaturas por teclado y los oyentes imprimen cada cambio (si el valor no cambia, no se debe notificar).

Código de partida (java)
import java.beans.*;import java.io.Serializable;import java.util.Scanner; public class Main {     public static class Termostato implements Serializable {        private final PropertyChangeSupport soporte = new PropertyChangeSupport(this);        private int temperatura = 20;         public Termostato() {}         public int getTemperatura() { return temperatura; }         public void setTemperatura(int nueva) {            // TODO: asigna el nuevo valor y avisa a los oyentes del cambio            temperatura = nueva;        }         public void addPropertyChangeListener(PropertyChangeListener l) { soporte.addPropertyChangeListener(l); }        public void removePropertyChangeListener(PropertyChangeListener l) { soporte.removePropertyChangeListener(l); }    }     public static void main(String[] args) {        Termostato t = new Termostato();        t.addPropertyChangeListener(e ->            System.out.println(e.getPropertyName() + ": " + e.getOldValue() + " -> " + e.getNewValue()));         Scanner sc = new Scanner(System.in);        while (sc.hasNextInt()) t.setTemperatura(sc.nextInt());        System.out.println("Final: " + t.getTemperatura());    }}

Ejemplo: Tres cambios

Entrada

22 25 18

Salida esperada

temperatura: 20 -> 22
temperatura: 22 -> 25
temperatura: 25 -> 18
Final: 18

Ejemplo: Valor repetido: no se notifica

Entrada

20 21 21

Salida esperada

temperatura: 20 -> 21
Final: 21

16. Propiedad restringida (constrained)

Medio · Componentes de Acceso a Datos · apuntesdam.com/subject/acceso-datos/topic/componentes-acceso-datos

El componente CuentaBancaria tiene una propiedad saldo restringida: antes de cambiarla pregunta a los oyentes con fireVetoableChange, y cualquiera puede vetar el cambio lanzando PropertyVetoException. Completa setSaldo. El programa registra un oyente que veta saldos negativos y aplica los movimientos leídos por teclado.

Código de partida (java)
import java.beans.*;import java.util.Scanner; public class Main {     public static class CuentaBancaria {        private final VetoableChangeSupport vetos = new VetoableChangeSupport(this);        private double saldo = 100;         public double getSaldo() { return saldo; }         public void setSaldo(double nuevo) throws PropertyVetoException {            // TODO: pregunta a los oyentes y, si nadie veta, cambia el saldo            saldo = nuevo;        }         public void addVetoableChangeListener(VetoableChangeListener l) { vetos.addVetoableChangeListener(l); }    }     public static void main(String[] args) {        CuentaBancaria cuenta = new CuentaBancaria();        cuenta.addVetoableChangeListener(e -> {            if ((double) e.getNewValue() < 0) throw new PropertyVetoException("saldo negativo", e);        });         Scanner sc = new Scanner(System.in);        while (sc.hasNextInt()) {            int movimiento = sc.nextInt();            try {                cuenta.setSaldo(cuenta.getSaldo() + movimiento);                System.out.println("OK " + movimiento + " -> " + cuenta.getSaldo());            } catch (PropertyVetoException ex) {                System.out.println("Vetado " + movimiento + ": " + ex.getMessage());            }        }    }}

Ejemplo: Ingreso y retirada válidos

Entrada

50 -120

Salida esperada

OK 50 -> 150.0
OK -120 -> 30.0

Ejemplo: Retirada que deja saldo negativo

Entrada

-150 20

Salida esperada

Vetado -150: saldo negativo
OK 20 -> 120.0

17. Persistencia de un JavaBean en XML

Medio · Componentes de Acceso a Datos · apuntesdam.com/subject/acceso-datos/topic/componentes-acceso-datos

Los JavaBeans pueden guardarse en XML con XMLEncoder y recuperarse con XMLDecoder, siempre que sigan las convenciones (constructor vacío, getters y setters). Completa guardar() y cargar() para el bean Configuracion. El programa lee el tema y el tamaño de letra por teclado, guarda el bean en config.xml, lo vuelve a cargar y muestra sus valores.

Código de partida (java)
import java.beans.*;import java.io.*;import java.util.Scanner; public class Main {     public static class Configuracion {        private String tema = "claro";        private int tamanoLetra = 14;         public Configuracion() {}        public String getTema() { return tema; }        public void setTema(String tema) { this.tema = tema; }        public int getTamanoLetra() { return tamanoLetra; }        public void setTamanoLetra(int t) { this.tamanoLetra = t; }    }     static void guardar(Configuracion c, File fichero) throws IOException {        // TODO: guarda el bean en XML con XMLEncoder    }     static Configuracion cargar(File fichero) throws IOException {        // TODO: recupera el bean con XMLDecoder        return new Configuracion();    }     public static void main(String[] args) throws IOException {        Scanner sc = new Scanner(System.in);        Configuracion c = new Configuracion();        c.setTema(sc.next());        c.setTamanoLetra(sc.nextInt());         File f = new File("config.xml");        guardar(c, f);        Configuracion leida = cargar(f);        System.out.println(leida.getTema() + " " + leida.getTamanoLetra());        System.out.println("XML generado: " + (f.length() > 0));    }}

Ejemplo: Tema oscuro, letra 16

Entrada

oscuro 16

Salida esperada

oscuro 16
XML generado: true

18. Componente DAO con eventos

Difícil · Componentes de Acceso a Datos · apuntesdam.com/subject/acceso-datos/topic/componentes-acceso-datos

Implementa AlumnoDaoMemoria, un componente de acceso a datos que cumple la interfaz AlumnoDao y además avisa a sus oyentes (DaoListener) cada vez que se guarda o se borra un alumno. El programa ejecuta las órdenes leídas por teclado: 'G nombre nota' guarda, 'B nombre' borra y 'L' lista los alumnos ordenados por nombre.

Código de partida (java)
import java.util.*; public class Main {     public interface DaoListener { void datosCambiados(String accion, String nombre); }     public interface AlumnoDao {        void guardar(String nombre, double nota);        boolean borrar(String nombre);        Map<String, Double> listar();        void addDaoListener(DaoListener l);    }     public static class AlumnoDaoMemoria implements AlumnoDao {        private final Map<String, Double> alumnos = new TreeMap<>();        private final List<DaoListener> oyentes = new ArrayList<>();         public void guardar(String nombre, double nota) {            // TODO: guarda y avisa a los oyentes        }         public boolean borrar(String nombre) {            // TODO: borra y avisa a los oyentes si existía            return false;        }         public Map<String, Double> listar() { return Collections.unmodifiableMap(alumnos); }         public void addDaoListener(DaoListener l) { oyentes.add(l); }    }     public static void main(String[] args) {        AlumnoDao dao = new AlumnoDaoMemoria();        dao.addDaoListener((accion, nombre) -> System.out.println("[evento] " + accion + " " + nombre));         Scanner sc = new Scanner(System.in);        while (sc.hasNext()) {            String orden = sc.next();            switch (orden) {                case "G" -> dao.guardar(sc.next(), Double.parseDouble(sc.next()));                case "B" -> { String n = sc.next(); if (!dao.borrar(n)) System.out.println("No existe " + n); }                case "L" -> dao.listar().forEach((n, nota) -> System.out.println(n + " " + nota));                default -> System.out.println("Orden desconocida");            }        }    }}

Ejemplo: Guardar y listar

Entrada

G Luis 6 G Ana 8.5 L

Salida esperada

[evento] guardado Luis
[evento] guardado Ana
Ana 8.5
Luis 6.0

Ejemplo: Borrar existente e inexistente

Entrada

G Ana 7 B Ana B Pepe L

Salida esperada

[evento] guardado Ana
[evento] borrado Ana
No existe Pepe

19. Un contenedor de inyección de dependencias

Muy difícil · Componentes de Acceso a Datos · apuntesdam.com/subject/acceso-datos/topic/componentes-acceso-datos

Spring, Jakarta CDI o Micronaut son contenedores de componentes: el programa declara qué componentes hay y de qué dependen, y el contenedor los crea en el orden correcto y los inyecta. Simula uno. «componente N [depende=A,B] [ambito=prototype]» declara un componente (singleton por defecto: una sola instancia; prototype: una nueva cada vez). «obtener N» muestra «obtener N» y antes de crear nada valida el grafo recorriendo las dependencias en orden: si falta un componente, « Error: no existe el componente N» (el pedido) o « Error: falta el componente X (lo necesita Y)»; si hay un ciclo, « Error: dependencia circular A → B → C → A» (desde la primera aparición). Si es válido, crea primero las dependencias (en orden, recursivamente) y después el componente, numerando las instancias globalmente desde 1: « crear X#k» (con « (prototype)» si lo es); los singletons ya creados se reutilizan sin mostrar nada. Termina con «→ N#k» y « (ya existía)» si era un singleton ya creado. «cerrar» destruye los singletons en orden inverso a su creación (« destruir X#k»; los prototype no los gestiona el contenedor), los olvida y muestra «Contenedor cerrado».

Código de partida (java)
import java.util.*; public class Main {    static Map<String, List<String>> dependencias = new LinkedHashMap<>();    static Map<String, String> ambitos = new HashMap<>();    static Map<String, Integer> singletons = new HashMap<>();   // componente → número de instancia    static List<String> creados = new ArrayList<>();            // singletons en orden de creación (para cerrar)    static int contador = 0;     // TODO: static String validar(String nombre, List<String> camino, String quien)    //       static int resolver(String nombre)     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        while (sc.hasNextLine()) {            String[] p = sc.nextLine().trim().split(" ");            switch (p[0]) {                case "componente" -> {                      // componente N [depende=A,B] [ambito=prototype]                    List<String> deps = new ArrayList<>();                    String ambito = "singleton";                    for (int i = 2; i < p.length; i++) {                        if (p[i].startsWith("depende=")) deps.addAll(List.of(p[i].substring(8).split(",")));                        if (p[i].startsWith("ambito=")) ambito = p[i].substring(7);                    }                    dependencias.put(p[1], deps);                    ambitos.put(p[1], ambito);                }                case "obtener" -> {                    System.out.println("obtener " + p[1]);                    // TODO: validar y, si no hay error, resolver                }                case "cerrar" -> {                    // TODO: destruir los singletons en orden inverso al de creación                    System.out.println("Contenedor cerrado");                }            }        }    }}

Ejemplo: Tienda

Entrada

componente DataSource
componente PedidoDao depende=DataSource
componente ClienteDao depende=DataSource
componente Correo ambito=prototype
componente Notificador depende=Correo
componente PedidoService depende=PedidoDao,ClienteDao,Notificador
componente Informe depende=PedidoDao,Correo ambito=prototype
obtener PedidoService
obtener PedidoService
obtener Informe
obtener Informe
obtener Factura
cerrar
obtener ClienteDao

Salida esperada

obtener PedidoService
  crear DataSource#1
  crear PedidoDao#2
  crear ClienteDao#3
  crear Correo#4 (prototype)
  crear Notificador#5
  crear PedidoService#6
→ PedidoService#6
obtener PedidoService
→ PedidoService#6 (ya existía)
obtener Informe
  crear Correo#7 (prototype)
  crear Informe#8 (prototype)
→ Informe#8
obtener Informe
  crear Correo#9 (prototype)
  crear Informe#10 (prototype)
→ Informe#10
obtener Factura
  Error: no existe el componente Factura
  destruir PedidoService#6
  destruir Notificador#5
  destruir ClienteDao#3
  destruir PedidoDao#2
  destruir DataSource#1
Contenedor cerrado
obtener ClienteDao
  crear DataSource#11
  crear ClienteDao#12
→ ClienteDao#12

Ejercicios de Acceso a Datos

20. Filtrar un fichero de log

Fácil · Ejercicios de Acceso a Datos · apuntesdam.com/subject/acceso-datos/topic/ejercicios-acceso-datos

El programa guarda en app.log las líneas recibidas por teclado. Completa filtrar() para que lea app.log y escriba en errores.log solo las líneas que contienen "ERROR", devolviendo cuántas ha copiado. Después se muestra el número y el contenido de errores.log.

Código de partida (java)
import java.io.*;import java.nio.file.*;import java.util.*; public class Main {     static int filtrar(Path origen, Path destino) throws IOException {        // TODO: copia a destino las líneas de origen que contienen "ERROR"        return 0;    }     public static void main(String[] args) throws IOException {        Path log = Path.of("app.log");        Files.write(log, new String(System.in.readAllBytes()).lines().toList());         Path errores = Path.of("errores.log");        int n = filtrar(log, errores);        System.out.println("Errores: " + n);        if (Files.exists(errores)) Files.readAllLines(errores).forEach(System.out::println);    }}

Ejemplo: Dos errores

Entrada

10:00 INFO arranque
10:01 ERROR conexión perdida
10:02 WARN reintento
10:03 ERROR tiempo agotado

Salida esperada

Errores: 2
10:01 ERROR conexión perdida
10:03 ERROR tiempo agotado

Ejemplo: Sin errores

Entrada

INFO todo bien
INFO fin

Salida esperada

Errores: 0

21. Conversor de CSV a JSON

Medio · Ejercicios de Acceso a Datos · apuntesdam.com/subject/acceso-datos/topic/ejercicios-acceso-datos

Convierte un fichero CSV con cabecera (nombre;ciudad;edad) en un array JSON, sin librerías externas. El programa guarda la entrada en personas.csv; completa convertir() para que devuelva el JSON con el formato exacto del ejemplo: [{"nombre":"Ana","ciudad":"Logroño","edad":20}, …] (la edad sin comillas, porque es un número).

Código de partida (java)
import java.io.*;import java.nio.file.*;import java.util.*; public class Main {     static String convertir(Path csv) throws IOException {        List<String> lineas = Files.readAllLines(csv);        String[] cabecera = lineas.get(0).split(";");        List<String> objetos = new ArrayList<>();        // TODO: recorre las líneas de datos y construye un objeto JSON por línea         return "[" + String.join(",", objetos) + "]";    }     public static void main(String[] args) throws IOException {        Path csv = Path.of("personas.csv");        Files.write(csv, new String(System.in.readAllBytes()).lines().toList());        System.out.println(convertir(csv));    }}

Ejemplo: Dos personas

Entrada

nombre;ciudad;edad
Ana;Logroño;20
Luis;Arnedo;22

Salida esperada

[{"nombre":"Ana","ciudad":"Logroño","edad":20},{"nombre":"Luis","ciudad":"Arnedo","edad":22}]

Ejemplo: Solo cabecera

Entrada

nombre;ciudad;edad

Salida esperada

[]

22. Media de precios por categoría (DOM)

Difícil · Ejercicios de Acceso a Datos · apuntesdam.com/subject/acceso-datos/topic/ejercicios-acceso-datos

El programa guarda el XML recibido en catalogo.xml. Cada <producto> tiene un atributo categoria y un elemento <precio>. Completa medias() para devolver un mapa ordenado categoría → precio medio. Se imprime cada categoría con su media con dos decimales.

Código de partida (java)
import java.io.*;import java.nio.file.*;import java.util.*;import javax.xml.parsers.*;import org.w3c.dom.*; public class Main {     static Map<String, Double> medias(File fichero) throws Exception {        Map<String, Double> resultado = new TreeMap<>();        // TODO: calcula el precio medio de cada categoría         return resultado;    }     public static void main(String[] args) throws Exception {        File f = new File("catalogo.xml");        Files.write(f.toPath(), System.in.readAllBytes());        medias(f).forEach((cat, media) -> System.out.printf(Locale.US, "%s %.2f%n", cat, media));    }}

Ejemplo: Dos categorías

Entrada

<catalogo><producto categoria="audio"><precio>40</precio></producto><producto categoria="video"><precio>100</precio></producto><producto categoria="audio"><precio>50</precio></producto></catalogo>

Salida esperada

audio 45.00
video 100.00

Ejercicios largos

23. Conversor de CSV a JSON con comillas y tipos

Difícil · Java · 75 minutos · apuntesdam.com/ejercicios/java/conversor-csv-a-json

Una academia exporta sus alumnos de una hoja de cálculo en CSV y quiere cargarlos en una aplicación web que solo entiende JSON. Parece trivial (separar por comas), pero los datos reales lo complican: hay apellidos como «López, Juan» que llevan una coma dentro y por eso van entre comillas, y textos que contienen las propias comillas, que en CSV se escriben dobles ("").

Además, en JSON los tipos importan: 8.5 es un número, true un booleano, un campo vacío es null y el resto son textos entre comillas. Hay una trampa clásica: un código postal como 08001 parece un número, pero si se convierte se pierde el cero. En JSON un número no puede empezar por cero, así que esos valores se quedan como texto.

Algunas líneas vienen mal (les faltan campos o tienen comillas sin cerrar). No se convierten, pero el programa dice cuáles eran para poder corregirlas.

Requisitos

  • La primera línea no vacía es la cabecera, con los nombres de los campos. Cada línea siguiente es un registro. Las líneas en blanco se ignoran (pero cuentan para numerar las líneas).

  • Los campos se separan por comas. Un campo puede ir entre comillas dobles, y entonces puede contener comas; dentro de las comillas, "" representa una comilla. A cada campo (y a cada nombre de la cabecera) se le quitan los espacios del principio y del final.

  • Un registro con un número de campos distinto del de la cabecera, o con comillas sin cerrar, no se convierte: se anota su número de línea.

  • Tipos: un campo vacío es null; un campo que cumple el formato de número JSON (signo menos opcional, 0 o una cifra del 1 al 9 seguida de más cifras, y opcionalmente un punto y decimales) se escribe tal cual; true y false son booleanos; todo lo demás es texto entre comillas, con " escrito \", \ escrito \\ y el tabulador escrito \t.

  • Salida: un array JSON con un objeto por registro, en el orden del fichero, con dos espacios de sangría para cada objeto y cuatro para cada propiedad ("campo": valor), comas entre elementos y nada al final de la última línea. Sin registros, [].

  • Después del JSON, Registros: N · Líneas ignoradas: M (l1, l2…), sin el paréntesis si M es 0. Si la entrada no tiene ni la cabecera, solo El fichero está vacío.

Formato de la entrada

  • Un fichero CSV: cabecera y una línea por registro, campos separados por comas y entre comillas cuando hace falta.

Ejemplo: Alumnos de una academia

Entrada

id,nombre,ciudad,cp,nota,beca
1,Ana Gil,Madrid,28001,8.5,true
2,"López, Juan",Barcelona,08001,6,false
3,"Eva ""la rápida"" Ruiz",Sevilla,41001,,true
4,Luis,Valencia,46001,7.25
5,Marta Díaz,Bilbao,48001,-0.5,false

Salida esperada

[
  {
    "id": 1,
    "nombre": "Ana Gil",
    "ciudad": "Madrid",
    "cp": 28001,
    "nota": 8.5,
    "beca": true
  },
  {
    "id": 2,
    "nombre": "López, Juan",
    "ciudad": "Barcelona",
    "cp": "08001",
    "nota": 6,
    "beca": false
  },
  {
    "id": 3,
    "nombre": "Eva \"la rápida\" Ruiz",
    "ciudad": "Sevilla",
    "cp": 41001,
    "nota": null,
    "beca": true
  },
  {
    "id": 5,
    "nombre": "Marta Díaz",
    "ciudad": "Bilbao",
    "cp": 48001,
    "nota": -0.5,
    "beca": false
  }
]
Registros: 4 · Líneas ignoradas: 1 (5)

Ejemplo: Espacios, barras y comillas sin cerrar

Entrada

codigo , descripcion , precio
A1 , "Cable HDMI 2m" , 7.95
A2,"Ruta C:\temp",0
A3,"Sin cerrar,1

A4,,

Salida esperada

[
  {
    "codigo": "A1",
    "descripcion": "Cable HDMI 2m",
    "precio": 7.95
  },
  {
    "codigo": "A2",
    "descripcion": "Ruta C:\\temp",
    "precio": 0
  },
  {
    "codigo": "A4",
    "descripcion": null,
    "precio": null
  }
]
Registros: 3 · Líneas ignoradas: 1 (4)
Código de partida (java)
import java.util.ArrayList;import java.util.List;import java.util.Scanner; public class Main {    /** Campos de una línea CSV separados por comas, con comillas dobles opcionales ("" es una comilla). null si hay comillas sin cerrar. */    static List<String> campos(String linea) {        // TODO        return new ArrayList<>();    }     /** Texto JSON entre comillas, con las comillas, barras y tabuladores escapados. */    static String cadena(String s) {        // TODO        return "\"" + s + "\"";    }     /** El valor JSON de un campo: null, número, booleano o texto. */    static String valor(String campo) {        // TODO        return cadena(campo);    }     public static void main(String[] args) {        Scanner sc = new Scanner(System.in);        // TODO: lee la cabecera y cada línea, y escribe el JSON y el resumen    }}

24. Fichero de productos con registros de tamaño fijo y acceso directo

Muy difícil · Java · 90 minutos · apuntesdam.com/ejercicios/java/fichero-de-registros-con-acceso-directo

Antes de las bases de datos, los programas de gestión guardaban sus datos en ficheros binarios con registros de tamaño fijo. Todavía hoy es la forma de entender cómo trabaja un gestor de bases de datos por dentro: si cada registro ocupa siempre lo mismo, la posición del registro número N se calcula con una multiplicación y se puede ir directamente a él, sin leer los anteriores.

En este fichero cada producto ocupa 77 bytes: el código (int, 4 bytes), el nombre (30 caracteres char de 2 bytes, 60 en total, rellenando con el carácter \0), el precio (double, 8), el stock (int, 4) y una marca de borrado (boolean, 1). El producto con código N vive en el byte (N − 1) × 77: es acceso directo, y por eso puede haber huecos entre productos.

Borrar un registro desplazando todos los siguientes sería lentísimo en un fichero grande, así que las bajas son lógicas: se pone la marca de borrado a true y el hueco queda libre para un alta futura con ese código.

Requisitos

  • El programa crea un fichero temporal vacío y ejecuta una orden por línea (la orden puede ir en minúsculas; las líneas vacías se ignoran; una orden desconocida escribe Orden desconocida).

  • ALTA código;nombre;precio;stock: el código es un entero de 1 a 9999, el nombre no puede estar vacío, el precio es un número no negativo con hasta dos decimales y el stock un entero no negativo (si no, Datos no válidos). Si ya hay un producto activo con ese código, Ya existe el producto N. Si no, se escribe el registro en su posición y se escribe Alta del producto N en el byte B, añadiendo (nombre recortado a 30 caracteres) si el nombre era más largo.

  • PRECIO código precio cambia solo el precio, sin tocar el resto del registro: Precio del producto N: antes → después (o Datos no válidos / No existe el producto N).

  • BAJA código pone la marca de borrado: Baja lógica del producto N, o No existe el producto N si no está activo. VER código escribe N · nombre · precio · S uds o No existe el producto N.

  • LISTAR escribe Productos activos: A y, con dos espacios delante, cada producto activo por orden de código con el formato de VER.

  • INFORME escribe Fichero: T bytes, H huecos de registro, A activos, L libres o borrados · valor del stock X, donde H es el tamaño del fichero entre 77 y el valor es la suma de precio × stock de los activos. Los precios se escriben con dos decimales, coma decimal y €.

  • Un registro está activo si el fichero llega hasta él, el código guardado en él es el suyo (los huecos que se crean al escribir más allá del final están llenos de ceros) y no tiene la marca de borrado.

Formato de la entrada

  • Una orden por línea: ALTA, PRECIO, BAJA, VER, LISTAR o INFORME.

Estructura de un registro (77 bytes)
CampoTipoBytesDesplazamiento
códigoint40
nombre30 × char604
preciodouble864
stockint472
borradoboolean176

Ejemplo: Altas, cambios y bajas

Entrada

ALTA 1;Ratón inalámbrico;9.95;20
ALTA 3;Teclado mecánico;49.90;5
ALTA 1;Repetido;1;1
LISTAR
PRECIO 3 44.50
BAJA 1
VER 1
VER 3
VER 2
INFORME

Salida esperada

Alta del producto 1 en el byte 0
Alta del producto 3 en el byte 154
Ya existe el producto 1
Productos activos: 2
  1 · Ratón inalámbrico · 9,95 € · 20 uds
  3 · Teclado mecánico · 49,90 € · 5 uds
Precio del producto 3: 49,90 € → 44,50 €
Baja lógica del producto 1
No existe el producto 1
3 · Teclado mecánico · 44,50 € · 5 uds
No existe el producto 2
Fichero: 231 bytes, 3 huecos de registro, 1 activos, 2 libres o borrados · valor del stock 222,50 €

Ejemplo: Datos que no valen

Entrada

ALTA 0;Cero;1;1
ALTA 10;;1;1
ALTA 10;Monitor;-5;1
ALTA 10;Monitor;99.999;1
ALTA abc
PRECIO 7 10
BAJA 7
listar
BORRAR 1

Salida esperada

Datos no válidos
Datos no válidos
Datos no válidos
Datos no válidos
Datos no válidos
No existe el producto 7
No existe el producto 7
Productos activos: 0
Orden desconocida
Código de partida (java)
import java.io.File;import java.io.IOException;import java.io.RandomAccessFile;import java.util.Locale;import java.util.Scanner; public class Main {    static final int NOMBRE = 30;                          // caracteres del nombre    static final int TAM = 4 + NOMBRE * 2 + 8 + 4 + 1;     // código + nombre + precio + stock + borrado = 77 bytes     static RandomAccessFile f;     /** Acceso directo: el registro del código N empieza en el byte (N - 1) × 77. */    static long posicion(int codigo) {        return (long) (codigo - 1) * TAM;    }     static String euros(double x) {        return String.format(Locale.ROOT, "%.2f", x).replace('.', ',') + " €";    }     static void orden(String linea) throws IOException {        // TODO: ALTA, PRECIO, BAJA, VER, LISTAR e INFORME con seek, read… y write…    }     public static void main(String[] args) throws IOException {        File fichero = File.createTempFile("productos", ".dat");        fichero.deleteOnExit();        try (RandomAccessFile raf = new RandomAccessFile(fichero, "rw")) {            f = raf;            Scanner sc = new Scanner(System.in);            while (sc.hasNextLine()) {                String linea = sc.nextLine().trim();                if (!linea.isEmpty()) orden(linea);            }        }    }}