8. Bases de Datos#
En esta sección nos centraremos en el almacenamiento de datos utilizando sistemas de bases de datos relacionales [2] y no relacionales [13]. Estos sistemas son fundamentales para el desarrollo de aplicaciones modernas, ya que la mayoría de ellas requiere una gestión de datos escalable, eficiente y segura.
Elegir entre utilizar una base de datos relacional o no relacional depende del tipo de aplicación. Los sistemas relacionales como PostgreSQL [12], MySQL [7] o Oracle [6], ofrecen un esquema estructurado y formal que ofrece integridad y consistencia de los datos, además de un lenguaje estándar como SQL para expresar consultas complejas de manera eficiente. Por otro lado, los sistemas no relacionales como MongoDB [1] o Redis [4], aportan flexibilidad en el manejo de estructuras de datos heterogéneas y una escalabilidad superior, lo que los hace especialmente adecuados para aplicaciones distribuidas con una gran demanda.
Característica |
Relacional (SQL) |
No Relacional (NoSQL) |
|---|---|---|
Ejemplos |
PostgreSQL, MySQL, Oracle |
MongoDB, Redis |
Modelo de datos |
Tablas con esquemas rígidos |
Documentos, clave-valor, grafos, etc. |
Lenguaje de consulta |
SQL |
Específico de cada sistema |
Consistencia |
Alta (ACID) |
Eventual o configurable (BASE) |
Escalabilidad |
Vertical |
Horizontal |
Adecuado para |
Transacciones, reportes, ERP |
Big Data, IoT, aplicaciones web modernas |
Flexibilidad del esquema |
Baja |
Alta |
Python cuenta con un ecosistema maduro de bibliotecas que permiten la comunicación con diversos sistemas gestores de bases de datos. En las siguientes secciones, se presentarán ejemplos prácticos de uso para diferentes tipos de gestores, tanto relacionales como no relacionales.
8.1. Bases de Datos relacionales#
Al igual que otros lenguajes, Python cuenta con una especificación estándar que deben seguir los autores de APIs para que las aplicaciones puedan interactuar con bases de datos sin depender de los detalles específicos del sistema de base de datos. Si ya has desarrollado aplicaciones de datos en otros lenguajes probablemente utilizaste librerías que siguen un estándar como ODBC (Open Database Connectivity), JDBC (Java Database Connectivity) o ADO.NET. Python por su parte cuenta con el Python Database API Specification v2.0 (PEP 249), donde se especifica una interfaz estándar para conectar aplicaciones Python con sistemas de bases de datos relacionales. Antes de entrar a los detalles veamos los componentes principales del estándar:
1. Constructores
Antes de empezar a interactuar con un servidor de base de datos, debemos establecer una conexión, típicamente utilizamos una cadena de conexión con los parámetros necesarios para establecer la conexión (host, puerto, usuario,etc.). Una vez establecida la conexión nos regresa un objeto que representa una conexión específica.
2. Conexión
Una conexión es un objeto de la clase Connection la cual
contiene métodos para interactuar a nivel alto con el servidor:
cursor() Crea un cursor con el cual podemos hacer consultas o enviar comandos de SQL al servidor.
commit() Compromete los cambios hechos por la transacción actual.
rollback() Deshace los cambios hechos por la transacción actual.
close() Cierra la conexión.
3. Cursor
Un cursor incluye los siguientes métodos:
execute(sql, params) ejecuta una consulta, envía los parámetros de la consulta por separado.
executemany(sql, seq_of_params) ejecuta varias consultas.
fetchone() devuelve un registro solamente.
fetchall() devuelve todos los registros.
fetchmany(size) devuelve un número específico de registros.
Atributos:
description información de los campos del resultado.
rowcount número de registros afectados por la última operación
4. Tipos de datos
La especificación del API también incluye tipos de datos estándar en una base de datos:
Date,Time,TimestampBinarySTRING,NUMBER,DATETIME,ROWID(como constantes tipo)
Cada módulo debe implementar funciones para convertir entre los tipos de Python y los de SQL.
5. Excepciones
Se define una jerarquía de excepciones estándar:
Exception
|__Warning
|__Error
|__InterfaceError
|__DatabaseError
|__DataError
|__OperationalError
|__IntegrityError
|__InternalError
|__ProgrammingError
|__NotSupportedError
Todos los módulos deben lanzar estas excepciones específicas para facilitar la portabilidad del código.
Base de Datos de Películas#
Como ejemplo de base de datos, utilizaremos un esquema relacional para almacenar información sobre películas. Podríamos basarnos en este ejemplo para programar alguna aplicación del tema, pero por ahora, el objetivo principal es ejemplificar el uso del lenguaje.
El modelo indica que una persona (Persona) puede tener varios roles
(Rol) en una película (Película) . Por ejemplo, el director de una
película, también puede ser el productor o e incluso uno de los actores. Para
modelar esta relación tripartita (persona–rol–película) se crea la entidad
Credito. Además, una película puede pertenecer a varios géneros. La
información de las películas se puede extraer de la plataforma «The Movie Data
Base (TMDB)»
Veamos ejemplos para SQLite y PostgreSQL:
8.2. SQLite#
SQLite es un sistema relacional de bases de datos extremadamente ligero, implementado como una librería en C que puede ser embebida en un proceso, como por ejemplo, un programa escrito en Python.
No requiere configuración, ni opera como un servidor independiente. Sin embargo, a pesar de su simplicidad, ofrece un alto rendimiento y soporte completo de transacciones ACID [16]. La base de datos se puede almacenar en un solo archivo y su licencia de dominio público la hace ideal para ambientes académicos, pero también profesionales. Además, SQLite se utiliza en móviles y navegadores web, y suele describirse como uno de los motores de bases de datos más desplegados del mundo.
La librería estándar de Python incluye el módulo sqlite3 que implementa
el DB API 2.0 visto anteriormente. Como no tiene un proceso independiente
utilizado como servidor y la base de datos es solamente un archivo, no requerimos
instalar nada y solamente nos «conectamos» pasando como argumento el nombre del
archivo con el que vamos a trabajar:
>>> import sqlite3 as sql
>>> con = sql.connect("movies.sqlite")
Antes de crear nuestra primera tabla, es importante conocer los tipos de datos de SQLite. SQLite es muy flexible en cuanto a los tipos de dato que utiliza e incluso es opcional indicar el tipo de dato. Es parecida a Python en el sentido de que el tipo de dato no se estipula a nivel de la columna, es más bien flexible y se almacena junto con cada valor. Sin embargo, a partir de la versión 3.37 es posible indicar tipos de datos estríctos. Los tipos de datos de almacenamiento de SQLite son los siguientes:
NULL. El valor nulo.
INTEGER. Entero con signo; el tamaño en bytes varía según el valor.
REAL. Número flotante IEEE de 8 bytes.
TEXT. Cadena de texto almacenada como UTF-8, UTF-16BE o UTF-16LE.
BLOB. Objeto binario almacenado tal cual se ingresó.
Las fechas y hora se almacenan como: - TEXT como cadenas en ISO8601 («YYYY-MM-DD HH:MM:SS.SSS»). - REAL usando el número Juliano, el número de días desde el mediodía del 24 de Noviembre del 4714 B.C. - INTEGER como tiempo de Unix, (segundos desde 1970-01-01 00:00:00 UTC).
En SQLite, las fechas no tienen un tipo nativo; su interpretación depende de la
aplicación. Al crear una tabla podemos indicar los tipos de datos en SQL
estándar o utilizando algunas restricciones, por ejemplo: VARCHAR(255),
SQLite ignorará la restricción de longitud (255) y lo tratará como el tipo
de dato TEXT.
movies.sql# 1CREATE TABLE IF NOT EXISTS PERSONA
2 (
3 ID INTEGER PRIMARY KEY,
4 NOMBRE TEXT NOT NULL
5 );
6
7CREATE TABLE IF NOT EXISTS ROL
8 (
9 ID INTEGER PRIMARY KEY,
10 ROL TEXT NOT NULL
11 );
12
13CREATE TABLE IF NOT EXISTS GENERO
14 (
15 ID INTEGER PRIMARY KEY,
16 GENERO TEXT NOT NULL
17 );
18
19CREATE TABLE IF NOT EXISTS PELICULA
20 (
21 ID INTEGER PRIMARY KEY,
22 TITULO TEXT NOT NULL,
23 ESTRENO REAL,
24 DURACION INTEGER,
25 PRESUPUESTO INTEGER,
26 RECAUDACION INTEGER,
27 POSTER TEXT,
28 TMBD_ID TEXT
29 );
30
31CREATE TABLE IF NOT EXISTS PELICULA_GENERO
32(
33 PELICULA INTEGER,
34 GENERO INTEGER,
35 PRIMARY KEY (PELICULA, GENERO),
36 FOREIGN KEY (PELICULA)
37 REFERENCES PELICULA(ID)
38 ON DELETE CASCADE
39 ON UPDATE NO ACTION,
40 FOREIGN KEY (GENERO)
41 REFERENCES GENERO (ID)
42 ON DELETE CASCADE
43 ON UPDATE NO ACTION
44);
45
46CREATE TABLE IF NOT EXISTS CREDITOS
47(
48 PERSONA INTEGER,
49 PELICULA INTEGER,
50 ROL INTEGER,
51 DETALLE TEXT,
52 PRIMARY KEY (PERSONA, PELICULA, ROL),
53 FOREIGN KEY (PERSONA)
54 REFERENCES PERSONA(ID)
55 ON DELETE CASCADE
56 ON UPDATE NO ACTION,
57 FOREIGN KEY (PELICULA)
58 REFERENCES PELICULA(ID)
59 ON DELETE CASCADE
60 ON UPDATE NO ACTION,
61 FOREIGN KEY (ROL)
62 REFERENCES ROL (ID)
63 ON DELETE CASCADE
64 ON UPDATE NO ACTION
65);
66
67INSERT INTO ROL (ID, ROL) VALUES (1, "Director");
68INSERT INTO ROL (ID, ROL) VALUES (2, "Directora");
69INSERT INTO ROL (ID, ROL) VALUES (3, "Actor");
70INSERT INTO ROL (ID, ROL) VALUES (4, "Actriz");
71INSERT INTO ROL (ID, ROL) VALUES (5, "Guionista");
72
73
74INSERT INTO GENERO (ID, GENERO) VALUES (35, "Comedia");
75INSERT INTO GENERO (ID, GENERO) VALUES (53, "Thriller");
76INSERT INTO GENERO (ID, GENERO) VALUES (18, "Drama");
77INSERT INTO GENERO (ID, GENERO) VALUES (37, "Western");
78
79INSERT INTO PERSONA (ID, NOMBRE) VALUES (20738, "Song Kang-ho");
80INSERT INTO PERSONA (ID, NOMBRE) VALUES (115290, "Lee Sun-kyun");
81INSERT INTO PERSONA (ID, NOMBRE) VALUES (556435, "Cho Yeo-jeong");
82INSERT INTO PERSONA (ID, NOMBRE) VALUES (1255881, "Choi Woo-shik");
83INSERT INTO PERSONA (ID, NOMBRE) VALUES (1442583, "Park So-dam");
84INSERT INTO PERSONA (ID, NOMBRE) VALUES (21684, "Bong Joon Ho");
85
86INSERT INTO PERSONA (ID, NOMBRE) VALUES (190, "Clint Eastwood");
87INSERT INTO PERSONA (ID, NOMBRE) VALUES (3265, "Eli Wallach");
88INSERT INTO PERSONA (ID, NOMBRE) VALUES (4078, "Lee Van Cleef");
89INSERT INTO PERSONA (ID, NOMBRE) VALUES (4385, "Sergio Leone");
90
91
92
93INSERT INTO Pelicula (ID, TITULO, ESTRENO, DURACION, PRESUPUESTO, RECAUDACION, POSTER)
94VALUES (496243, 'Parasite', '2019-12-25', 133, 11363000, 257591776, '7IiTTgloJzvGI1TAYymCfbfl3vT.jpg');
95INSERT INTO Pelicula (ID, TITULO, ESTRENO, DURACION, PRESUPUESTO, RECAUDACION, POSTER)
96VALUES (429, 'The Good, the Bad and the Ugly', '1969-04-17', 133, 1200000, 38900000, 'bX2xnavhMYjWDoZp1VM6VnU1xwe.jpg');
97
98INSERT INTO PELICULA_GENERO (PELICULA, GENERO) VALUES (496243, 35);
99INSERT INTO PELICULA_GENERO (PELICULA, GENERO) VALUES (496243, 53);
100INSERT INTO PELICULA_GENERO (PELICULA, GENERO) VALUES (496243, 18);
101INSERT INTO PELICULA_GENERO (PELICULA, GENERO) VALUES (429, 37);
102
103INSERT INTO CREDITOS (PERSONA, PELICULA, ROL, DETALLE) VALUES (20738, 496243, 3, 'Kim Ki-taek');
104INSERT INTO CREDITOS (PERSONA, PELICULA, ROL, DETALLE) VALUES (115290, 496243, 3, 'Park Dong-ik');
105INSERT INTO CREDITOS (PERSONA, PELICULA, ROL, DETALLE) VALUES (556435, 496243, 4, 'Yeon-kyo');
106INSERT INTO CREDITOS (PERSONA, PELICULA, ROL, DETALLE) VALUES (1255881, 496243, 3, 'Ki-jung');
107INSERT INTO CREDITOS (PERSONA, PELICULA, ROL, DETALLE) VALUES (21684, 496243, 1, NULL);
108
109INSERT INTO CREDITOS (PERSONA, PELICULA, ROL, DETALLE) VALUES (190, 429, 3, 'Blondie');
110INSERT INTO CREDITOS (PERSONA, PELICULA, ROL, DETALLE) VALUES (3265, 429, 3, 'Tuco Ramirez');
111INSERT INTO CREDITOS (PERSONA, PELICULA, ROL, DETALLE) VALUES (4078, 429, 3, 'Sentenza - Angel Eyes');
112INSERT INTO CREDITOS (PERSONA, PELICULA, ROL, DETALLE) VALUES (4385, 429, 1, NULL);
Vamos a cargar el script utilizando python:
>>> import sqlite3 as sql
>>> con = sql.connect("movies.sqlite")
>>> with open('.\\docs\\source\\movies.sql', 'r') as f:
... sql_script = f.read()
...
>>> cursor = con.cursor()
>>> cursor.executescript(sql_script)
<sqlite3.Cursor object at 0x00000175F2123CC0>
>>> con.commit()
>>> con.close()
Para cargar el archivo utilizamos el método open() para abrir el
script. Ajusta la ruta si estás en otro sistema operativo o estructura de
carpetas. En el ejemplo ejecutamos el script con el método
cursor.executescript(script).
El script agrega la información de dos películas:
Si utilizas un editor de texto como Visual Studio Code, puedes instalar un ``plug-in` para visualizar la base de datos que hemos creado. Por ejemplo, el SQLite Viewer de Forian Klampfer.
Una vez creada la base de datos, podemos conectarnos y hacer consultas
utilizando el cursor.execute(). El cursor ahora sí contiene
elementos ya que el comando es una consulta SELECT y puede regresar ciertos
datos. Podemos iterar el cursor recuperando un registro a la vez con el método
fetchone().
>>> res = cursor.execute("SELECT * FROM Persona");
>>> res.fetchone()
(190, 'Clint Eastwood')
>>> res.fetchone()
(3265, 'Eli Wallach')
También podemos leer el resultado de la consulta en su totalidad, consumiendo todo el iterador:
>>> import sqlite3 as sql
>>> con = sql.connect("movies.sqlite")
>>> cursor = con.cursor()
>>> res = cursor.execute("SELECT * FROM Persona");
>>> res.fetchall() # Resultados recortados por espacio:
[(190, 'Clint Eastwood'), (3265, 'Eli Wallach'), (4078, 'Lee Van Cleef'), (1442583, 'Park So-dam')]
>>> res.fetchall() # El iterador ya se consumió en su totalidad
[]
Otra operación básica es la inserción de nuevos registros, que se realiza
utilizando la sentencia SQL INSERT INTO. Un aspecto importante a considerar es
la forma en que construimos la consulta a partir de marcadores de posición en
lugar de concatenar cadenas directamente. Esta técnica no solo facilita el
manejo de valores dinámicos, sino que también previene ataques de inyección SQL
(SQL Injection), ya que sqlite3 se encarga de validar y
escapar los valores proporcionados antes de ejecutar el comando.
Veamos un ejemplo de inserción utilizando una consulta parametrizada:
>>> cursor.execute("INSERT INTO Persona(ID, NOMBRE) VALUES (?, ?)", (999, "Nuevo Actor"))
<sqlite3.Cursor object at 0x00000248C990C840>
>>> con.commit()
Notamos que se usan signos de interrogación (?) como marcadores de posición, y los valores se pasan como una tupla.
Si tenemos varios registros se puede utilizar executemany():
>>> personas = [(1001, "Leonardo DiCaprio"), (1002, "Brad Pitt")]
>>> cursor.executemany("INSERT INTO Persona (ID, NOMBRE) VALUES (?, ?)", personas)
<sqlite3.Cursor object at 0x00000248C990C840>
>>> con.commit()
Podemos encapsular los comandos en funciones para mejorar la estructura de nuestro programa:
>>> def conectar_db(nombre_archivo):
... return sql.connect(nombre_archivo)
...
>>> def insertar_persona(con, persona_id, nombre):
... cur = con.cursor()
... cur.execute("INSERT INTO Persona (ID, NOMBRE) VALUES (?, ?)", (persona_id, nombre))
... con.commit()
...
>>> con = conectar_db("movies.sqlite")
>>> insertar_persona(con, 138, "Quentin Tarantino")
>>> con.close()
En este caso la conexión se establece solo una vez al principio del programa y se reutiliza enviando la referencia a los distintos métodos que operan sobre nuestros datos.
Al enviar comandos a un sistema de bases de datos, estos se ejecutan en el contexto de una transacción, la cual debe completarse en su totalidad o revertirse completamente en caso de error. Una transacción es, por definición, atómica: todas las operaciones que contiene deben realizarse con éxito; de lo contrario, deben deshacerse como si nunca hubieran ocurrido.
En caso de que se produzca un error durante la ejecución de alguna operación,
podemos revertir los cambios explícitamente utilizando el comando ROLLBACK. Si
todas las operaciones se ejecutan correctamente, debemos confirmar los cambios
con el comando COMMIT.
En Python, podemos utilizar el objeto Connection como un gestor de contexto
(with) para manejar transacciones de forma automática. En este modo, si el
bloque with finaliza sin excepciones, se envía automáticamente un COMMIT. En
caso de que ocurra una excepción o el commit falle, la transacción se revierte
automáticamente mediante un ROLLBACK.
Connection# 1con = sqlite3.connect(":memory:") # Creamos la base de datos en memoria
2
3# Creamos una nueva tabla Persona, con una restricción UNIQUE
4con.execute("CREATE TABLE Persona(id INTEGER PRIMARY KEY, nombre VARCHAR UNIQUE)")
5
6# Si hay exito automáticamente se llama con.commit()
7with con:
8 con.execute("INSERT INTO Persona(nombre) VALUES(?)", ("Leonardo DiCaprio",))
9
10# Se ejecuta con.rollback() si el bloque termina debido a una excepción,
11# debemos atrapar la excepción
12try:
13 with con:
14 con.execute("INSERT INTO Persona(nombre) VALUES(?)", ("Leonardo DiCaprio",))
15except sqlite3.IntegrityError:
16 print("No se puede insertar la persona dos veces.")
17
18# El objecto ``Connection`` utilizado solo compromete o deshace transacciones, no
19# cierra la conexión
20con.close()
8.3. Resumen de capítulo#
En este capítulo revisamos el papel de los sistemas de bases de datos en el desarrollo de aplicaciones modernas, comparando enfoques relacionales (SQL) y no relacionales (NoSQL), así como los criterios prácticos para elegir entre ambos.
Posteriormente introdujimos el estándar Python DB-API 2.0 (PEP 249), explicando sus componentes principales: conexiones, cursores, transacciones, tipos de datos y jerarquía de excepciones. Con esta base, trabajamos un ejemplo con SQLite, un motor relacional ligero integrado en la biblioteca estándar, con el que construimos y consultamos una base de datos de películas.
Finalmente, mostramos buenas prácticas esenciales al interactuar con bases de
datos desde Python: uso de consultas parametrizadas para prevenir inyección SQL,
y manejo correcto de transacciones mediante commit(), rollback() y el uso
de Connection como gestor de contexto con with.