Salta el contingut

MySQL des de Java amb JDBC

1. Què és JDBC?

JDBC (Java Database Connectivity) és l'API estàndard de Java per connectar-se a bases de dades relacionals. Forma part del JDK i defineix un conjunt d'interfícies que tots els drivers de BD relacionals implementen.

Gràcies a JDBC, el codi que escriviu per a MySQL és molt similar al que usaríeu per a PostgreSQL o Oracle: canviant el driver i la URL de connexió, la resta del codi gairebé no canvia.

Les classes principals de JDBC estan al paquet java.sql:

Classe/Interfície Descripció
DriverManager Gestiona els drivers i crea connexions
Connection Representa una connexió activa a la BD
Statement Executa sentències SQL simples
PreparedStatement Executa sentències SQL parametritzades (recomanat)
ResultSet Conté els resultats d'una consulta SELECT

2. La Base de Dades de Pràctica

Treballarem amb una base de dades d'un fòrum amb dues taules relacionades:

SQL
1
2
3
4
5
-- Taula principal: publicacions del fòrum
publicacio (id, nick_que_publica, text, data_publicacio, paraules_clau)

-- Taula relacionada: comentaris a cada publicació
comentari (id, nick_comenta, text, valoracio, data_comentari, publicacio_id)

La relació és 1:M (una publicació pot tenir molts comentaris).

Per crear i poblar la base de dades, executeu a MySQL:

SQL
CREATE DATABASE forum;
USE forum;

CREATE TABLE publicacio (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nick_que_publica VARCHAR(50) NOT NULL,
    text TEXT NOT NULL,
    data_publicacio DATETIME NOT NULL,
    paraules_clau VARCHAR(200)
);

CREATE TABLE comentari (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nick_comenta VARCHAR(50) NOT NULL,
    text TEXT NOT NULL,
    valoracio INT CHECK (valoracio BETWEEN 1 AND 5),
    data_comentari DATETIME NOT NULL,
    publicacio_id INT NOT NULL,
    FOREIGN KEY (publicacio_id) REFERENCES publicacio(id)
);

I després executeu el fitxer import.sql per inserir les dades de prova. Tens enllaçats els arxius ací: crearBBDD.sql i import.sql

3. Connexió a MySQL

La URL de connexió JDBC per a MySQL té aquest format:

Text Only
jdbc:mysql://host:port/nom_base_de_dades?opcions

Exemple:

Java
1
2
3
String url  = "jdbc:mysql://localhost:3306/forum?useSSL=false&serverTimezone=UTC";
String user = "root";
String pass = "la_vostra_contrasenya";

serverTimezone

L'opció serverTimezone=UTC evita errors de zona horària que apareixen amb versions recents del connector MySQL.

4. READ: Consultar Dades amb SELECT

4.1. El ResultSet

Quan executem un SELECT, JDBC ens retorna un ResultSet. Penseu-lo com un cursor que apunta inicialment abans de la primera fila. Hem d'anar avançant fila a fila amb next().

Text Only
ResultSet:
┌──────────────────────────────────────────┐
│  → (cursor inicial, abans de la 1a fila) │
├──────────────────────────────────────────┤
│  fila 1: id=1, nick="usuari_joan", ...   │
├──────────────────────────────────────────┤
│  fila 2: id=2, nick="maria_dev", ...     │
├──────────────────────────────────────────┤
│  fila 3: ...                             │
└──────────────────────────────────────────┘
Text Only
1
2
3
4
5
6
7
8
ResultSet:
┌──────────────────────────────────────────┐
│  → fila 1: id=1, nick="usuari_joan", ... │
├──────────────────────────────────────────┤
│  fila 2: id=2, nick="maria_dev", ...     │
├──────────────────────────────────────────┤
│  fila 3: ...                             │
└──────────────────────────────────────────┘
Text Only
1
2
3
4
5
6
7
8
ResultSet:
┌──────────────────────────────────────────┐
│  fila 1: id=1, nick="usuari_joan", ...   │
├──────────────────────────────────────────┤
│  → fila 2: id=2, nick="maria_dev", ...   │
├──────────────────────────────────────────┤
│  fila 3: ...                             │
└──────────────────────────────────────────┘
Text Only
ResultSet:
┌──────────────────────────────────────────┐
│  fila 1: id=1, nick="usuari_joan", ...   │
├──────────────────────────────────────────┤
│  fila 2: id=2, nick="maria_dev", ...     │
├──────────────────────────────────────────┤
│  fila 3: ...                             │
├──────────────────────────────────────────┤
│  → (cursor final, fora de les dades)     │
└──────────────────────────────────────────┘

