Conexión a base de datos con JDBC en Java

JDBC es la API estándar para hablar con una base de datos desde Java. El código es casi el mismo para MariaDB, PostgreSQL u Oracle: lo único que cambia es el jar del controlador y la URL.

El controlador

El ejemplo de esta página usa SQLite, que guarda la base en un archivo y no necesita servidor: así puedes copiarlo y ejecutarlo sin instalar nada.

<dependency>
  <groupId>org.xerial</groupId>
  <artifactId>sqlite-jdbc</artifactId>
  <version>3.53.4.0</version>
</dependency>
Base de datosURLArtefacto de Maven
SQLitejdbc:sqlite:biblioteca.dborg.xerial:sqlite-jdbc
MariaDBjdbc:mariadb://host:3306/baseorg.mariadb.jdbc:mariadb-java-client
MySQLjdbc:mysql://host:3306/basecom.mysql:mysql-connector-j
PostgreSQLjdbc:postgresql://host:5432/baseorg.postgresql:postgresql
El Class.forName("com.mysql.jdbc.Driver") ya no hace falta. Desde JDBC 4 —Java 6— los controladores se registran solos al estar en el classpath. Si lo ves en un ejemplo, es de antes de 2006.

Conectar

import java.sql.*;

public class Jdbc {
    public static void main(String[] args) throws Exception {
        String url = "jdbc:sqlite:biblioteca.db";

        try (Connection con = DriverManager.getConnection(url)) {
            var meta = con.getMetaData();
            System.out.println("conectado a " + meta.getDatabaseProductName()
                             + " " + meta.getDatabaseProductVersion());
        }
    }
}
salidaconectado a SQLite 3.53.4

Con un servidor, la llamada lleva usuario y contraseña: DriverManager.getConnection(url, usuario, clave). El try (…) cierra la conexión pase lo que pase, que es importante: una conexión que se queda abierta acaba agotando el servidor.

Crear la tabla

try (Statement st = con.createStatement()) {
    st.execute("DROP TABLE IF EXISTS libros");
    st.execute("""
        CREATE TABLE libros (
            id     INTEGER PRIMARY KEY AUTOINCREMENT,
            titulo TEXT NOT NULL,
            autor  TEXT NOT NULL,
            anio   INTEGER
        )""");
}

Statement vale para las sentencias fijas que escribes tú. Para cualquier cosa que lleve datos de fuera, lo de abajo.

Insertar: PreparedStatement y lotes

String insertar = "INSERT INTO libros (titulo, autor, anio) VALUES (?, ?, ?)";

con.setAutoCommit(false);
try (PreparedStatement ps = con.prepareStatement(insertar)) {
    for (var l : libros) {
        ps.setString(1, l.titulo());
        ps.setString(2, l.autor());
        ps.setInt(3, l.anio());
        ps.addBatch();
    }
    int[] n = ps.executeBatch();
    con.commit();
    System.out.println("insertados: " + n.length);
}
con.setAutoCommit(true);
salidainsertados: 3

Tres cosas en un bloque. Los parámetros van con ? y se rellenan con setString, setInt y compañía, numerados desde 1, no desde 0. El lote manda todas las inserciones juntas en vez de una por viaje. Y desactivar el autocommit las mete en una sola transacción: o entran todas o no entra ninguna, y además es muchísimo más rápido.

Consultar

try (PreparedStatement ps = con.prepareStatement(
        "SELECT id, titulo, autor, anio FROM libros WHERE anio < ? ORDER BY anio")) {
    ps.setInt(1, 1970);
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            System.out.printf("  %d  %-14s %-10s %d%n",
                    rs.getInt("id"), rs.getString("titulo"),
                    rs.getString("autor"), rs.getInt("anio"));
        }
    }
}
salida1  El Quijote     Cervantes  1605
  3  Pedro Páramo   Rulfo      1955
  2  Rayuela        Cortázar   1963

Un ResultSet empieza antes de la primera fila: por eso el while (rs.next()), que avanza y devuelve false al acabar. Se puede leer por nombre de columna, como aquí, o por posición —también desde 1—. El nombre es más lento y mucho más legible; salvo en un bucle de millones de filas, compensa.

Con columnas que admiten nulos, getInt devuelve 0 cuando el valor es NULL. Para distinguirlos hay que preguntar rs.wasNull() justo después, o leer con getObject(…, Integer.class), que devuelve null.

Modificar

try (PreparedStatement ps = con.prepareStatement("UPDATE libros SET anio = ? WHERE titulo = ?")) {
    ps.setInt(1, 1967);
    ps.setString(2, "Rayuela");
    System.out.println("actualizadas: " + ps.executeUpdate());
}
salidaactualizadas: 1

executeQuery para los SELECT y executeUpdate para INSERT, UPDATE y DELETE; este último devuelve cuántas filas cambiaron, que casi siempre conviene comprobar.

Por qué PreparedStatement y no concatenar

String malicioso = "x\" OR \"1\"=\"1";

try (PreparedStatement ps = con.prepareStatement("SELECT COUNT(*) FROM libros WHERE titulo = ?")) {
    ps.setString(1, malicioso);
    System.out.println("con PreparedStatement: " + ps.executeQuery().getInt(1));
}

try (Statement st = con.createStatement()) {
    var rs = st.executeQuery("SELECT COUNT(*) FROM libros WHERE titulo = \"" + malicioso + "\"");
    System.out.println("concatenando:          " + rs.getInt(1));
}
salidacon PreparedStatement: 0
concatenando:          3
Ahí está, en dos líneas de salida. El mismo texto, buscado como dato, no encuentra nada: correcto, no hay ningún libro que se llame así. Concatenado dentro de la consulta, deja de ser un dato y pasa a ser SQL: la condición se convierte en OR "1"="1", que es siempre cierta, y devuelve los tres libros. Con un DELETE en vez de un SELECT, el resultado es el que te imaginas. Nunca se construye una consulta pegando cadenas, ni siquiera «porque este dato viene de dentro».

Una advertencia de Java 25

salidaWARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::load has been called by org.sqlite.SQLiteJDBCLoader in an unnamed module
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module

El controlador de SQLite carga una biblioteca nativa, y desde Java 24 eso avisa. No es un error y el programa funciona; para que no salga, se arranca con java --enable-native-access=ALL-UNNAMED …. Los controladores de MariaDB y PostgreSQL son Java puro y no lo producen.

Para…Usa
ConectarDriverManager.getConnection(url, usuario, clave)
Una sentencia fijaStatement
Cualquier cosa con datosPreparedStatement con ?
Muchas insercionesaddBatch() + executeBatch() sin autocommit
Leer el resultadowhile (rs.next())
Saber cuántas filas cambiaronlo que devuelve executeUpdate()
Una aplicación de verdadun pool como HikariCP, no DriverManager suelto
En una aplicación con varios usuarios no se abre una conexión por petición: se usa un pool (HikariCP es el habitual) que las reutiliza. Y por encima de JDBC están JPA/Hibernate o jOOQ, que escriben el SQL por ti. Pero todos acaban llamando a esto mismo, y por eso conviene entenderlo.

Más información en inglés: Database connection with JDBC in Java.

Todo el código se compiló y se ejecutó con Java 25 LTS en un contenedor limpio antes de publicar esta página; las salidas están copiadas de esa ejecución.

Deja un comentario