Conectarse a la base de datos desde el código
En resumen: Un programa habla con una base de datos mediante un controlador: abre una conexión a la dirección de la base, envía la consulta, lee las filas una a una y cierra la conexión.
Problema
La página «Menos de 5 €»
El sitio web de la tienda tiene una página con los productos baratos. Ya conoces la consulta para ella:
Pero nadie en el sitio escribe consultas en un cuadro. Cuando alguien abre la página, el servidor de la tienda ejecuta un programa. El programa envía esta consulta a la base de datos, recibe las 17 filas como valores de su propio lenguaje (una cadena, un número) y las pone en el HTML. ¿Cómo habla exactamente un programa con una base de datos?
La idea
Los cinco pasos
Es igual en todos los lenguajes:
- Controlador. Una biblioteca que habla el idioma de una base de datos concreta (SQLite, PostgreSQL, MySQL).
- Conexión. El programa dice dónde está la base de datos (su dirección) y se conecta.
- Consulta. El texto SQL va a la base de datos.
- Filas. El resultado vuelve fila por fila, y el programa lee sus columnas como valores.
- Cierre. La conexión se libera, incluso si ocurrió un error a medias.
Aquí está la misma consulta, con una pequeña copia de la tienda en memoria (:memory:, seis productos que setup crea al empezar):
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) { setup(conn); String sql = "SELECT name, price FROM products WHERE price < 5 ORDER BY price"; try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery(sql)) { while (rs.next()) { System.out.printf("%s: %.2f €%n", rs.getString("name"), rs.getDouble("price")); } }}Bollo de mantequilla: 0.80 €Pan blanco: 1.20 €Leche fresca: 1.60 €Mantequilla: 3.20 €using var conn = new SqliteConnection("Data Source=:memory:");conn.Open();Setup(conn);var cmd = conn.CreateCommand();cmd.CommandText = "SELECT name, price FROM products WHERE price < 5 ORDER BY price";using var reader = cmd.ExecuteReader();while (reader.Read()){ Console.WriteLine($"{reader.GetString(0)}: {reader.GetDouble(1):F2} €");}Bollo de mantequilla: 0.80 €Pan blanco: 1.20 €Leche fresca: 1.60 €Mantequilla: 3.20 €conn = sqlite3.connect(":memory:")setup(conn)sql = "SELECT name, price FROM products WHERE price < 5 ORDER BY price"for name, price in conn.execute(sql): print(f"{name}: {price:.2f} €")conn.close()Bollo de mantequilla: 0.80 €Pan blanco: 1.20 €Leche fresca: 1.60 €Mantequilla: 3.20 €const db = new DatabaseSync(":memory:");setup(db);const sql = "SELECT name, price FROM products WHERE price < 5 ORDER BY price";for (const row of db.prepare(sql).all()) { console.log(`${row.name}: ${row.price.toFixed(2)} €`);}db.close();Bollo de mantequilla: 0.80 €Pan blanco: 1.20 €Leche fresca: 1.60 €Mantequilla: 3.20 €Del quinto paso se ocupa el lenguaje: try (…) en Java, using en C#, close() en Python y JavaScript. En Python with termina la transacción, no la conexión, por eso aquí close() está escrito de forma explícita.
Más precisamente, para curiosos
setup ejecuta el texto SQL que crea la tabla con los seis productos:
static void setup(Connection conn) throws SQLException { try (Statement st = conn.createStatement()) { st.executeUpdate(""" CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL NOT NULL, discount INTEGER); INSERT INTO products VALUES (1, 'Pan blanco', 1.20, NULL), (3, 'Bollo de mantequilla', 0.80, 10), (5, 'Leche fresca', 1.60, NULL), (7, 'Queso blanco', 7.90, 15), (9, 'Mantequilla', 3.20, NULL), (17, 'Café en grano', 14.90, 0); """); }}static void Setup(SqliteConnection conn){ var cmd = conn.CreateCommand(); cmd.CommandText = """ CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL NOT NULL, discount INTEGER); INSERT INTO products VALUES (1, 'Pan blanco', 1.20, NULL), (3, 'Bollo de mantequilla', 0.80, 10), (5, 'Leche fresca', 1.60, NULL), (7, 'Queso blanco', 7.90, 15), (9, 'Mantequilla', 3.20, NULL), (17, 'Café en grano', 14.90, 0); """; cmd.ExecuteNonQuery();}def setup(conn): conn.executescript(""" CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL NOT NULL, discount INTEGER); INSERT INTO products VALUES (1, 'Pan blanco', 1.20, NULL), (3, 'Bollo de mantequilla', 0.80, 10), (5, 'Leche fresca', 1.60, NULL), (7, 'Queso blanco', 7.90, 15), (9, 'Mantequilla', 3.20, NULL), (17, 'Café en grano', 14.90, 0); """)function setup(db) { db.exec(` CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL NOT NULL, discount INTEGER); INSERT INTO products VALUES (1, 'Pan blanco', 1.20, NULL), (3, 'Bollo de mantequilla', 0.80, 10), (5, 'Leche fresca', 1.60, NULL), (7, 'Queso blanco', 7.90, 15), (9, 'Mantequilla', 3.20, NULL), (17, 'Café en grano', 14.90, 0); `);}Los controladores de los ejemplos: sqlite-jdbc en Java (el paquete org.xerial:sqlite-jdbc), Microsoft.Data.Sqlite en C# (NuGet), y en Python y JavaScript SQLite viene incluido (sqlite3, y node:sqlite desde Node 22). Cada ejemplo se ha ejecutado y su salida es exactamente esta.
Míralo
La dirección de la base y la contraseña
SQLite es un archivo y la dirección es la ruta hasta él. Si creaste shop.db en la lección «SQL y las bases de datos reales» (sqlite3 shop.db < shop-es.sql), cambia :memory: por la ruta y quita setup: en Java es jdbc:sqlite:shop.db, en C# Data Source=shop.db, y en Python y JavaScript simplemente "shop.db". El mismo programa mostrará los 17 productos.
Una base de datos de servidor (PostgreSQL, MySQL, SQL Server) vive en otro programa, a menudo en otra computadora. Su dirección tiene un host, un puerto, un nombre de base de datos, un usuario y una contraseña:
String url = "jdbc:postgresql://localhost:5432/shop";String password = System.getenv("SHOP_DB_PASSWORD");try (Connection conn = DriverManager.getConnection(url, "shop_app", password)) { // las mismas consultas}var password = Environment.GetEnvironmentVariable("SHOP_DB_PASSWORD");var url = $"Host=localhost;Port=5432;Database=shop;Username=shop_app;Password={password}";using var conn = new NpgsqlConnection(url);conn.Open();import osimport psycopg
conn = psycopg.connect(host="localhost", port=5432, dbname="shop", user="shop_app", password=os.environ["SHOP_DB_PASSWORD"])import pg from "pg";
const db = new pg.Client({ host: "localhost", port: 5432, database: "shop", user: "shop_app", password: process.env.SHOP_DB_PASSWORD });await db.connect();La contraseña no va en el código. El código va a git, donde lo ve todo el que tiene acceso, y la contraseña del sitio real es distinta de la de tu computadora. Por eso viene de fuera: de una variable de entorno, como aquí, o del almacén de secretos del proveedor de hosting.
Estos cuatro ejemplos no se ejecutan en las comprobaciones del sitio, porque necesitan un servidor PostgreSQL. Los controladores son org.postgresql, Npgsql, psycopg y pg.
Pruébalo
La consulta para otra página
La página «Agotado» del panel de administración muestra los nombres de los productos con existencias 0, en orden alfabético. Escribe la consulta que enviará el programa.
Solución
SELECT name FROM products WHERE stock = 0 ORDER BY name;El programa no cambia: solo cambia el texto de sql. Por eso conviene probar primero la consulta en un cuadro como este y solo después ponerla en el código.
La idea
De la columna al valor
Cada columna llega como un valor de un tipo del lenguaje. INTEGER se convierte en int (o long), REAL en double, TEXT en String / string / str. Lo interesante es NULL. En Python es None, en JavaScript null, y en Java y C# depende de cómo lo leas:
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) { setup(conn); try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery("SELECT name, discount FROM products WHERE id IN (1, 3, 17)")) { while (rs.next()) { int discount = rs.getInt("discount"); String text = rs.wasNull() ? "sin descuento" : discount + "%"; System.out.println(rs.getString("name") + ": getInt devuelve " + discount + ", así que " + text); } }}Pan blanco: getInt devuelve 0, así que sin descuentoBollo de mantequilla: getInt devuelve 10, así que 10%Café en grano: getInt devuelve 0, así que 0%using var conn = new SqliteConnection("Data Source=:memory:");conn.Open();Setup(conn);var cmd = conn.CreateCommand();cmd.CommandText = "SELECT name, discount FROM products WHERE id IN (1, 3, 17)";using var reader = cmd.ExecuteReader();while (reader.Read()){ var text = reader.IsDBNull(1) ? "sin descuento" : $"{reader.GetInt32(1)}%"; Console.WriteLine($"{reader.GetString(0)}: {text}");}Pan blanco: sin descuentoBollo de mantequilla: 10%Café en grano: 0%conn = sqlite3.connect(":memory:")setup(conn)for name, discount in conn.execute("SELECT name, discount FROM products WHERE id IN (1, 3, 17)"): text = "sin descuento" if discount is None else f"{discount}%" print(f"{name}: {discount!r}, así que {text}")conn.close()Pan blanco: None, así que sin descuentoBollo de mantequilla: 10, así que 10%Café en grano: 0, así que 0%const db = new DatabaseSync(":memory:");setup(db);for (const row of db.prepare("SELECT name, discount FROM products WHERE id IN (1, 3, 17)").all()) { const text = row.discount === null ? "sin descuento" : `${row.discount}%`; console.log(`${row.name}: ${row.discount}, así que ${text}`);}db.close();Pan blanco: null, así que sin descuentoBollo de mantequilla: 10, así que 10%Café en grano: 0, así que 0%El pan blanco no tiene descuento (NULL), mientras que el café tiene un descuento del 0 %. Son cosas distintas. En Java getInt devuelve 0 en ambos casos y solo wasNull() los distingue. En C# GetInt32 sobre un NULL se detiene con un error, así que preguntas primero con IsDBNull.
Ejercicio
Los productos nuevos
La página de inicio muestra los productos nuevos: el nombre y el precio de los productos añadidos (added_at) el 1 de junio de 2025 o después, los más nuevos primero y, si la fecha coincide, por nombre.
Solución
SELECT name, price FROM products WHERE added_at >= '2025-06-01' ORDER BY added_at DESC, name;Con dos claves en ORDER BY: added_at DESC, name. Sin la segunda clave, los dos productos del 1 de agosto pueden salir cada vez en otro orden, y en el sitio se nota.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: SELECT: elegir columnas · WHERE: filtrar filas · NULL: sin valor · ORDER BY, LIMIT y DISTINCT
- Continúa en: Inyección SQL y parámetros · ORM y el problema N+1 · Migraciones del esquema · Pools de conexiones, copias de seguridad y permisos
- Ejercicios sobre el tema: La base de datos en el programa