Interazione con il DBMS MySQL

LT
Luca Terribili
Autore

Java è una delle tecnologie più diffuse per lo sviluppo di applicazioni enterprise, grazie alla sua capacità di interagire in modo efficace con i diversi sistemi di gestione dei dati. In particolare, quando si lavora con MySQL, la combinazione di Java e JDBC (Java Database Connectivity) consente di eseguire query, aggiornare record e gestire transazioni in maniera semplice e sicura. Questo articolo ti guiderà passo‑passo nella configurazione dell’ambiente, nella creazione di una connessione stabile e nell’esecuzione di una query di esempio.

Nel corso della lettura troverai indicazioni pratiche, frammenti di codice pronti all’uso e consigli per evitare gli errori più comuni. Ogni sezione è arricchita da approfondimenti che ti aiuteranno a comprendere non solo il come, ma anche il perché di ciascuna operazione.

Alla fine dell’articolo avrai a disposizione tutti gli strumenti necessari per integrare Java con un database MySQL, pronto per la realizzazione di progetti più complessi e performanti.

Passo 1: Installa il driver JDBC di MySQL

Il driver JDBC è il ponte che permette a Java di comunicare con il server MySQL. Senza di esso, il tuo programma non sarebbe in grado di aprire una connessione né di inviare comandi SQL.

Puoi scaricare il driver ufficiale dal sito di MySQL nella sezione Connector/J. Dopo averlo scaricato, estrai il file .jar e copialo nella directory lib del tuo progetto o aggiungilo al classpath del tuo IDE (Eclipse, IntelliJ, NetBeans). Se utilizzi un sistema di build come Maven o Gradle, è possibile includere il driver come dipendenza, semplificando ulteriormente la gestione delle librerie.

Una volta inserito il JAR, verifica che il tuo progetto lo riconosca correttamente compilando un semplice programma di test. Se la compilazione riesce senza errori, il driver è pronto per essere utilizzato nella fase successiva.

Un dettaglio che evita una fonte di errori molto comune: non serve chiamare Class.forName("com.mysql.cj.jdbc.Driver"). Connector/J si registra da solo all’avvio della JVM tramite il meccanismo standard ServiceLoader della specifica JDBC. Il caricamento manuale è un residuo delle versioni 5.x: se lo trovi in un tutorial, è un segnale che il codice è vecchio.

Passo 2: Configura la connessione al database

Per stabilire una connessione, è necessario fornire a Java tre informazioni fondamentali: l’URL del database, il nome utente e la password. Il driver da caricare non è una quarta informazione, perché si registra da solo.

Queste impostazioni vengono solitamente racchiuse in una classe di utilità, in modo da centralizzare la gestione della connessione. Qui di seguito trovi un esempio di classe DatabaseConnection che non tiene la connessione in una variabile statica.

Il pattern Singleton su Connection, diffuso nei tutorial di base, è infatti sbagliato, e per tre motivi concreti:

  • Connection non è thread-safe: condividerla tra thread significa che due richieste simultanee usano la stessa sessione e gli stessi statement, con interferenze sui risultati e sugli autocommit.
  • Una volta chiusa, close() non è reversibile: se la connessione statica è chiusa e in seguito si richiama getConnection(), la variabile non è null, quindi viene restituita la connessione già chiusa e la prima operazione solleva SQLException: No operations allowed after connection closed.
  • Un metodo che aprirebbe la connessione non può propagare l’errore se lo gestisce internamente: se la connessione fallisce restituirebbe null, lasciando il chiamante esposto a un NullPointerException che si manifesta molto più lontano dal punto in cui il problema si è verificato.

La connessione, insomma, è una risorsa con un ciclo di vita: si apre, si usa, si chiude. Questo è ciò che il try‑with‑resources (Java 7) esprime già nel linguaggio.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public final class DatabaseConnection {

    private DatabaseConnection() {
    }

    public static Connection open() throws SQLException {
        String url = requireEnv("DB_URL");
        String username = requireEnv("DB_USER");
        String password = requireEnv("DB_PASSWORD");

        return DriverManager.getConnection(url, username, password);
    }

    private static String requireEnv(String name) {
        String value = System.getenv(name);
        if (value == null || value.isBlank()) {
            throw new IllegalStateException("Variabile d'ambiente mancante: " + name);
        }
        return value;
    }
}

Tre scelte da notare. Il metodo dichiara throws SQLException invece di catturare l’eccezione e restituire null: chi chiama decide cosa fare e il contesto dell’errore resta intatto. Le credenziali arrivano da variabili d’ambiente, con un controllo esplicito, in modo che un valore mancante fallisca subito e con un messaggio chiaro invece che a metà dell’elaborazione. Infine il metodo restituisce un’istanza nuova ogni volta, quindi non c’è stato nascosto e non ci sono condizioni di gara.

Sostituisci mydatabase, app_user e la password nei valori corrispondenti al tuo ambiente:

export DB_URL="jdbc:mysql://localhost:3306/mydatabase?useSSL=false&serverTimezone=UTC"
export DB_USER="app_user"
export DB_PASSWORD="una-password-che-non-e-nel-repository"

È buona pratica gestire le credenziali in maniera sicura, ad esempio utilizzando file di configurazione esterni o variabili d’ambiente, per evitare di esporle nel codice sorgente. Una password scritta nel sorgente finisce nel repository Git e nelle copie di lavoro di chiunque ne abbia accesso.

Passo 3: Esegui la query

Una volta ottenuta la connessione, puoi creare un oggetto Statement o PreparedStatement per inviare le tue query SQL al server MySQL. Nella maggior parte dei casi è consigliato PreparedStatement: i parametri viengono inviati su un canale separato dal testo SQL, il che rende impossibile che un valore finisca nella query, e previene quindi le SQL injection. Per operazioni di lettura (SELECT) senza parametri anche Statement va bene.

