Apuntes personales para el examen oral. Los escribo como me los explicaría a mí misma: primero la teoría que puede caer suelta, luego cómo está hecho cada proyecto y por qué cada decisión. Incluyo trozos de código con la ruta y línea para localizarlos rápido si me preguntan algo concreto.
Índice
- Teoría general (lo que puede preguntar sin abrir el código)
- Proyecto Escalada
- Proyecto Valorant
- Diferencias clave entre los dos
- Banco de preguntas y respuestas cortas
1. Teoría general
1.1 ¿Qué es JDBC?
JDBC (Java Database Connectivity) es la API estándar de Java para conectarse a bases de datos relacionales y ejecutar SQL. Vive en el paquete java.sql (con apoyo de javax.sql para cosas modernas como DataSource).
JDBC define solo el contrato (interfaces: Connection, Statement, PreparedStatement, ResultSet…). Cada base de datos aporta su driver que implementa esas interfaces. En mis proyectos el driver es org.postgresql:postgresql:42.7.10, declarado en el pom.xml.
Flujo típico de una operación JDBC
- Pedir una conexión (
Connection). - Crear una sentencia (
PreparedStatement) con el SQL. - Asignar parámetros (
ps.setString(1, ...),ps.setInt(2, ...)). - Ejecutar:
executeQuery()para SELECT,executeUpdate()para INSERT/UPDATE/DELETE. - Recorrer el ResultSet si hay datos.
- Cerrar todo (en mis proyectos con
try-with-resources).
1.2 Statement vs PreparedStatement
Statement: SQL en texto plano. Vulnerable a inyección SQL si concatenas input del usuario.PreparedStatement: SQL con?como parámetros. Se rellenan tipado:ps.setInt,ps.setString, etc.
Ventajas de PreparedStatement:
- Previene inyección SQL (los valores nunca se interpretan como código SQL).
- Se puede precompilar y reutilizar.
- Maneja correctamente tipos, fechas,
NULL, comillas…
En mis dos proyectos uso siempre PreparedStatement. Si me preguntan por qué, esta es la respuesta.
1.3 ¿Qué es un ResultSet?
Es el cursor que representa las filas devueltas por un SELECT. Se recorre con rs.next() (avanza una fila y devuelve true si hay datos). Se leen columnas por nombre o índice: rs.getString("nom"), rs.getInt("id").
1.4 Patrón DAO
DAO = Data Access Object. Es un objeto cuya única responsabilidad es acceder a los datos (leer y escribir en la BD). Ventajas:
- Aísla el SQL en un único sitio.
- Si cambio de BD (PostgreSQL → MySQL), solo toco los DAO.
- El resto del programa (controladores, vista) no sabe nada de SQL.
En mis proyectos cada entidad tiene su DAO (EscolaDAO, ViaDAO, AgentDAO, MapDAO…) y todos implementan una interfaz genérica CRUD<T>.
1.5 ¿Qué es CRUD?
Las 4 operaciones básicas sobre datos:
- Create → INSERT
- Read → SELECT
- Update → UPDATE
- Delete → DELETE
Mi interfaz CRUD<T> declara esos métodos con genéricos, así cada DAO la usa con su tipo concreto.
1.6 Patrón MVC
Modelo-Vista-Controlador. Separa responsabilidades:
- Model: clases que representan los datos (POJOs con atributos, constructores, getters/setters y
toString). - View: entrada y salida por consola (
View.java). - Controller: orquesta: pide datos a la vista, llama a los DAO, muestra resultados.
Mis proyectos añaden una capa DAO entre el controlador y la BD, así que el patrón completo es MVC + DAO.
1.7 Pool de conexiones y HikariCP
Abrir una conexión a la BD es costoso (handshake TCP, autenticación, etc.). Un pool de conexiones mantiene un conjunto de conexiones ya abiertas y las reutiliza: cuando pides una, te da una libre; cuando la “cierras”, vuelve al pool en vez de cerrarse de verdad.
HikariCP es la librería de pool que uso (com.zaxxer.hikari). Pasos:
- Crear un
HikariConfigcon url, usuario, password, tamaño máximo… - Crear un
HikariDataSource(config). - Pasar el
DataSourcea los DAO. - Cada
ds.getConnection()saca una conexión del pool. - El
try-with-resourcesla devuelve al pool al terminar.
1.8 ¿Qué es un DataSource?
Interfaz estándar javax.sql.DataSource: una fábrica de conexiones. HikariDataSource la implementa. Se pasa a los DAO como abstracción — no saben si por debajo hay un pool o no. Es la forma moderna de obtener conexiones (en vez del viejo DriverManager.getConnection).
1.9 try-with-resources
Sintaxis: try (Recurso r = ...) { ... }. Cualquier objeto que implemente AutoCloseable se cierra automáticamente al salir del bloque, incluso si hay excepción. Aquí lo uso siempre con Connection, PreparedStatement y ResultSet. Garantiza que nunca quedan conexiones abiertas y no hay fugas del pool.
1.10 Transacciones (ACID)
Una transacción es un grupo de operaciones SQL que se aplican todas o ninguna. Propiedades ACID:
- Atomicidad: todo o nada.
- Consistencia: la BD pasa de un estado válido a otro.
- Aislamiento: transacciones concurrentes no se interfieren.
- Durabilidad: una vez confirmadas, persisten aunque se caiga el sistema.
Por defecto JDBC trabaja en autocommit (cada sentencia se confirma sola). Para transacción manual:
conn.setAutoCommit(false);try { // varias operaciones conn.commit();} catch (SQLException e) { conn.rollback();}Honestidad sobre mis proyectos: no uso transacciones manuales. Cada operación de DAO abre su propia conexión y se autoconfirma. En sitios donde sería deseable lo reconozco con un comentario:
- En
CrearVia(Escalada): la vía y sus llargs no comparten transacción → si falla a mitad puede quedar una vía sin todos sus llargs. - En
SincronitzarAgents(Valorant): los roles, agentes y abilities tampoco comparten transacción.
Si me preguntan cómo mejorarlo: agrupar esas operaciones en una sola transacción usando una única conexión con setAutoCommit(false) + commit/rollback.
1.11 Claves foráneas y borrado en cascada
Una FK es una columna que apunta a la PK de otra tabla, garantizando integridad referencial.
ON DELETE CASCADE: si borras el padre, se borran las hijas.ON DELETE SET NULL: si borras el padre, la FK de las hijas se pone a NULL.
1.12 Maven
Maven gestiona dependencias y build. El pom.xml declara:
- Dependencias (driver PostgreSQL, HikariCP, Gson, slf4j).
- Versión de Java (
maven.compiler.release= 21). - Cómo ejecutar (
exec-maven-pluginconmainClass = Main).
Comandos:
mvn compile→ compila.mvn exec:java→ ejecutaMain.
1.13 slf4j-api + slf4j-nop
SLF4J es una fachada de logging que usa HikariCP internamente. Añado slf4j-nop para silenciar warnings de “no hay logger configurado”.
1.14 ¿Qué es una API REST? (Valorant)
Un servicio web al que se le piden datos por HTTP (GET) y devuelve normalmente JSON. Un endpoint es una URL concreta del API (ej. /v1/agents). El código 200 significa OK.
1.15 ¿Qué es JSON y qué es Gson?
JSON: formato de texto para intercambiar datos. Objetos {}, arrays [], pares clave: valor.
Gson (de Google): librería para convertir entre JSON y objetos Java. En mi proyecto Valorant la uso a bajo nivel (JsonParser, JsonObject, JsonArray, JsonElement).
2. Proyecto Escalada
2.1 De qué va
Aplicación de consola en Java para gestionar escuelas de escalada, sectores, vías, escaladores y ascensiones. CRUD completo + consultas personalizadas (SQL más avanzado). Sin API externa, solo BD PostgreSQL.
Tecnologías: Java 21, PostgreSQL, JDBC, HikariCP, Maven. Arquitectura MVC + DAO.
2.2 Modelo de datos
escola (1) ──< (N) sector ──< (N) via ──< (N) llarg ▲ │ creada_per_idescalador (1) ──< (N) ascensio >── (N) viaDefinido en db/schema.sql:
escola(línea 9):idSERIAL PK,nomUNIQUE NOT NULL,lloc,aproximacio,popularitatcon CHECK (baixa/mitjana/alta).sector(línea 20): FKescola_id→escola(id)ON DELETE CASCADE.escalador(línea 38): id, nom, alies, edat, estil_preferit.via(línea 49): muchas columnas con CHECK constraints (orientacio,estat,tipus,tipus_roca), FKsector_idCASCADE y FKcreada_per_idSET NULL.llarg(línea 76): FKvia_idCASCADE.ascensio(línea 90): FKsescalador_idyvia_id, ambas CASCADE.
Lo importante: las CHECK constraints validan los valores a nivel de BD, además de la validación que hace el controlador.
2.3 Arranque (src/Main.java)
public class Main { public static void main(String[] args) { ConnectionProvider connectionProvider = new PostgresSQLConnection(); try (HikariDataSource ds = (HikariDataSource) connectionProvider.dataSource()) { View.missatge("Connexió a la DB establerta correctament."); MenuPrincipal.executar(ds); } }}- Línea 11: crea el proveedor de conexión PostgreSQL.
- Línea 13: abre el pool dentro de
try-with-resources→ al salir, el pool se cierra y libera todas las conexiones. - Línea 16: pasa el
DataSourceal menú. Toda la app comparte ese único pool.
2.4 Capa de conexión
src/DAO/ConnectionProvider.java
Interfaz con un método de instancia y uno estático:
public interface ConnectionProvider { DataSource dataSource();
static Properties loadProperties(String db) { Properties fitxer = new Properties(); try (FileInputStream fis = new FileInputStream("db/config.properties")) { fitxer.load(fis); } catch (IOException e) { throw new RuntimeException("No s'ha pogut carregar db/config.properties: " + e.getMessage()); } Properties props = new Properties(); props.setProperty("url", fitxer.getProperty("db.url." + db)); props.setProperty("user", fitxer.getProperty("db.user." + db)); props.setProperty("password", fitxer.getProperty("db.password." + db)); return props; }}El parámetro db permite tener varias configuraciones (postgresql, mysql) en el mismo fichero db/config.properties.
src/DAO/PostgreSQL/PostgresSQLConnection.java
public DataSource dataSource() { Properties props = DAO.ConnectionProvider.loadProperties("postgresql"); HikariConfig cfg = new HikariConfig(); cfg.setJdbcUrl(props.getProperty("url")); cfg.setUsername(props.getProperty("user")); cfg.setPassword(props.getProperty("password")); cfg.setMaximumPoolSize(5); cfg.setPoolName("EscaladaPool"); return new HikariDataSource(cfg);}Configura HikariCP: máximo 5 conexiones, nombre del pool EscaladaPool.
2.5 Modelos (src/Model/)
Son POJOs: atributos privados + dos constructores (uno con id para leer de BD, otro sin id para crear nuevos) + getters/setters + toString().
Detalles importantes:
Via.java: el más grande (13 campos). Guarda las fechas comoString; la conversión a/desdeTimestampse hace en el DAO.Sector.java:latitud/longitudsonFloat(objeto, admitenull).- Ejemplo del
toStringdeEscola:
@Overridepublic String toString() { return "Escola #" + id + " | nom: " + text(nom) + " | lloc: " + text(lloc) + " | aproximacio: " + text(aproximacio) + " | popularitat: " + text(popularitat);}
private static String text(String value) { if (value == null || value.isBlank()) return "sense dades"; return value;}Interfaz CRUD<T> (src/Model/Interfaces/CRUD.java)
public interface CRUD<T> { void create(T obj) throws DAOException; T read(int id) throws DAOException; void update(T obj) throws DAOException; void delete(int id) throws DAOException; List<T> getAll() throws DAOException;}Genérica con <T>. En Escalada read/delete recibe int (luego en Valorant cambia a String porque las PK son UUID).
Excepción propia (src/Model/Exceptions/DAOException.java)
public class DAOException extends Exception { public DAOException(String message) { super(message); } public DAOException(String message, Throwable cause) { super(message, cause); }}Es checked (extiende Exception). Sirve para envolver las SQLException técnicas en una excepción del dominio con mensaje legible. El controlador nunca ve SQLException directamente.
2.6 Los DAO (corazón de JDBC)
Patrón general con EscolaDAO como ejemplo. Constructor recibe el DataSource y lo guarda:
public EscolaDAO(DataSource ds) { this.ds = ds;}create con recuperación del id autogenerado
public void create(Escola escola) throws DAOException { String sql = "INSERT INTO escola (nom, lloc, aproximacio, popularitat) VALUES (?, ?, ?, ?)"; try (Connection conn = ds.getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, escola.getNom()); ps.setString(2, escola.getLloc()); ps.setString(3, escola.getAproximacio()); ps.setString(4, escola.getPopularitat()); ps.executeUpdate();
try (ResultSet keys = ps.getGeneratedKeys()) { if (keys.next()) escola.setId(keys.getInt(1)); } } catch (SQLException e) { throw new DAOException("Error al crear l'escola: " + e.getMessage(), e); }}Puntos clave:
ds.getConnection()saca una conexión del pool.Statement.RETURN_GENERATED_KEYS→ pide al driver que devuelva la PK autogenerada (SERIAL). Se lee congetGeneratedKeys()y se mete en el objeto.- La
SQLExceptionse envuelve enDAOExceptioncon mensaje legible.
read por id
public Escola read(int id) throws DAOException { String sql = "SELECT id, nom, lloc, aproximacio, popularitat FROM escola WHERE id = ?"; try (Connection conn = ds.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, id); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) return mapRow(rs); return null; } } catch (SQLException e) { throw new DAOException("Error al llegir l'escola amb id " + id + ": " + e.getMessage(), e); }}mapRow (helper privado)
Centraliza la conversión de fila a objeto:
private Escola mapRow(ResultSet rs) throws SQLException { return new Escola( rs.getInt("id"), rs.getString("nom"), rs.getString("lloc"), rs.getString("aproximacio"), rs.getString("popularitat") );}Método extra countVies (líneas 118-135)
Cuenta vías de una escuela con un JOIN, porque via no tiene escola_id directo:
public int countVies(int escolaId) throws DAOException { String sql = "SELECT COUNT(*) FROM via v " + "JOIN sector s ON v.sector_id = s.id " + "WHERE s.escola_id = ?"; // ... ejecutar y devolver rs.getInt(1)}ViaDAO — particularidades importantes
- Helper de fechas
setTimestamp(línea 161):
private static void setTimestamp(PreparedStatement ps, int idx, String value) throws SQLException { if (value == null || value.isBlank()) { ps.setNull(idx, Types.TIMESTAMP); } else { ps.setTimestamp(idx, Timestamp.valueOf(value)); }}actualitzarEstats()(línea 147): antes de leer/listar vías, ejecuta un UPDATE que pone a'Apte'las vías cuyodata_fi_estatya pasó. Lógica automática de caducidad:
String sql = "UPDATE via SET estat = 'Apte' " + "WHERE estat IN ('Construccio', 'Tancada') AND data_fi_estat < NOW()";EscaladorDAO.getNivell(int escaladorId) (línea 118)
Devuelve el grado máximo ascendido por un escalador:
String sql = "SELECT MAX(v.grau_dificultat) FROM ascensio a " + "JOIN via v ON a.via_id = v.id " + "WHERE a.escalador_id = ?";Ojo: MAX sobre texto usa orden lexicográfico del grado (lo dice el propio comentario del código).
AscensioDAO.getByEscalador(int) (línea 120)
Ascensiones de un escalador ordenadas por fecha DESC. Usa ps.setDate y rs.getDate para tipo DATE.
LlargDAO — no implementa CRUD
Métodos a medida: create, getByVia, deleteByVia, sumLlargades. Este último (línea 81) usa COALESCE para devolver 0 si no hay llargs:
String sql = "SELECT COALESCE(SUM(llargada), 0) FROM llarg WHERE via_id = ?";ConsultesDAO — las consultas avanzadas (muy preguntable)
Usa record de Java (clases inmutables compactas) para devolver resultados que no encajan en un modelo:
public record SectorAmbViesDisponibles(int id, String sectorNom, String escolaNom, int viesDisponibles) {}Truco para ordenar grados de escalada por dificultad real, no lexicográficamente:
private static final String GRAUS_SQL_ARRAY = "ARRAY['3','3+','4','4+','5','5+','6a','6a+','6b','6b+','6c','6c+'," + "'7a','7a+','7b','7b+','7c','7c+','8a','8a+','8b','8b+','8c','8c+'," + "'9a','9a+','9b','9b+','9c','9c+']::text[]";Luego en SQL: array_position(GRAUS_SQL_ARRAY, v.grau_dificultat) devuelve la posición = dificultad numérica.
Consultas destacadas:
viesQueEsTrobaranDisponibles(escolaId)— vías en Construccio/Tancada condata_fi_estat > NOW().viesPerRangDificultat(min, max)— usaarray_position(...) BETWEEN ? AND ?.escolesAmbRestriccionsActives— usaCOUNT(...) FILTER (WHERE ...)(agregado condicional de PostgreSQL),LEFT JOIN,GROUP BY ... HAVING.sectorsAmbMesDeXViesDisponibles(min)—GROUP BY+HAVING COUNT(v.id) > ?.escaladorsAmbMateixNivellMaxim— usa CTE (WITH ... AS), dos subconsultas.viesPassadesAApteRecentment(dies)—data_fi_estat >= NOW() - (?::int * INTERVAL '1 day').viesMesLlarguesEscola(escolaId)— CTE que suma llargadas y selecciona las de máxima longitud.
Conceptos SQL que pueden caer aquí: JOIN, LEFT JOIN, GROUP BY, HAVING, COUNT/MAX/SUM, COALESCE, FILTER, CTE (WITH), arrays de PostgreSQL, intervalos de tiempo.
2.7 Vista (src/View/View.java)
Clase con métodos estáticos y un único Scanner estático. Responsabilidades:
- Entrada validada:
llegirEnter(reintenta si no es número),llegirEnterPositiu,llegirFloat,llegirLiniaNoBuida. - Salida:
missatge(String). - “Limpiar” consola:
netejarConsola()imprime 60 líneas en blanco (no hay clear portable en Java de consola). - Menús: solo imprimen texto.
Clave conceptual: la View no sabe nada de BD ni de lógica. Solo I/O.
2.8 Controladores (src/Controller/)
Organizados por carpetas según operación: Menus/, CrearObjectes/, ActualitzarObjectes/, EliminarObjectes/, LlistarObjecte/ (por id), LlistarTots/, ConsultesPreecreades/.
MenuPrincipal.executar(DataSource ds)
Instancia todos los DAO una sola vez y bucle con switch:
public static void executar(DataSource ds) { EscolaDAO escolaDAO = new EscolaDAO(ds); EscaladorDAO escaladorDAO = new EscaladorDAO(ds); SectorDAO sectorDAO = new SectorDAO(ds); ViaDAO viaDAO = new ViaDAO(ds); LlargDAO llargDAO = new LlargDAO(ds); AscensioDAO ascensioDAO = new AscensioDAO(ds); ConsultesDAO consultesDAO = new ConsultesDAO(ds);
boolean sortir = false; while (!sortir) { View.mostrarMenuPrincipal(); int opcio = View.llegirEnter("Escull una opció: "); View.netejarConsola(); switch (opcio) { case 1 -> MenuEscola.executar(escolaDAO, consultesDAO); case 2 -> MenuEscalador.executar(escaladorDAO, ascensioDAO, consultesDAO); // ... case 0 -> sortir = true; default -> View.mostrarOpcioIncorrecta(); } }}Usa switch expression moderno (case 1 -> ...).
Patrón de controlador CRUD (ejemplo CrearEscola)
public static void executar(EscolaDAO dao) { String nom = View.llegirLiniaNoBuida("Nom de l'escola: "); String lloc = View.llegirLinia("Lloc: "); String aproximacio = View.llegirLinia("Aproximació (descripció com arribar): "); String popularitat; while (true) { String entrada = View.llegirLinia("Popularitat (baixa / mitjana / alta): ").trim(); if (entrada.equalsIgnoreCase("baixa") || entrada.equalsIgnoreCase("mitjana") || entrada.equalsIgnoreCase("alta")) { popularitat = entrada.toLowerCase(); break; } View.missatge("Valor no vàlid. Opcions: baixa, mitjana, alta"); }
Escola escola = new Escola(nom, lloc, aproximacio, popularitat); try { dao.create(escola); View.missatge("Escola creada amb id " + escola.getId()); } catch (DAOException e) { View.missatge(e.getMessage()); }}Pasos: 1) leer datos validados de la View, 2) crear el objeto, 3) llamar al DAO en try/catch (DAOException), 4) mostrar resultado.
ActualitzarEscola — patrón “dejar en blanco = mantener”
String nom = View.llegirLinia("Nou nom [" + escola.getNom() + "]: ");if (!nom.isBlank()) escola.setNom(nom);CrearVia — el más complejo (preguntable)
- Valida el grado con regex:
^([3-5]\\+?|[6-9][a-c]\\+?)$. - Si el estado no es
Apte, pide fechas inicio/fin y las valida conTimestamp.valueOf. - El tipo de vía limita los ancoratges válidos (esportiva → pocos; tradicional → más).
- Valida que el sector y el escalador existan leyéndolos del DAO antes de crear la vía.
- Crea la vía y luego los llargs (1 si esportiva, N si classica/gel).
- Comentario clave (línea 64): vía y llargs no comparten transacción única → si falla a mitad puede quedar inconsistente. Mejora: transacción explícita.
ConsultesController
Tiene los arrays GRAUS y ESTATS en Java (espejo de los de SQL). Cada método: pide parámetros validados, llama al ConsultesDAO, formatea la salida recorriendo los record. ordreGrau() convierte un grado a su posición (1-based) para pasarlo a la consulta SQL. Corrige rangos invertidos automáticamente.
3. Proyecto Valorant
3.1 De qué va
Aplicación de consola que gestiona agentes y mapas de Valorant obtenidos de una API REST externa (valorant-api.com) o de ficheros JSON locales, y los persiste en PostgreSQL.
Lo nuevo respecto a Escalada:
- Cliente HTTP para consumir API REST (
java.net.http.HttpClient). - Parseo de JSON con Gson.
- Upsert (
INSERT ... ON CONFLICT DO UPDATE) para sincronizar. - PKs tipo UUID (String), no enteros autogenerados.
- Sincronización: copia parcial vs completa.
Tecnologías: Java 21, PostgreSQL, JDBC, HikariCP, Maven, Gson 2.11.0. MVC + DAO + capa API.
3.2 Modelo de datos
role (1) ──< (N) agent ──< (N) abilitymap (independiente)role:uuidPK VARCHAR(36), display_name, description. Los 4 roles (Duelista, Controlador, Centinela, Iniciador).agent:uuidPK, datos del agente,role_uuidFK → roleON DELETE SET NULL, ylast_updated TIMESTAMP DEFAULT NOW().ability:id SERIALPK,agent_uuidFK → agentON DELETE CASCADE, slot, display_name, description.map:uuidPK, muchos campos descriptivos, multiplicadores de coordenadas ylast_updated.
Las tablas se crean con CREATE TABLE IF NOT EXISTS. PK tipo VARCHAR(36) porque la API da UUIDs.
3.3 Capa API (src/Api/) — lo distintivo
ValorantApiClient.java — cliente HTTP
public class ValorantApiClient { private static final String BASE = "https://valorant-api.com/v1"; private final HttpClient client = HttpClient.newHttpClient();
public String fetchAgents() { return get(BASE + "/agents?isPlayableCharacter=true"); } public String fetchMaps() { return get(BASE + "/maps"); } public String fetchAgentByUuid(String uuid) { return get(BASE + "/agents/" + uuid); } public String fetchMapByUuid(String uuid) { return get(BASE + "/maps/" + uuid); }
private String get(String url) { try { HttpRequest req = HttpRequest.newBuilder() .uri(URI.create(url)) .GET() .build(); HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString()); if (resp.statusCode() != 200) { throw new RuntimeException("L'API ha retornat codi " + resp.statusCode() + " per a: " + url); } return resp.body(); } catch (IOException e) { throw new RuntimeException("Error de connexió amb l'API: " + e.getMessage(), e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Connexió interrompuda: " + e.getMessage(), e); } }}Cosas a remarcar:
- Uso
java.net.http.HttpClient(cliente HTTP nativo de Java 11+, sin librerías externas). - Compruebo
statusCode() == 200; si no, lanzoRuntimeException. - Al capturar
InterruptedExceptionrestauro el flag conThread.currentThread().interrupt()(buena práctica).
ValorantJsonLoader.java — lector de ficheros
private String read(String filePath) { try { return Files.readString(Paths.get(filePath)); } catch (IOException e) { throw new RuntimeException("No s'ha pogut llegir el fitxer JSON: " + e.getMessage(), e); }}Lee un JSON local. Sirve para cargar datos sin conexión, siempre que el fichero tenga la misma estructura que el endpoint (campo data con array).
ValorantMapper.java — el parser JSON (Gson)
Convierte JSON (String) → objetos del modelo. Uso Gson a bajo nivel (JsonParser, JsonObject, JsonArray).
public List<Agent> parseAgents(String json) { try { JsonObject root = JsonParser.parseString(json).getAsJsonObject(); JsonArray data = root.getAsJsonArray("data"); List<Agent> agents = new ArrayList<>(); for (JsonElement el : data) { Agent agent = parseAgentObject(el.getAsJsonObject()); if (agent != null) agents.add(agent); } return agents; } catch (JsonSyntaxException | IllegalStateException e) { throw new RuntimeException("L'estructura del JSON d'agents ha canviat o és invàlida: " + e.getMessage(), e); }}Método especial parseRolesFromAgentsJson (línea 57): extrae los roles únicos del JSON de agentes. Existe porque los roles hay que insertarlos antes que los agentes (FK). Evita duplicados con roles.stream().noneMatch(...).
Helpers null-safe (línea 146) — clave para no petar con campos nulos del API:
private String getString(JsonObject obj, String key) { if (!obj.has(key) || obj.get(key).isJsonNull()) return null; return obj.get(key).getAsString();}3.4 Modelos
Agent.java: incluyeroleUuid(FK) yroleName(lo rellena el JOIN al leer),lastUpdated(Timestamp) y listaabilities.Ability.java: id, agentUuid, slot, displayName, description.Role.java: uuid, displayName, description.ValorantMap.java: se llama así porqueMapchocaba conjava.util.Map. Muchos campos + multiplicadoresdouble.
CRUD<T> en Valorant — diferencia con Escalada
public interface CRUD<T> { void create(T obj) throws DAOException; T read(String uuid) throws DAOException; void update(T obj) throws DAOException; void delete(String uuid) throws DAOException; List<T> getAll() throws DAOException;}read/delete recibe String porque las PK son UUIDs.
3.5 DAOs
Mismo patrón JDBC que Escalada (try-with-resources, PreparedStatement, mapRow), pero con novedades:
AgentDAO.java — text blocks, LEFT JOIN, upsert
Uso text blocks ("""...""") para SQL multilínea:
String sql = """ SELECT a.*, r.display_name AS role_name FROM agent a LEFT JOIN role r ON a.role_uuid = r.uuid WHERE a.uuid = ? """;existsByUuid(uuid) — comprobar existencia sin traer datos (para la copia parcial):
public boolean existsByUuid(String uuid) throws DAOException { String sql = "SELECT 1 FROM agent WHERE uuid = ?"; try (Connection conn = ds.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, uuid); try (ResultSet rs = ps.executeQuery()) { return rs.next(); } } catch (SQLException e) { throw new DAOException("Error al verificar l'agent: " + e.getMessage(), e); }}upsert(agent) — INSERT o UPDATE en una sola sentencia (lo que hace posible sincronizar idempotentemente):
String sql = """ INSERT INTO agent (uuid, display_name, description, developer_name, release_date, is_playable_character, is_available_for_test, is_base_content, role_uuid, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW()) ON CONFLICT (uuid) DO UPDATE SET display_name = EXCLUDED.display_name, description = EXCLUDED.description, developer_name = EXCLUDED.developer_name, release_date = EXCLUDED.release_date, is_playable_character = EXCLUDED.is_playable_character, is_available_for_test = EXCLUDED.is_available_for_test, is_base_content = EXCLUDED.is_base_content, role_uuid = EXCLUDED.role_uuid, last_updated = NOW() """;Si el uuid ya existe (ON CONFLICT), actualiza todos los campos usando EXCLUDED.campo (el valor que se intentaba insertar) y renueva last_updated.
parseReleaseDate — la API da fecha en ISO 8601 con zona ("1970-01-01T00:00:00Z"):
private Timestamp parseReleaseDate(String date) { if (date == null || date.isBlank()) return null; try { return Timestamp.from(OffsetDateTime.parse(date).toInstant()); } catch (Exception e) { return null; }}MapDAO.java
Igual que AgentDAO: CRUD + existsByUuid + upsert. Tiene setMapParams (línea 156) que pone los 14 parámetros, reutilizado por create y upsert (mismo orden). Evita duplicar código.
RoleDAO.java
CRUD + upsert. Usado para insertar los roles antes que los agentes durante la sincronización.
AbilityDAO.java — sin interfaz CRUD
Métodos: create (con RETURN_GENERATED_KEYS para el id SERIAL), getByAgent(agentUuid), deleteByAgent(agentUuid). Las abilities se gestionan en bloque por agente (borrar todas y recrear).
3.6 Controladores
Carpetas: Menus/, LlistarTots/, ActualitzarObjectes/, EliminarObjectes/, MostrarFontExterna/, Sincronitzar/.
MenuPrincipal
Instancia los DAO y también ValorantApiClient, ValorantJsonLoader, ValorantMapper:
AgentDAO agentDAO = new AgentDAO(ds);RoleDAO roleDAO = new RoleDAO(ds);AbilityDAO abilityDAO = new AbilityDAO(ds);MapDAO mapDAO = new MapDAO(ds);ValorantApiClient client = new ValorantApiClient();ValorantJsonLoader loader = new ValorantJsonLoader();ValorantMapper mapper = new ValorantMapper();MostrarAgentsEndpoint / MostrarAgentsJSON
Solo muestran los datos de la fuente externa (API o fichero). No tocan la BD. Llaman al client/loader → mapper.parseAgents(json) → imprimen.
SincronitzarAgents — la operación estrella
public static void executar(AgentDAO agentDAO, RoleDAO roleDAO, AbilityDAO abilityDAO, ValorantApiClient client, ValorantJsonLoader loader, ValorantMapper mapper) { View.mostrarMenuFontExterna(); int font = View.llegirEnter("Font: "); if (font == 0) return;
View.mostrarMenuSincronitzacio(); int tipus = View.llegirEnter("Tipus: "); if (tipus == 0) return;
String json; try { if (font == 1) { json = client.fetchAgents(); } else if (font == 2) { String path = View.llegirLiniaNoBuida("Ruta del fitxer JSON: "); json = loader.loadAgents(path); } else { return; } } catch (RuntimeException e) { View.missatge("Error en obtenir les dades: " + e.getMessage()); return; }
List<Agent> agents = mapper.parseAgents(json); List<Role> roles = mapper.parseRolesFromAgentsJson(json);
int afegits = 0, actualitzats = 0;
try { // Roles primero — agent tiene FK a role for (Role role : roles) roleDAO.upsert(role);
for (Agent agent : agents) { if (tipus == 1) { // Còpia parcial: ignora els agents que ja existeixen if (!agentDAO.existsByUuid(agent.getUuid())) { agentDAO.create(agent); if (agent.getAbilities() != null) { for (Ability ab : agent.getAbilities()) abilityDAO.create(ab); } afegits++; } } else if (tipus == 2) { // Còpia completa: upsert i reemplaça abilities agentDAO.upsert(agent); abilityDAO.deleteByAgent(agent.getUuid()); if (agent.getAbilities() != null) { for (Ability ab : agent.getAbilities()) abilityDAO.create(ab); } actualitzats++; } } } catch (DAOException e) { View.missatge("Error durant la sincronització: " + e.getMessage()); }}Diferencia parcial vs completa (clásico de examen):
- Parcial (tipo 1): añade solo lo que falta. Respeta lo existente.
- Completa (tipo 2):
upsertdel agente y reemplaza todas las abilities (delete + recrear).
Limitación honesta: no hay una transacción única envolviendo todo el bucle. Si falla a mitad queda sincronizado a medias. Mejora: setAutoCommit(false) sobre una conexión compartida + commit/rollback.
ActualitzarAgent
Lista los agentes, pide UUID, valida que existe, obtiene datos frescos de la fuente externa (API por UUID, o busca en el JSON con stream().filter(...).findFirst()), pide confirmación (s/n), hace update + reemplaza abilities (deleteByAgent + recrear).
EliminarAgent
Lista, pide UUID, confirma, delete. Las abilities caen automáticamente por ON DELETE CASCADE.
4. Diferencias clave entre los dos proyectos
| Aspecto | Escalada | Valorant |
|---|---|---|
| Origen de datos | Solo entrada manual + BD | API REST + JSON + BD |
| PK | int SERIAL autogenerado | String UUID (VARCHAR 36) |
CRUD.read/delete recibe | int | String |
| Librerías extra | — | Gson (JSON) |
| Operación destacada | Consultas SQL avanzadas (ConsultesDAO) | Sincronización + upsert |
| Fechas | Timestamp/Date ↔ String en el DAO | ISO 8601 → Timestamp con OffsetDateTime |
| HTTP | No | java.net.http.HttpClient |
| Capa extra | Controller/ConsultesPreecreades | Api/ + Controller/Sincronitzar |
| Común | JDBC, HikariCP, MVC+DAO, DAOException, try-with-resources, PreparedStatement | igual |
5. Banco de preguntas y respuestas cortas
- ¿Por qué
PreparedStatementy noStatement? Evita inyección SQL, parametriza tipos, se puede precompilar. - ¿Cómo recuperas el id autogenerado?
prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)+getGeneratedKeys(). - ¿Qué hace
try-with-resources? Cierra automáticamenteConnection/PreparedStatement/ResultSet→ devuelve la conexión al pool sin fugas. - ¿Para qué el pool HikariCP? Reutilizar conexiones (abrirlas es caro). Límite 5.
- ¿Qué es
DataSource? Interfaz estándar, fábrica de conexiones. Aquí la implementaHikariDataSource. - ¿Por qué una interfaz
CRUD<T>? Contrato común + genéricos → cada DAO la implementa con su tipo. - ¿Por qué
DAOExceptionpropia? EncapsulaSQLExceptiontécnica en una excepción del dominio con mensaje claro. El controlador no veSQLException. - ¿Qué es upsert y por qué en Valorant?
INSERT ... ON CONFLICT DO UPDATE. Permite sincronizar sin duplicar. - ¿Por qué insertar roles antes que agentes? Integridad referencial:
agent.role_uuides FK arole.uuid. - ¿Por qué
ValorantMapy noMap? Para no chocar conjava.util.Map. - ¿Dónde está la validación? En el controlador (formato, opciones) y en la BD (CHECK constraints, FK, NOT NULL).
- ¿Hay transacciones? No explícitas; cada DAO autoconfirma. Mejora: agrupar vía+llargs / agente+abilities en una transacción.
- ¿Cómo se evita NullPointerException con JSON? Helpers
getString/getBool/getDoubleque compruebanhaseisJsonNull. - ¿Qué patrón arquitectónico usáis? MVC + DAO.
- ¿Problema N+1? En
LlistarEscoles/LlistarAgentsse hace una consulta extra por cada fila (countVies, abilities). Mejora: JOIN o agregado. - ¿Qué es una API REST? Servicio web al que se le piden datos por HTTP. Devuelve JSON normalmente.
- ¿Qué hace
EXCLUDED.campoen un upsert? Es el valor que se intentaba insertar y que ha entrado en conflicto. - ¿Para qué
slf4j-nop? Silenciar warnings de logging de HikariCP. - ¿Qué pasa al borrar un agente? Por
ON DELETE CASCADE, se borran también sus abilities. - Flujo en una frase: Main abre el pool Hikari → MenuPrincipal crea los DAO → los controladores piden datos por la View, llaman a los DAO, que ejecutan SQL con
PreparedStatementsobre conexiones del pool y devuelven objetos del Model.
Si me bloqueo, vuelvo a la frase 20 y reconstruyo desde ahí. El resto son detalles de cada capa.