Acceso a Datos en DAM: ficheros, JDBC e Hibernate con ejercicios de examen resueltos

Acceso a Datos (AD) es el módulo de segundo de DAM en el que aprendes a guardar y recuperar información desde un programa Java: en ficheros de texto y binarios, en XML, en una base de datos relacional con JDBC y con un ORM como Hibernate. Para aprobarlo necesitas dominar cinco ejercicios tipo: leer y escribir un fichero de texto, trabajar con registros de tamaño fijo en un fichero de acceso aleatorio, recorrer un XML, hacer un CRUD con JDBC usando PreparedStatement y transacciones, y mapear una entidad con Hibernate para consultarla con HQL.
Es el módulo en el que se juntan Programación y Bases de Datos de primero. Por eso, cuando algo falla, a menudo no sabes si el problema está en el código Java, en la consulta SQL o en la configuración. Aquí tienes qué entra, cinco ejercicios resueltos y explicados, los errores que más vemos y cómo preparar el examen.
Qué entra en Acceso a Datos
Acceso a Datos es el módulo 0486 del título de DAM y se da en segundo curso. Las horas y el orden de los temas cambian según la comunidad y el centro, y la parte de bases de datos no relacionales es la que más varía. Estos son los bloques habituales:
| Bloque | Qué te piden | Lo que más falla |
|---|---|---|
| Ficheros | Texto, binarios, serialización de objetos, acceso aleatorio | Rutas relativas y calcular mal el tamaño del registro |
| XML y JSON | Leer y generar XML con DOM o SAX, a veces JAXB o JSON | Los nodos de texto vacíos entre etiquetas |
| Conectores (JDBC) | Conectar a MySQL o PostgreSQL, CRUD, transacciones | Concatenar valores en la consulta SQL |
| Mapeo objeto-relacional | Hibernate o JPA: entidades, sesiones, consultas HQL | Escribir HQL con nombres de tabla en lugar de nombres de clase |
| Bases de datos no relacionales | MongoDB en la mayoría de centros; en temarios antiguos, db4o o eXist | Querer usarlas como si fueran tablas |
| Componentes de acceso a datos | Patrón DAO, separar la lógica del acceso | Mezclar SQL con la interfaz gráfica |
Si sabes hacer los cinco ejercicios de abajo sin mirar los apuntes, tienes la base de la mayor parte del módulo.
Cinco ejercicios típicos resueltos
Los enunciados cambian según el profesor, pero la mayoría de exámenes de Acceso a Datos son una variación de estos. Intenta hacer cada uno antes de mirar la solución.
1. Leer un CSV y generar otro fichero
Enunciado: el fichero alumnos.csv tiene una cabecera y una línea por alumno con el formato nombre;nota. Genera aprobados.txt con los alumnos que tienen un 5 o más. Las líneas mal formadas se avisan por pantalla y no detienen el programa.
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
public class FiltrarAprobados {
public static void main(String[] args) {
Path origen = Path.of("alumnos.csv");
Path destino = Path.of("aprobados.txt");
try (BufferedReader lector = Files.newBufferedReader(origen, StandardCharsets.UTF_8);
BufferedWriter escritor = Files.newBufferedWriter(destino, StandardCharsets.UTF_8)) {
lector.readLine(); // salta la cabecera
String linea;
int numLinea = 1;
while ((linea = lector.readLine()) != null) {
numLinea++;
if (linea.isBlank()) {
continue;
}
String[] campos = linea.split(";");
try {
String nombre = campos[0].trim();
double nota = Double.parseDouble(campos[1].trim().replace(',', '.'));
if (nota >= 5) {
escritor.write(nombre + " - " + nota);
escritor.newLine();
}
} catch (ArrayIndexOutOfBoundsException | NumberFormatException e) {
System.err.println("Línea " + numLinea + " mal formada: " + linea);
}
}
} catch (NoSuchFileException e) {
System.err.println("No existe el fichero: " + e.getFile());
} catch (IOException e) {
System.err.println("Error de entrada/salida: " + e.getMessage());
}
}
}
Lo que hay que entender: el try con recursos cierra el lector y el escritor aunque haya una excepción. Si no cierras el escritor, el fichero puede quedarse vacío porque los datos siguen en el búfer. Indicar UTF_8 evita que las tildes salgan rotas. La línea mal formada se gestiona dentro del bucle para que un solo dato incorrecto no tire todo el programa, que es justo lo que suele pedir el enunciado. Y replace(',', '.') cubre las notas escritas como 7,5, que parseDouble no acepta.
2. Registros de tamaño fijo con RandomAccessFile
Enunciado: guarda alumnos en un fichero binario con el formato id (entero), nombre (20 caracteres) y nota (decimal). Debe permitir leer y modificar un alumno a partir de su id, sin recorrer el fichero desde el principio.
import java.io.IOException;
import java.io.RandomAccessFile;
public class FicheroAlumnos {
private static final int LONG_NOMBRE = 20; // caracteres
private static final int TAM_REGISTRO = 4 + LONG_NOMBRE * 2 + 8; // int + char x 20 + double = 52 bytes
static void escribir(RandomAccessFile raf, int id, String nombre, double nota) throws IOException {
raf.seek((long) (id - 1) * TAM_REGISTRO);
raf.writeInt(id);
StringBuilder sb = new StringBuilder(nombre);
sb.setLength(LONG_NOMBRE); // recorta o rellena hasta 20 caracteres
raf.writeChars(sb.toString());
raf.writeDouble(nota);
}
static void leer(RandomAccessFile raf, int id) throws IOException {
long posicion = (long) (id - 1) * TAM_REGISTRO;
if (posicion >= raf.length()) {
System.out.println("No existe el alumno " + id);
return;
}
raf.seek(posicion);
int idLeido = raf.readInt();
char[] nombre = new char[LONG_NOMBRE];
for (int i = 0; i < LONG_NOMBRE; i++) {
nombre[i] = raf.readChar();
}
double nota = raf.readDouble();
if (idLeido == 0) {
System.out.println("El registro " + id + " está vacío");
} else {
System.out.println(idLeido + " " + new String(nombre).trim() + " " + nota);
}
}
public static void main(String[] args) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile("alumnos.dat", "rw")) {
escribir(raf, 1, "Ana", 7.5);
escribir(raf, 2, "Luis", 4.25);
escribir(raf, 3, "Marta", 9.0);
leer(raf, 2); // salta directamente al registro 2
escribir(raf, 2, "Luis", 5.0); // modificar es sobrescribir en la misma posición
leer(raf, 2);
}
}
}
Lo que hay que entender: el acceso aleatorio funciona porque todos los registros miden lo mismo. La posición de un registro es (id - 1) * TAM_REGISTRO, y seek() salta ahí sin leer lo anterior. Cada char ocupa 2 bytes con writeChars, por eso el nombre ocupa 40 bytes y no 20. Ese cálculo es la pregunta trampa del examen: si te equivocas en un byte, a partir del segundo registro lees datos sin sentido. No uses writeUTF para el nombre en este ejercicio: guarda textos de longitud variable y rompe el tamaño fijo. Si escribes el alumno 7 sin haber escrito el 4, 5 y 6, el hueco se rellena con ceros; por eso se comprueba idLeido == 0.
3. Recorrer un XML con DOM
Enunciado: lee el fichero alumnos.xml y muestra el id, el nombre y la nota de cada alumno.
<alumnos>
<alumno id="1">
<nombre>Ana</nombre>
<nota>7.5</nota>
</alumno>
<alumno id="2">
<nombre>Luis</nombre>
<nota>4.25</nota>
</alumno>
</alumnos>
import java.io.File;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
public class LeerXml {
public static void main(String[] args) throws Exception {
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = builder.parse(new File("alumnos.xml"));
doc.getDocumentElement().normalize();
NodeList alumnos = doc.getElementsByTagName("alumno");
for (int i = 0; i < alumnos.getLength(); i++) {
Element alumno = (Element) alumnos.item(i);
String id = alumno.getAttribute("id");
String nombre = alumno.getElementsByTagName("nombre").item(0).getTextContent();
String nota = alumno.getElementsByTagName("nota").item(0).getTextContent();
System.out.println(id + " - " + nombre + ": " + nota);
}
}
}
Lo que hay que entender: DOM carga todo el documento en memoria como un árbol, lo que lo hace cómodo para recorrerlo y modificarlo. SAX lo lee de principio a fin lanzando eventos, sin cargarlo entero, y por eso se usa con ficheros grandes. El fallo típico es recorrer getChildNodes() y hacer un cast a Element: los saltos de línea y espacios entre etiquetas también son nodos (de texto), y el cast falla con ClassCastException. Buscar por nombre de etiqueta, como en el ejemplo, lo evita. Fíjate también en la diferencia entre atributo (id, se lee con getAttribute) y elemento hijo (nombre, se busca como etiqueta).
4. CRUD con JDBC y una transacción
Enunciado: en la base de datos instituto existen las tablas alumno (id, nombre, nota) y matricula (alumno_id, modulo_id). Lista los alumnos aprobados de mayor a menor nota. Después, matricula a un alumno nuevo en varios módulos: si falla cualquier inserción, no debe quedar guardado nada.
import java.sql.*;
public class AlumnoDAO {
private static final String URL = "jdbc:mysql://localhost:3306/instituto";
private static final String USUARIO = "root";
private static final String CLAVE = "1234";
public static void main(String[] args) {
try (Connection con = DriverManager.getConnection(URL, USUARIO, CLAVE)) {
listarAprobados(con);
matricular(con, "Marta", 486, 490);
} catch (SQLException e) {
System.err.println("Error de base de datos: " + e.getMessage());
}
}
static void listarAprobados(Connection con) throws SQLException {
String sql = "SELECT id, nombre, nota FROM alumno WHERE nota >= ? ORDER BY nota DESC";
try (PreparedStatement ps = con.prepareStatement(sql)) {
ps.setDouble(1, 5.0);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
System.out.printf("%d %s %.2f%n",
rs.getInt("id"), rs.getString("nombre"), rs.getDouble("nota"));
}
}
}
}
static void matricular(Connection con, String nombre, int... modulos) throws SQLException {
con.setAutoCommit(false); // a partir de aquí, nada se guarda hasta el commit
try (PreparedStatement psAlumno = con.prepareStatement(
"INSERT INTO alumno (nombre) VALUES (?)", Statement.RETURN_GENERATED_KEYS);
PreparedStatement psMatricula = con.prepareStatement(
"INSERT INTO matricula (alumno_id, modulo_id) VALUES (?, ?)")) {
psAlumno.setString(1, nombre);
psAlumno.executeUpdate();
int alumnoId;
try (ResultSet claves = psAlumno.getGeneratedKeys()) {
claves.next();
alumnoId = claves.getInt(1);
}
for (int modulo : modulos) {
psMatricula.setInt(1, alumnoId);
psMatricula.setInt(2, modulo);
psMatricula.executeUpdate();
}
con.commit();
} catch (SQLException e) {
con.rollback(); // deshace todo lo de esta transacción
throw e;
} finally {
con.setAutoCommit(true);
}
}
}
Lo que hay que entender:
PreparedStatementsiempre que haya datos. Los?se rellenan consetString,setInt, etc. Así se evita la inyección SQL y los errores con comillas y decimales. Concatenar valores en el texto de la consulta es el error que más penaliza en este módulo.- Los índices empiezan en 1, tanto en los parámetros (
setDouble(1, ...)) como en las columnas delResultSet. rs.next()antes de leer. ElResultSetempieza antes de la primera fila. Si lees sin llamar anext(), salta una excepción.- Transacción: por defecto JDBC guarda cada sentencia al momento (autocommit). Al desactivarlo, las inserciones se quedan pendientes hasta
commit(), yrollback()las deshace si algo falla. Así no queda un alumno sin matrículas o con la mitad. RETURN_GENERATED_KEYSte devuelve el id autoincremental que acaba de asignar la base de datos, para usarlo en la tabla relacionada.- Necesitas el driver de MySQL (o del sistema gestor que uséis) en el proyecto, normalmente como dependencia de Maven. Si falta, el error es
No suitable driver found.
Si las consultas SQL todavía te cuestan, repasa primero cómo aprobar SQL y el modelo entidad-relación: en este módulo se da por hecho que eso lo dominas.
5. Entidad con Hibernate y consulta HQL
Enunciado: mapea la tabla alumno con Hibernate, inserta un alumno y muestra los aprobados ordenados por nota usando HQL.
package es.instituto.modelo;
import jakarta.persistence.*;
@Entity
@Table(name = "alumno")
public class Alumno {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(nullable = false, length = 50)
private String nombre;
private Double nota;
protected Alumno() {
// Hibernate necesita un constructor sin argumentos
}
public Alumno(String nombre, Double nota) {
this.nombre = nombre;
this.nota = nota;
}
public Integer getId() { return id; }
public String getNombre() { return nombre; }
public Double getNota() { return nota; }
}
El fichero hibernate.cfg.xml, en src/main/resources, indica a qué base de datos conectar y qué clases mapear:
<hibernate-configuration>
<session-factory>
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/instituto</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.connection.password">1234</property>
<property name="hibernate.hbm2ddl.auto">update</property>
<property name="hibernate.show_sql">true</property>
<mapping class="es.instituto.modelo.Alumno"/>
</session-factory>
</hibernate-configuration>
import java.util.List;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import es.instituto.modelo.Alumno;
public class PruebaHibernate {
public static void main(String[] args) {
try (SessionFactory sf = new Configuration().configure().buildSessionFactory();
Session session = sf.openSession()) {
Transaction tx = session.beginTransaction();
session.persist(new Alumno("Ana", 7.5));
tx.commit();
List<Alumno> aprobados = session
.createQuery("from Alumno a where a.nota >= :minima order by a.nota desc", Alumno.class)
.setParameter("minima", 5.0)
.getResultList();
aprobados.forEach(a -> System.out.println(a.getNombre() + " " + a.getNota()));
}
}
}
Lo que hay que entender: con un ORM ya no escribes el INSERT: guardas un objeto y Hibernate genera el SQL. Con show_sql activado lo ves en la consola, y conviene mirarlo para entender qué está haciendo. HQL se parece a SQL, pero trabaja con clases y atributos, no con tablas y columnas: es from Alumno a where a.nota, con el nombre de la clase Java y de su atributo. Si escribes el nombre de la tabla y no coincide con el de la clase, Hibernate no lo encuentra. El parámetro :minima cumple la misma función que el ? de JDBC.
Un detalle de versiones que da muchos problemas: Hibernate 6 usa el paquete jakarta.persistence, y los apuntes antiguos usan javax.persistence. Si mezclas tutoriales de distintas épocas, las anotaciones no se reconocen. Usa lo que indique la versión de vuestro proyecto.
Los errores que más suspenden en Acceso a Datos
Concatenar datos en el SQL. "... WHERE nombre = '" + nombre + "'" funciona hasta que alguien se llama O'Brien, y además es la puerta a la inyección SQL. Muchos profesores lo penalizan aunque el programa funcione.
No cerrar conexiones ni ficheros. Sin try con recursos, las conexiones abiertas se acumulan hasta el error Too many connections, y los ficheros escritos se quedan a medias. Cierra siempre Connection, PreparedStatement, ResultSet y los flujos.
El fichero "no existe" pero está ahí. Las rutas relativas parten de la carpeta desde la que se ejecuta el programa, que en el IDE suele ser la raíz del proyecto, no la carpeta del .java. Si dudas, imprime Path.of("alumnos.csv").toAbsolutePath() y mira dónde lo está buscando.
Calcular mal el tamaño del registro. En RandomAccessFile, cada char son 2 bytes, cada int 4 y cada double 8. Un solo byte de diferencia desplaza todos los registros siguientes.
Olvidar el commit. Si desactivas el autocommit y no haces commit(), los cambios se pierden al cerrar la conexión y parece que el INSERT no hace nada.
Borrar la base de datos sin querer. Con hibernate.hbm2ddl.auto en create, Hibernate borra y vuelve a crear las tablas cada vez que arranca. Para hacer pruebas sobre datos que ya tienes, usa update o validate.
LazyInitializationException. Aparece al acceder a una relación de una entidad (por ejemplo, las matrículas de un alumno) después de cerrar la sesión. Por defecto, Hibernate carga muchas de esas relaciones solo cuando las usas, y para eso necesita la sesión abierta. Carga lo que necesites antes de cerrarla.
Mezclar el acceso a datos con la interfaz. Poner el SQL dentro del botón de la ventana funciona, pero es justo lo que el módulo enseña a no hacer. Separa el acceso en clases DAO: el enunciado lo suele pedir y la corrección lo puntúa.
Cómo prepararlo
Repasa SQL y Java antes de empezar. Acceso a Datos se apoya en los dos módulos de primero. Si fallas en un JOIN o en una excepción, el problema no es de este módulo. Los fallos de Java que más se arrastran están en los errores de Java más comunes en DAM y DAW.
Monta tu entorno antes del examen. Base de datos instalada, driver en el proyecto, Hibernate configurado y un ejemplo que funcione de principio a fin. Muchos alumnos pierden la mitad del tiempo del examen peleándose con la configuración y no con el ejercicio.
Haz un mismo ejercicio con todas las técnicas. Coge una tabla de alumnos y haz el mismo CRUD con fichero de texto, con fichero de acceso aleatorio, con JDBC y con Hibernate. Ves las diferencias mucho más claras que con cuatro ejercicios distintos, y aprendes qué ventaja tiene cada una, que es una pregunta teórica habitual.
Ten a mano las plantillas. Lectura con BufferedReader, registro de RandomAccessFile con su tamaño calculado, recorrido DOM, PreparedStatement con transacción y entidad con su configuración. Si las escribes de memoria, el examen es adaptarlas.
Lee los errores enteros. En este módulo los mensajes son largos, pero la causa suele estar en una línea concreta: la tabla que no existe, la columna mal escrita, el driver que falta. Busca la primera línea que menciona tu código o la causa (Caused by). Si te pierdes, sigue este método para depurar paso a paso.
En segundo también tienes Programación de Servicios y Procesos, que suele ser el otro módulo complicado del curso. Si te toca a la vez, aquí tienes cómo aprobar PSP con ejercicios de hilos y sockets resueltos. Y si estás pensando en el proyecto final, casi todos llevan una capa de acceso a datos: tienes ideas de proyecto final para DAM y DAW.
Cuándo pedir ayuda
Si haces los ejercicios, entiendes los errores que te salen y sabes explicar la diferencia entre JDBC y Hibernate, vas bien. Merece la pena buscar ayuda cuando:
- No consigues que tu proyecto conecte a la base de datos y llevas días con la configuración.
- Copias el ejemplo de Hibernate de los apuntes, funciona, pero no sabrías crear una entidad nueva.
- Mezclas SQL, Java y XML y ya no sabes en cuál de los tres está el fallo.
- Tienes Acceso a Datos pendiente y necesitas aprobarlo para hacer las prácticas en empresa o presentar el proyecto.
En nuestras clases de refuerzo para DAM y DAW trabajamos con tus enunciados y en tu propio proyecto: lo configuramos juntos, buscamos dónde se rompe y lo resuelves tú, para que en el examen no dependas de nadie. Si lo que falla es la base, también damos clases de SQL online y clases de Java online. Son clases online uno a uno a 25 € la hora, sin matrícula ni permanencia.
Preguntas frecuentes
¿Qué se da en Acceso a Datos en DAM?
Acceso a Datos es el módulo 0486 de segundo de DAM. Enseña a guardar y recuperar información desde un programa: ficheros de texto y binarios, XML, bases de datos relacionales con JDBC, mapeo objeto-relacional con Hibernate o JPA, bases de datos no relacionales (sobre todo MongoDB) y el patrón DAO para organizar el acceso a datos.
¿Es difícil Acceso a Datos?
Es un módulo exigente porque junta Programación y Bases de Datos de primero, y añade configuración (drivers, Maven, Hibernate) que puede bloquearte antes de escribir una línea útil. Si llegas con Java y SQL bien asentados y montas el entorno con tiempo, se aprueba practicando los ejercicios tipo.
¿Qué diferencia hay entre JDBC e Hibernate?
JDBC es la API estándar de Java para conectarse a una base de datos y ejecutar SQL escrito por ti; tú conviertes cada fila en un objeto. Hibernate es un ORM que funciona por encima de JDBC: mapea clases Java a tablas y genera el SQL. Con JDBC tienes control total sobre las consultas; con Hibernate escribes menos código repetitivo.
¿Qué diferencia hay entre DOM y SAX?
DOM carga el XML completo en memoria como un árbol, lo que permite recorrerlo en cualquier orden y modificarlo. SAX lo lee de forma secuencial y avisa con eventos cada vez que encuentra una etiqueta, sin cargarlo entero. DOM es más cómodo; SAX gasta menos memoria con ficheros grandes.
¿Hibernate o JPA en Acceso a Datos?
JPA (hoy Jakarta Persistence) es la especificación estándar y Hibernate es su implementación más usada. En la práctica se suelen usar juntas: anotaciones de JPA en las entidades y Hibernate por debajo. Usa la forma que se dé en tu centro, porque los ejemplos de los apuntes cambian bastante entre una y otra.
¿Se da MongoDB en Acceso a Datos?
En muchos centros sí, como ejemplo de base de datos no relacional (documental). En temarios más antiguos se usaban db4o, una base de datos orientada a objetos, o eXist para XML. Pregunta a tu profesor qué entra, porque es la parte del temario que más cambia de un centro a otro.