Il seguente esempio mostra come eseguire una semplice query con un parametro e stampare a console i risultati. Viene illustrato l’uso di ResultSet per iterare sulle righe restituite e recuperare i valori delle colonne tramite i metodi getInt, getString, ecc. Le tre risorse sono dichiarate nel try‑with‑resources e vengono chiuse automaticamente, anche in caso di errore.

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class DatabaseQuery {
    private static final String SELECT_BY_NAME = "SELECT id, name FROM mytable WHERE name LIKE ?";

    public static void main(String[] args) {
        try (Connection connection = DatabaseConnection.open();
             PreparedStatement statement = connection.prepareStatement(SELECT_BY_NAME)) {

            statement.setString(1, "%");

            try (ResultSet resultSet = statement.executeQuery()) {
                while (resultSet.next()) {
                    int id = resultSet.getInt("id");
                    String name = resultSet.getString("name");
                    System.out.println("Id: " + id + ", Name: " + name);
                }
            }
        } catch (SQLException e) {
            System.err.println("Errore durante l'esecuzione della query:");
            e.printStackTrace();
        }
    }
}

Il try‑with‑resources chiude le risorse in ordine inverso, anche se il corpo del blocco solleva un’eccezione o esegue un return. Nelle versioni 7 e 8 un errore durante la chiusura veniva ignorato in silenzio; dalla versione 9 l’eccezione viene aggiunta a quella già in corso di propagazione. Per questo, in presenza di try‑with‑resources non serve più alcun blocco finally per chiudere ResultSet, Statement e Connection.

Per l’inserimento o l’aggiornamento si usa executeUpdate(), che restituisce il numero di righe coinvolte:

String sql = "INSERT INTO mytable (name, email) VALUES (?, ?)";

try (Connection connection = DatabaseConnection.open();
     PreparedStatement statement = connection.prepareStatement(sql)) {

    statement.setString(1, "Mario Rossi");
    statement.setString(2, "mario.rossi@example.com");

    int righe = statement.executeUpdate();
    System.out.println("Righe inserite: " + righe);
} catch (SQLException e) {
    System.err.println("Errore durante l'inserimento: " + e.getMessage());
}

Inoltre, per migliorare le performance, valuta l’uso di batch processing quando devi inserire o aggiornare grandi volumi di dati: con addBatch() ed executeBatch() il driver raggruppa le istruzioni e invia una sola volta al server. Se le operazioni devono riuscire o fallire insieme, raggruppale in una transazione con setAutoCommit(false), commit() e rollback().

Passo 4: Quando serve un connection pool

DriverManager.getConnection() apre una nuova connessione fisica a ogni chiamata. Quello è un costo reale: handshake TCP, autenticazione, negoziazione del protocollo. Su uno script singolo non lo noti; in un server che risponde a centinaia di richieste al secondo diventa il collo di bottiglia, e il numero di connessioni contemporanee può far superare il limite max_connections del server MySQL.

È qui che entra in gioco un connection pool: un insieme di connessioni già aperte che vengono prestate e restituite. Il Singleton con una Connection statica era un tentativo di risolvere proprio questo problema, ma lo risolve nel modo peggiore: una sola connessione per tutti i thread non è un pool, è il contrario di un pool. Un pool ne gestisce N, e ogni thread ne prende una in prestito per la durata del proprio lavoro.

HikariCP è la libreria più diffusa in Java:

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>6.3.0</version>
</dependency>

E si usa in questo modo:

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class PooledQuery {

    public static void main(String[] args) throws SQLException {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(System.getenv("DB_URL"));
        config.setUsername(System.getenv("DB_USER"));
        config.setPassword(System.getenv("DB_PASSWORD"));
        config.setMaximumPoolSize(10);
        config.setMinimumIdle(2);
        config.setConnectionTimeout(3000);

        try (HikariDataSource dataSource = new HikariDataSource(config)) {
            try (Connection connection = dataSource.getConnection();
                 PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM mytable");
                 ResultSet resultSet = statement.executeQuery()) {

                resultSet.next();
                System.out.println("Righe: " + resultSet.getInt(1));
            }
        }
    }
}

Il codice di accesso al database non cambia: si usa ancora il try‑with‑resources e si chiude ancora la Connection. La differenza è che getConnection() non apre una connessione nuova, ne prende in prestito una già aperta, e la close() la restituisce al pool invece di chiuderla. Questa è la proprietà fondamentale: con un pool, chiudere la connessione è sempre l’azione corretta.

Il DataSource va creato una volta sola, all’avvio dell’applicazione, e tenuto come campo statico: creare un HikariDataSource per ogni richiesta equivale a non avere un pool, con il sovraccosto del pool in più.

Conclusione

Hai ora a disposizione una guida completa per installare il driver JDBC, configurare la connessione al database MySQL e eseguire le query più comuni utilizzando Java. Queste basi ti permetteranno di costruire applicazioni robuste, in grado di gestire dati in modo efficiente e sicuro.

I due punti da portare a casa sono semplici: la connessione ha un ciclo di vita e va chiusa con il try‑with‑resources, mai tenuta in una variabile statica; e quando il numero di connessioni simultanee cresce, il problema non si risolve limitando le connessioni a una, ma introducendo un pool come HikariCP.

Per andare oltre, considera l’adozione di framework come Hibernate o JPA, che astraggono ulteriormente la comunicazione con il database e offrono funzionalità avanzate di mappatura degli oggetti. Inoltre, approfondire le tecniche di gestione delle transazioni ti consentirà di scalare le tue applicazioni in ambienti di produzione ad alta domanda.

Buono sviluppo e buona programmazione con Java e MySQL!