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.
También te puede interesar
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 datos | URL | Artefacto de Maven |
|---|---|---|
| SQLite | jdbc:sqlite:biblioteca.db | org.xerial:sqlite-jdbc |
| MariaDB | jdbc:mariadb://host:3306/base | org.mariadb.jdbc:mariadb-java-client |
| MySQL | jdbc:mysql://host:3306/base | com.mysql:mysql-connector-j |
| PostgreSQL | jdbc:postgresql://host:5432/base | org.postgresql:postgresql |
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.
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
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 |
|---|---|
| Conectar | DriverManager.getConnection(url, usuario, clave) |
| Una sentencia fija | Statement |
| Cualquier cosa con datos | PreparedStatement con ? |
| Muchas inserciones | addBatch() + executeBatch() sin autocommit |
| Leer el resultado | while (rs.next()) |
| Saber cuántas filas cambiaron | lo que devuelve executeUpdate() |
| Una aplicación de verdad | un pool como HikariCP, no DriverManager suelto |
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.