Cada crida a rs.next() avança el cursor una posició i retorna true si hi ha fila, o false si hem arribat al final.

Per llegir els valors de cada columna usem mètodes get...() indicant el nom de la columna o la seva posició (1-indexada):

Java
1
2
3
4
rs.getString("nick_que_publica")  // per nom de columna
rs.getString(2)                   // per posició (la 2a columna)
rs.getInt("valoracio")
rs.getTimestamp("data_publicacio")

4.2. Exemple: Llistar totes les publicacions

Java
import java.sql.*;

public class LlistarPublicacions {
    public static void main(String[] args) {

        // Dades de connexió
        String url  = "jdbc:mysql://localhost:3306/forum?useSSL=false&serverTimezone=UTC";
        String user = "root";
        String pass = "1234";

        // SQL que volem executar
        String sql = "SELECT id, nick_que_publica, text, data_publicacio FROM publicacio ORDER BY data_publicacio";

        // try-with-resources: tanca Connection i Statement automàticament
        try (Connection conn = DriverManager.getConnection(url, user, pass);
             Statement  stmt = conn.createStatement();
             ResultSet  rs   = stmt.executeQuery(sql)) {

            // Iterem fila a fila pel ResultSet
            while (rs.next()) {
                int    id   = rs.getInt("id");
                String nick = rs.getString("nick_que_publica");
                String text = rs.getString("text");
                String data = rs.getString("data_publicacio");

                System.out.printf("[%d] %s (%s)%n", id, nick, data);
                System.out.println("     " + text);
                System.out.println();
            }

        } catch (SQLException e) {
            // Mostrem el codi d'error SQL i el missatge
            System.err.println("Error SQL " + e.getErrorCode() + ": " + e.getMessage());
        }
    }
}

5. PreparedStatement: Consultes amb Paràmetres

Quan la consulta depèn de valors que provenen de l'usuari (o de variables), mai hem de concatenar strings directament. Això podria causar un SQL Injection (atac de seguretat).

Java
1
2
3
4
5
// MAL: vulnerable a SQL Injection!
String sql = "SELECT * FROM publicacio WHERE nick_que_publica = '" + nick + "'";

// BÉ: usar PreparedStatement amb paràmetres (?)
String sql = "SELECT * FROM publicacio WHERE nick_que_publica = ?";

El PreparedStatement usa ? com a marcadors de posició i després omplim els valors amb els mètodes set...():

Java
String sql = "SELECT * FROM publicacio WHERE nick_que_publica = ?";

try (Connection conn = DriverManager.getConnection(url, user, pass);
     PreparedStatement pstmt = conn.prepareStatement(sql)) {

    // Posem el valor al primer '?' (posició 1)
    pstmt.setString(1, "maria_dev");

    // Executem i processem el ResultSet
    try (ResultSet rs = pstmt.executeQuery()) {
        while (rs.next()) {
            System.out.println(rs.getString("text"));
        }
    }

} catch (SQLException e) {
    System.err.println("Error: " + e.getMessage());
}

5.1. Exemple: Buscar publicacions per paraula clau

Java
import java.sql.*;

