4164 palabras
21 minutos
Apuntes para el examen oral - Escalada, Valorant y teoría JDBC

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#

  1. Teoría general (lo que puede preguntar sin abrir el código)
  2. Proyecto Escalada
  3. Proyecto Valorant
  4. Diferencias clave entre los dos
  5. 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#

  1. Pedir una conexión (Connection).
  2. Crear una sentencia (PreparedStatement) con el SQL.
  3. Asignar parámetros (ps.setString(1, ...), ps.setInt(2, ...)).
  4. Ejecutar: executeQuery() para SELECT, executeUpdate() para INSERT/UPDATE/DELETE.
  5. Recorrer el ResultSet si hay datos.
  6. 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:

  1. Crear un HikariConfig con url, usuario, password, tamaño máximo…
  2. Crear un HikariDataSource(config).
  3. Pasar el DataSource a los DAO.
  4. Cada ds.getConnection() saca una conexión del pool.
  5. El try-with-resources la 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-plugin con mainClass = Main).

Comandos:

  • mvn compile → compila.
  • mvn exec:java → ejecuta Main.

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_id
escalador (1) ──< (N) ascensio >── (N) via

Definido en db/schema.sql:

  • escola (línea 9): id SERIAL PK, nom UNIQUE NOT NULL, lloc, aproximacio, popularitat con CHECK (baixa/mitjana/alta).
  • sector (línea 20): FK escola_idescola(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), FK sector_id CASCADE y FK creada_per_id SET NULL.
  • llarg (línea 76): FK via_id CASCADE.
  • ascensio (línea 90): FKs escalador_id y via_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 DataSource al 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 como String; la conversión a/desde Timestamp se hace en el DAO.
  • Sector.java: latitud/longitud son Float (objeto, admite null).
  • Ejemplo del toString de Escola:
@Override
public 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 con getGeneratedKeys() y se mete en el objeto.
  • La SQLException se envuelve en DAOException con 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#

  1. 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));
}
}
  1. actualitzarEstats() (línea 147): antes de leer/listar vías, ejecuta un UPDATE que pone a 'Apte' las vías cuyo data_fi_estat ya 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 con data_fi_estat > NOW().
  • viesPerRangDificultat(min, max) — usa array_position(...) BETWEEN ? AND ?.
  • escolesAmbRestriccionsActives — usa COUNT(...) 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/.

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 con Timestamp.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:

  1. Cliente HTTP para consumir API REST (java.net.http.HttpClient).
  2. Parseo de JSON con Gson.
  3. Upsert (INSERT ... ON CONFLICT DO UPDATE) para sincronizar.
  4. PKs tipo UUID (String), no enteros autogenerados.
  5. 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) ability
map (independiente)
  • role: uuid PK VARCHAR(36), display_name, description. Los 4 roles (Duelista, Controlador, Centinela, Iniciador).
  • agent: uuid PK, datos del agente, role_uuid FK → role ON DELETE SET NULL, y last_updated TIMESTAMP DEFAULT NOW().
  • ability: id SERIAL PK, agent_uuid FK → agent ON DELETE CASCADE, slot, display_name, description.
  • map: uuid PK, muchos campos descriptivos, multiplicadores de coordenadas y last_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, lanzo RuntimeException.
  • Al capturar InterruptedException restauro el flag con Thread.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: incluye roleUuid (FK) y roleName (lo rellena el JOIN al leer), lastUpdated (Timestamp) y lista abilities.
  • Ability.java: id, agentUuid, slot, displayName, description.
  • Role.java: uuid, displayName, description.
  • ValorantMap.java: se llama así porque Map chocaba con java.util.Map. Muchos campos + multiplicadores double.

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/.

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): upsert del 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#

AspectoEscaladaValorant
Origen de datosSolo entrada manual + BDAPI REST + JSON + BD
PKint SERIAL autogeneradoString UUID (VARCHAR 36)
CRUD.read/delete recibeintString
Librerías extraGson (JSON)
Operación destacadaConsultas SQL avanzadas (ConsultesDAO)Sincronización + upsert
FechasTimestamp/Date ↔ String en el DAOISO 8601 → Timestamp con OffsetDateTime
HTTPNojava.net.http.HttpClient
Capa extraController/ConsultesPreecreadesApi/ + Controller/Sincronitzar
ComúnJDBC, HikariCP, MVC+DAO, DAOException, try-with-resources, PreparedStatementigual

5. Banco de preguntas y respuestas cortas#

  1. ¿Por qué PreparedStatement y no Statement? Evita inyección SQL, parametriza tipos, se puede precompilar.
  2. ¿Cómo recuperas el id autogenerado? prepareStatement(sql, Statement.RETURN_GENERATED_KEYS) + getGeneratedKeys().
  3. ¿Qué hace try-with-resources? Cierra automáticamente Connection/PreparedStatement/ResultSet → devuelve la conexión al pool sin fugas.
  4. ¿Para qué el pool HikariCP? Reutilizar conexiones (abrirlas es caro). Límite 5.
  5. ¿Qué es DataSource? Interfaz estándar, fábrica de conexiones. Aquí la implementa HikariDataSource.
  6. ¿Por qué una interfaz CRUD<T>? Contrato común + genéricos → cada DAO la implementa con su tipo.
  7. ¿Por qué DAOException propia? Encapsula SQLException técnica en una excepción del dominio con mensaje claro. El controlador no ve SQLException.
  8. ¿Qué es upsert y por qué en Valorant? INSERT ... ON CONFLICT DO UPDATE. Permite sincronizar sin duplicar.
  9. ¿Por qué insertar roles antes que agentes? Integridad referencial: agent.role_uuid es FK a role.uuid.
  10. ¿Por qué ValorantMap y no Map? Para no chocar con java.util.Map.
  11. ¿Dónde está la validación? En el controlador (formato, opciones) y en la BD (CHECK constraints, FK, NOT NULL).
  12. ¿Hay transacciones? No explícitas; cada DAO autoconfirma. Mejora: agrupar vía+llargs / agente+abilities en una transacción.
  13. ¿Cómo se evita NullPointerException con JSON? Helpers getString/getBool/getDouble que comprueban has e isJsonNull.
  14. ¿Qué patrón arquitectónico usáis? MVC + DAO.
  15. ¿Problema N+1? En LlistarEscoles/LlistarAgents se hace una consulta extra por cada fila (countVies, abilities). Mejora: JOIN o agregado.
  16. ¿Qué es una API REST? Servicio web al que se le piden datos por HTTP. Devuelve JSON normalmente.
  17. ¿Qué hace EXCLUDED.campo en un upsert? Es el valor que se intentaba insertar y que ha entrado en conflicto.
  18. ¿Para qué slf4j-nop? Silenciar warnings de logging de HikariCP.
  19. ¿Qué pasa al borrar un agente? Por ON DELETE CASCADE, se borran también sus abilities.
  20. 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 PreparedStatement sobre 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.

Apuntes para el examen oral - Escalada, Valorant y teoría JDBC
https://blog.lucialv.com/posts/apuntes-examen-oral-escalada-valorant/
Autor
Lucía
Publicado el
2026-05-28
Licencia
CC BY-NC-SA 4.0