erDiagram
LLIBRE {
int id PK
string titol
string isbn
int any_publicacio
int disponible
}
19 Gestió de bases de dades amb JDBC
Objectius
- Explicar què és JDBC i quin paper hi juga el driver.
- Obrir i tancar una connexió a una base de dades amb
DriverManageriConnection. - Escriure SQL bàsic:
CREATE TABLE,SELECT,INSERT,UPDATE,DELETE. - Distingir
StatementdePreparedStatementi saber per què el segon és més segur. - Recórrer resultats amb
ResultSet. - Usar SQLite com a base de dades d’exemple.
- Organitzar l’accés a dades amb el patró DAO.
Al capítol anterior vam fer sobreviure les dades amb serialització. Ara farem el salt a una de les opcions utilitzada a nivell empresarial: guardar la informació en una base de dades relacional i accedir-hi des de Java amb JDBC.
19.1 Bases de dades relacionals
Una base de dades relacional organitza la informació en taules.
Cada taula té columnes (els camps) i files (els registres). És com un full de càlcul, però amb regles estrictes de tipus i relacions.
Per parlar amb la base de dades fem servir SQL (Structured Query Language), un llenguatge estàndard per crear taules, inserir dades i consultar-les.
19.2 Què és JDBC?
JDBC (Java Database Connectivity) és l’API estàndard de Java per connectar-se a bases de dades.
La gràcia és que ofereix les mateixes classes i mètodes (Connection, Statement, ResultSet…) independentment de la base de dades utilitzada.
La peça que tradueix aquestes crides al dialecte concret de cada base de dades és el driver.
flowchart LR
A["Programa Java"] --> B["API JDBC<br/>(java.sql)"]
B --> C["Driver SQLite"]
B --> D["Driver MySQL"]
B --> E["Driver PostgreSQL"]
C --> C1[("SQLite")]
D --> D1[("MySQL")]
E --> E1[("PostgreSQL")]
Al programar amb JDBC i no direcament contra una base de dades concreta, pots canviar de SGBD canviant només el driver i l’URL de connexió, sense reescriure tota la lògica.
Aquesta és la mateixa idea de programar contra interfícies.
19.3 SQLite
Una opció senzilla per començar és SQLite ja que:
- La base de dades sencera és un únic fitxer (
biblioteca.db). - No cal instal·lar cap servidor: el motor va dins la mateixa aplicació.
- Fa servir SQL estàndard.
Java ens proporciona les classes Connection, DriverManager, PreparedStatement i ResultSet però no els drivers en sí.
Per tant, necessitem una biblioteca externa. Les opcions que tenim per utilitzar-la són:
- Descarregar el .jar manualment i importar-lo al projecte (classpath).
- Utilitzar Maven.
Per una introducció al tema, qualsevol de les dues en serveix, però l’estàndard a dia d’avui o la recomanació és Maven.
Per això, hem d’ afegir el driver com a dependència al pom.xml:
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.46.0.0</version>
</dependency>19.4 La connexió
Tot comença obtenint una Connection. La demanem al DriverManager passant-li una URL de connexió, l’adreça de la base de dades.
import java.sql.*;
public class ProvaConnexio {
public static void main(String[] args) {
// Per a SQLite, l'URL és jdbc:sqlite: + ruta del fitxer
String url = "jdbc:sqlite:biblioteca.db";
try (Connection conn = DriverManager.getConnection(url)) {
System.out.println("Connexió establerta correctament!");
} catch (SQLException e) {
System.out.println("Error de connexió: " + e.getMessage());
}
}
}Connection, Statement, PreparedStatement i ResultSet són recursos que cal tancar.
Si no els tanques, deixes connexions obertes i pots esgotar-les.
El try-with-resources (try (...) { }) els tanca automàticament encara que hi hagi una excepció. Fes-lo servir sempre.
L’URL de connexió canvia segons el SGBD:
| SGBD | URL de connexió (exemple) |
|---|---|
| SQLite | jdbc:sqlite:biblioteca.db |
| MySQL | jdbc:mysql://localhost:3306/biblioteca |
| PostgreSQL | jdbc:postgresql://localhost:5432/biblioteca |
19.5 SQL bàsic des de Java
Recorrem les operacions fonamentals. En conjunt, les operacions de crear, llegir, actualitzar i esborrar es coneixen com a CRUD (Create, Read, Update, Delete).
flowchart LR
C["INSERT<br/>(Create)"] --- R["SELECT<br/>(Read)"] --- U["UPDATE<br/>(Update)"] --- D["DELETE<br/>(Delete)"]
Crear una taula
Les sentències que no retornen files (crear taules, inserir, actualitzar, esborrar) s’executen amb executeUpdate (o execute per a DDL).
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:biblioteca.db");
Statement stmt = conn.createStatement()) {
String sql = """
CREATE TABLE IF NOT EXISTS llibre (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titol TEXT NOT NULL,
isbn TEXT UNIQUE,
any_publicacio INTEGER,
disponible INTEGER DEFAULT 1
)
""";
stmt.execute(sql);
System.out.println("Taula creada (o ja existent).");
} catch (SQLException e) {
System.out.println("Error: " + e.getMessage());
}IF NOT EXISTS evita l’error si la taula ja existeix. PRIMARY KEY AUTOINCREMENT fa que l’id es generi sol.
19.5.1 Inserir dades
Aquí introduïm ja la manera correcta de passar valors: amb un PreparedStatement.
Els signes ? són paràmetres que omplim després.
String sql = "INSERT INTO llibre (titol, isbn, any_publicacio) VALUES (?, ?, ?)";
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:biblioteca.db");
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, "El nom del vent"); // Primer ?
ps.setString(2, "978-8401352836"); // Segon ?
ps.setInt(3, 2007); // Tercer ?
int files = ps.executeUpdate(); // Retorna el número de files afectades
System.out.println(files + " fila inserida.");
} catch (SQLException e) {
System.out.println("Error en inserir: " + e.getMessage());
}A JDBC, els paràmetres (setString(1, ...)) i les columnes del ResultSet es numeren des de l’1, no des del 0 com als arrays.
És una de les poques coses de Java que comencen a comptar per 1.
19.5.2 Consultar dades
Les consultes que retornen files s’executen amb executeQuery, que ens dona un ResultSet: un cursor que recorrem fila a fila amb next().
String sql = "SELECT id, titol, any_publicacio FROM llibre WHERE disponible = 1";
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:biblioteca.db");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) { // Avança a la fila següent
int id = rs.getInt("id");
String titol = rs.getString("titol");
int any = rs.getInt("any_publicacio");
System.out.println(id + " - " + titol + " (" + any + ")");
}
} catch (SQLException e) {
System.out.println("Error en consultar: " + e.getMessage());
}flowchart TD
A["executeQuery()"] --> B{"rs.next()?"}
B -->|"cert"| C["llegir columnes<br/>getInt/getString..."] --> B
B -->|"fals"| D["fi del recorregut"]
next()
Un ResultSet acabat de crear està col·locat abans de la primera fila.
Si crides rs.getString(...) sense haver cridat rs.next() primer, obtindràs una SQLException.
Sempre while (rs.next()) { ... } o if (rs.next()) { ... }.
19.5.3 Actualitzar i esborrar
Segueixen el mateix patró que INSERT: PreparedStatement + executeUpdate.
// Marcar un llibre com a no disponible (prestat)
String sqlUpdate = "UPDATE llibre SET disponible = 0 WHERE id = ?";
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:biblioteca.db");
PreparedStatement ps = conn.prepareStatement(sqlUpdate)) {
ps.setInt(1, 3);
int files = ps.executeUpdate();
System.out.println(files + " llibre actualitzat.");
} catch (SQLException e) {
System.out.println("Error: " + e.getMessage());
}
// Esborrar un llibre
String sqlDelete = "DELETE FROM llibre WHERE id = ?";
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:biblioteca.db");
PreparedStatement ps = conn.prepareStatement(sqlDelete)) {
ps.setInt(1, 3);
ps.executeUpdate();
} catch (SQLException e) {
System.out.println("Error: " + e.getMessage());
}Comparativa entre Statement i PreparedStatement
Tots dos executen SQL, però tenen diferències importants.
| Aspecte | Statement |
PreparedStatement |
|---|---|---|
| Com passa els valors | Concatenant text a la SQL | Amb paràmetres ? i setXxx |
| Seguretat | Vulnerable a injecció SQL | Protegit: els valors s’escapen sols |
| Rendiment | Es compila cada cop | Es precompila (millor si es repeteix) |
| Tipus de dades | Ho poses tot com a text | setInt, setString, setDouble… |
| Quan usar-lo | SQL fix sense valors variables | Sempre que hi hagi valors variables |
Per evitar la injecció de SQL cal utilitzar PreparedStatement.
Imagina que construeixes la consulta enganxant text que ve de l’usuari:
// MAI facis això!
String nom = /* text escrit per l'usuari */;
String sql = "SELECT * FROM usuari WHERE nom = '" + nom + "'";Si l’usuari escriu x' OR '1'='1, la consulta es converteix en ... WHERE nom = 'x' OR '1'='1', que és sempre certa i retorna tots els usuaris.
Això és una injecció SQL, una de les vulnerabilitats més comunes.
Fes servir sempre PreparedStatement per a qualsevol valor que vingui de l’usuari o d’una variable.
Amb ps.setString(1, nom), el text de l’usuari es tracta com a dada, mai com a codi SQL. És més segur i, a més, més net.
19.6 El patró DAO
Escampar codi SQL per tot el programa és una mala idea: si canvies la base de dades, has de tocar-ho tot.
El patró DAO (Data Access Object) separa la lògica de l’aplicació de l’accés a dades.
La idea: per a cada entitat (Llibre) creem un objecte DAO (LlibreDAO) que ofereix mètodes com inserir, cercaPerId, llistarTots, actualitzar, esborrar.
La resta del programa treballa amb objectes Llibre i no veu mai el SQL.
flowchart LR
A["Aplicació<br/>(menú, lògica)"] -->|"objectes Llibre"| B["LlibreDAO"]
B -->|"SQL via JDBC"| C[("biblioteca.db")]
La classe de domini
public class Llibre {
private int id;
private String titol;
private String isbn;
private int anyPublicacio;
public Llibre(int id, String titol, String isbn, int anyPublicacio) {
this.id = id;
this.titol = titol;
this.isbn = isbn;
this.anyPublicacio = anyPublicacio;
}
// getters i setters omesos per brevetat
public int getId() { return id; }
public String getTitol() { return titol; }
public String getIsbn() { return isbn; }
public int getAnyPublicacio() { return anyPublicacio; }
}La classe DAO del llibre
import java.sql.*;
import java.util.*;
public class LlibreDAO {
private final String url = "jdbc:sqlite:biblioteca.db";
private Connection connectar() throws SQLException {
return DriverManager.getConnection(url);
}
public void inserir(Llibre llibre) {
String sql = "INSERT INTO llibre (titol, isbn, any_publicacio) VALUES (?, ?, ?)";
try (Connection conn = connectar();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, llibre.getTitol());
ps.setString(2, llibre.getIsbn());
ps.setInt(3, llibre.getAnyPublicacio());
ps.executeUpdate();
} catch (SQLException e) {
System.out.println("Error en inserir: " + e.getMessage());
}
}
public List<Llibre> llistarTots() {
List<Llibre> llibres = new ArrayList<>();
String sql = "SELECT id, titol, isbn, any_publicacio FROM llibre";
try (Connection conn = connectar();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
llibres.add(new Llibre(
rs.getInt("id"),
rs.getString("titol"),
rs.getString("isbn"),
rs.getInt("any_publicacio")
));
}
} catch (SQLException e) {
System.out.println("Error en llistar: " + e.getMessage());
}
return llibres;
}
public Llibre cercaPerId(int id) {
String sql = "SELECT id, titol, isbn, any_publicacio FROM llibre WHERE id = ?";
try (Connection conn = connectar();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return new Llibre(
rs.getInt("id"),
rs.getString("titol"),
rs.getString("isbn"),
rs.getInt("any_publicacio")
);
}
}
} catch (SQLException e) {
System.out.println("Error en cercar: " + e.getMessage());
}
return null; // no trobat
}
public void esborrar(int id) {
String sql = "DELETE FROM llibre WHERE id = ?";
try (Connection conn = connectar();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, id);
ps.executeUpdate();
} catch (SQLException e) {
System.out.println("Error en esborrar: " + e.getMessage());
}
}
}Ús del DAO des de l’aplicació
Fixa’t que el programa principal treballa amb objectes i no escriu ni una línia de SQL:
public class App {
public static void main(String[] args) {
LlibreDAO dao = new LlibreDAO();
dao.inserir(new Llibre(0, "Solaris", "978-8433920423", 1961));
System.out.println("--- Catàleg ---");
for (Llibre l : dao.llistarTots()) {
System.out.println(l.getId() + ": " + l.getTitol());
}
}
}El projecte BiblioTech fa servir exactament aquest patró: una classe de domini per cada entitat i el seu DAO corresponent, amb SQLite com a magatzem.
Així la lògica del menú de consola queda neta i el dia de demà es podria canviar a MySQL tocant només els DAO.
19.7 Errors típics
- No tancar recursos → usa
try-with-resourcessempre. - Llegir el
ResultSetsensenext()→ ens donarà unaSQLException. - Numerar paràmetres/columnes des del 0 → a JDBC comencen per 1.
- Concatenar valors amb
+a la SQL → risc d’injecció; usaPreparedStatement. - Oblidar el driver al
pom.xml** →SQLException: No suitable driver found. - Confondre
executeQuery(SELECT) ambexecuteUpdate(INSERT/UPDATE/DELETE).
19.8 Mini-exercicis
- Afegeix al
LlibreDAOel mètodeactualitzar(Llibre)que modifiqui títol, ISBN i any a partir de l’id. - Escriu una consulta
SELECTambPreparedStatementque retorni els llibres publicats després d’un any donat com a paràmetre. - Crea una taula
soci(id, nom, carnet) i el seuSociDAOamb els mètodes CRUD complets. - Explica per què
PreparedStatementprotegeix contra la injecció SQL, amb un exemple d’entrada maliciosa.
19.9 Resum
- JDBC és l’API estàndard de Java per a bases de dades; el driver l’adapta a cada SGBD.
- Obtens una
ConnectionambDriverManageri la tanques ambtry-with-resources. - CRUD =
INSERT/SELECT/UPDATE/DELETE;executeUpdateper modificar,executeQuery+ResultSetper consultar. - Usa
PreparedStatementsempre que hi hagi valors variables (seguretat i rendiment). - El patró DAO separa la lògica de l’aplicació de l’accés a dades.
- SQLite (un sol fitxer, sense servidor) és ideal per aprendre.