public class BuscarPerParaulaClau {
    public static void main(String[] args) {

        String url  = "jdbc:mysql://localhost:3306/forum?useSSL=false&serverTimezone=UTC";
        String user = "root";
        String pass = "1234";

        // Busquem publicacions que continguin "spring" a les paraules clau
        String paraula = "spring";

        // LIKE amb % per buscar que contingui la paraula
        String sql = "SELECT id, nick_que_publica, text FROM publicacio WHERE paraules_clau LIKE ?";

        try (Connection conn = DriverManager.getConnection(url, user, pass);
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            // Establim el paràmetre: %spring% (qualsevol cosa, spring, qualsevol cosa)
            pstmt.setString(1, "%" + paraula + "%");

            try (ResultSet rs = pstmt.executeQuery()) {
                System.out.println("Publicacions amb paraula clau '" + paraula + "':");
                System.out.println("=".repeat(50));

                while (rs.next()) {
                    System.out.printf("  [%d] %s%n", rs.getInt("id"), rs.getString("nick_que_publica"));
                    System.out.println("       " + rs.getString("text"));
                }
            }

        } catch (SQLException e) {
            System.err.println("Error: " + e.getMessage());
        }
    }
}

6. CREATE: Inserir Dades amb INSERT

Per inserir dades usem executeUpdate() en lloc de executeQuery(). Aquest mètode retorna el nombre de files afectades.

Java
import java.sql.*;

