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.
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)); }}
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); }}
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); }}
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
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
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 }}
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
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) }}
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
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)
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
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()); } }}
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()); } } }}
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
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()); }}
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; }