Serialización y persistencia

Serializable y mapeo objeto-relacional manual

java.io.Serializable

Serializable es un mecanismo para convertir un objeto a una secuencia de bytes y reconstruirlo. El uso principal era para RMI y caché distribuida. Hoy se prefiere JSON (Jackson, Gson) o Protobuf para serialización porque son más interoperables. transient excluye campos de la serialización (contraseñas, conexiones...).

import java.io.*;

// Serializable: marca que un objeto puede convertirse a bytes y recuperarse
public class Configuracion implements Serializable {
    private static final long serialVersionUID = 1L; // versión para compatibilidad

    private String host;
    private int puerto;
    private transient String password; // transient: no se serializa

    public Configuracion(String host, int puerto, String password) {
        this.host = host;
        this.puerto = puerto;
        this.password = password;
    }
    // getters...
}

// Serializar a fichero
Configuracion cfg = new Configuracion("localhost", 5432, "secreto");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("config.ser"))) {
    oos.writeObject(cfg);
}

// Deserializar desde fichero
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("config.ser"))) {
    Configuracion recuperada = (Configuracion) ois.readObject();
    System.out.println(recuperada.getHost());    // "localhost"
    System.out.println(recuperada.getPassword()); // null — era transient
}

serialVersionUID

// serialVersionUID: versión del esquema de serialización
// Si la clase cambia y no coincide el UID, lanza InvalidClassException al deserializar
public class Pedido implements Serializable {
    private static final long serialVersionUID = 2L; // cambiar al modificar la estructura

    private Long id;
    private String descripcion;
    // Si se añade un campo nuevo, los ficheros serializados con UID=1 fallarán al cargar
    // Incrementar serialVersionUID fuerza la detección del cambio
}

Mapeo objeto-relacional manual

Sin un ORM, el mapeo de relaciones entre tablas hay que hacerlo manualmente. El problema más común es el N+1: cargar una lista de entidades padre y luego hacer una consulta por entidad para cargar sus hijos. La solución es siempre un JOIN que traiga todo en una consulta.

// Mapeo objeto-relacional manual: sin ORM
// La "N+1 problem" es fácil de caer — cargar colecciones con consulta separada por entidad

// MAL: una consulta por pedido (N+1)
List<Cliente> clientes = repo.buscarTodos();
for (Cliente c : clientes) {
    List<Pedido> pedidos = pedidoRepo.buscarPorClienteId(c.getId()); // N consultas
    c.setPedidos(pedidos);
}

// BIEN: un JOIN para traerlo todo de una vez
String sql = """
    SELECT c.id AS cliente_id, c.nombre,
           p.id AS pedido_id, p.total, p.fecha
    FROM clientes c
    LEFT JOIN pedidos p ON c.id = p.cliente_id
    ORDER BY c.id
    """;

try (Connection con = dataSource.getConnection();
     PreparedStatement ps = con.prepareStatement(sql);
     ResultSet rs = ps.executeQuery()) {

    Map<Long, Cliente> clienteMap = new LinkedHashMap<>();
    while (rs.next()) {
        long clienteId = rs.getLong("cliente_id");
        Cliente cliente = clienteMap.computeIfAbsent(clienteId, id -> {
            Cliente c = new Cliente();
            try { c.setId(id); c.setNombre(rs.getString("nombre")); }
            catch (SQLException e) { throw new RuntimeException(e); }
            c.setPedidos(new ArrayList<>());
            return c;
        });
        long pedidoId = rs.getLong("pedido_id");
        if (pedidoId != 0) { // puede ser null si el cliente no tiene pedidos (LEFT JOIN)
            Pedido p = new Pedido();
            try {
                p.setId(pedidoId);
                p.setTotal(rs.getBigDecimal("total"));
                p.setFecha(rs.getDate("fecha").toLocalDate());
            } catch (SQLException e) { throw new RuntimeException(e); }
            cliente.getPedidos().add(p);
        }
    }
    return new ArrayList<>(clienteMap.values());
}

Serialización binaria vs JDBC vs ORM

// Serialización binaria vs persistencia en BBDD:
//
// java.io.Serializable (serialización binaria):
//   + Sencillo para objetos en memoria, ficheros temporales, caché en disco
//   + Sin dependencia de BBDD
//   - Formato frágil: cualquier cambio en la clase puede romper la compatibilidad
//   - No es consultable: no se puede hacer "SELECT" sobre objetos serializados
//   - Raramente usada en aplicaciones nuevas (JSON/XML son más interoperables)
//
// Mapeo relacional manual (JDBC):
//   + Control total sobre el SQL generado
//   + Portable entre BBDD
//   - Verbose: mucho código de plumbing (el ORM automatiza esto)
//   - Gestión manual de relaciones y caché de primer nivel
//
// ORM (JPA/Hibernate):
//   + Mapeo automático, consultas JPQL, caché, lazy loading
//   - Caja negra: genera SQL inesperado si no se entiende el modelo
//   - El conocimiento de JDBC es útil para depurar queries generadas
Conclusión de la sección: JDBC es la base de todos los frameworks de persistencia en Java. JPA/Hibernate, Spring Data y MyBatis generan llamadas JDBC internamente. Conocer JDBC directo permite entender el SQL real que ejecutan los frameworks, diagnosticar problemas de rendimiento y mantener código legacy sin frameworks.

Siguiente sección → Novedades funcionales de Java 8

Índice de la sección

Índice del curso