public class AfegirPublicacio {
    public static void main(String[] args) {

        String url  = "jdbc:mysql://localhost:3306/forum?useSSL=false&serverTimezone=UTC";
        String user = "root";
        String pass = "1234";

        // INSERT amb paràmetres per a cada valor que volem inserir
        String sql = "INSERT INTO publicacio (nick_que_publica, text, data_publicacio, paraules_clau) "
                   + "VALUES (?, ?, NOW(), ?)";

        // Statement.RETURN_GENERATED_KEYS indica que volem obtenir l'id generat
        try (Connection conn = DriverManager.getConnection(url, user, pass);
             PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {

            // Omplim els paràmetres en ordre (posicions 1, 2, 3)
            pstmt.setString(1, "nou_usuari");
            pstmt.setString(2, "Primera publicació al fòrum! Hola a tothom.");
            pstmt.setString(3, "presentacio,java");

            // executeUpdate() retorna el nombre de files inserides
            int filesAfectades = pstmt.executeUpdate();
            System.out.println("Files inserides: " + filesAfectades);

            // Recuperem l'ID generat automàticament per MySQL (AUTO_INCREMENT)
            try (ResultSet claus = pstmt.getGeneratedKeys()) {
                if (claus.next()) {
                    int idNou = claus.getInt(1);
                    System.out.println("Nova publicació creada amb id: " + idNou);
                }
            }

        } catch (SQLException e) {
            System.err.println("Error en inserir: " + e.getMessage());
        }
    }
}

7. UPDATE: Modificar Dades

Java
import java.sql.*;

public class ActualitzarParaulesClau {
    public static void main(String[] args) {

        String url  = "jdbc:mysql://localhost:3306/forum?useSSL=false&serverTimezone=UTC";
        String user = "root";
        String pass = "1234";

        // Actualitzem les paraules clau d'una publicació concreta
        String sql = "UPDATE publicacio SET paraules_clau = ? WHERE id = ?";

        try (Connection conn = DriverManager.getConnection(url, user, pass);
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            // Primer paràmetre: el nou valor
            pstmt.setString(1, "spring,java,backend,tutorial");
            // Segon paràmetre: l'id de la publicació a modificar
            pstmt.setInt(2, 1);

            int filesAfectades = pstmt.executeUpdate();

            if (filesAfectades > 0) {
                System.out.println("Publicació actualitzada correctament.");
            } else {
                System.out.println("No s'ha trobat cap publicació amb aquest id.");
            }

        } catch (SQLException e) {
            System.err.println("Error en actualitzar: " + e.getMessage());
        }
    }
}

8. DELETE: Eliminar Dades

Java
import java.sql.*;

public class EliminarComentari {
    public static void main(String[] args) {

        String url  = "jdbc:mysql://localhost:3306/forum?useSSL=false&serverTimezone=UTC";
        String user = "root";
        String pass = "1234";

        int idComentariAEliminar = 3;

        String sql = "DELETE FROM comentari WHERE id = ?";

        try (Connection conn = DriverManager.getConnection(url, user, pass);
             PreparedStatement pstmt = conn.prepareStatement(sql)) {

            pstmt.setInt(1, idComentariAEliminar);

            int filesAfectades = pstmt.executeUpdate();

            if (filesAfectades > 0) {
                System.out.println("Comentari " + idComentariAEliminar + " eliminat.");
            } else {
                System.out.println("No s'ha trobat el comentari " + idComentariAEliminar);
            }

        } catch (SQLException e) {
            System.err.println("Error en eliminar: " + e.getMessage());
        }
    }
}

9. Consulta amb JOIN: Publicació i els seus Comentaris

Un dels punts forts de les bases de dades relacionals és poder unir taules. Veiem com obtenir una publicació amb tots els seus comentaris:

Java
import java.sql.*;

public class PublicacioAmbComentaris {
    public static void main(String[] args) {

        String url  = "jdbc:mysql://localhost:3306/forum?useSSL=false&serverTimezone=UTC";
        String user = "root";
        String pass = "1234";

        int idPublicacio = 1;

        // Primer obtenim la publicació
        String sqlPub = "SELECT * FROM publicacio WHERE id = ?";
        // Després obtenim tots els comentaris d'aquesta publicació, ordenats per data
        String sqlCom = "SELECT * FROM comentari WHERE publicacio_id = ? ORDER BY data_comentari";

        try (Connection conn = DriverManager.getConnection(url, user, pass)) {

            // --- Llegim la publicació ---
            try (PreparedStatement pstmt = conn.prepareStatement(sqlPub)) {
                pstmt.setInt(1, idPublicacio);
                try (ResultSet rs = pstmt.executeQuery()) {
                    if (rs.next()) {
                        System.out.println("=== PUBLICACIÓ ===");
                        System.out.println("Nick: " + rs.getString("nick_que_publica"));
                        System.out.println("Data: " + rs.getString("data_publicacio"));
                        System.out.println("Text: " + rs.getString("text"));
                        System.out.println("Tags: " + rs.getString("paraules_clau"));
                    }
                }
            }

            System.out.println();
            System.out.println("--- COMENTARIS ---");

            // --- Llegim els comentaris ---
            try (PreparedStatement pstmt = conn.prepareStatement(sqlCom)) {
                pstmt.setInt(1, idPublicacio);
                try (ResultSet rs = pstmt.executeQuery()) {
                    while (rs.next()) {
                        String nick      = rs.getString("nick_comenta");
                        String text      = rs.getString("text");
                        int    valoracio = rs.getInt("valoracio");
                        String data      = rs.getString("data_comentari");

                        // Representem la valoració amb estrelles
                        String estrelles = "★".repeat(valoracio) + "☆".repeat(5 - valoracio);

                        System.out.printf("%s  [%s] (%s)%n", estrelles, nick, data);
                        System.out.println("  " + text);
                    }
                }
            }

        } catch (SQLException e) {
            System.err.println("Error: " + e.getMessage());
        }
    }
}

10. Resum dels Mètodes Principals

10.1. Connection

Mètode Descripció
createStatement() Crea un Statement per a SQL sense paràmetres
prepareStatement(sql) Crea un PreparedStatement per a SQL amb ?
close() Tanca la connexió (automàtic amb try-with-resources)

10.2. PreparedStatement / Statement

Mètode Descripció
executeQuery() Executa SELECT, retorna ResultSet
executeUpdate() Executa INSERT/UPDATE/DELETE, retorna files afectades
setString(pos, val) Estableix un paràmetre de tipus String
setInt(pos, val) Estableix un paràmetre de tipus int
setDouble(pos, val) Estableix un paràmetre de tipus double
setTimestamp(pos, val) Estableix un paràmetre de tipus data/hora

10.3. ResultSet

Mètode Descripció
next() Avança al següent registre. Retorna false al final
getString(col) Llegeix una columna com a String
getInt(col) Llegeix una columna com a int
getDouble(col) Llegeix una columna com a double
getTimestamp(col) Llegeix una columna com a Timestamp
wasNull() Comprova si el darrer valor llegit era NULL