1.4-Connector-Python - by - Ricardo Mamaní
1.4-Connector-Python - by - Ricardo Mamaní
1.4-Connector-Python - by - Ricardo Mamaní
Programación 2
Módulo 1: Bases de Datos
Tema 4: Acceso a una base de datos desde Python
1.4 Acceso a una base de datos desde Python
1.4.1. Cómo funciona un conector
1.4.5.1. Seguridad
1.4.5.4. Otros
Bibliografía
1.4.1. Cómo funciona un conector
MySQL permite conexiones a un servidor desde aplicaciones clientes a través de
Conectores. Los conectores son software (librerías) que funcionan como un adaptador entre
la interfaz del SGBD y un lenguaje de programación.
Y también brinda conectores desarrollados por la comunidad, para lenguajes como PHP,
Ruby, Perl, Go, Rust, etc.
Figura 1.22
En el caso de Python, Connector/Python es una librería que contiene clases y funciones que
cumplimentan la especificación que tiene Python para una API de bases de datos, llamada
DB-API 2.0. Esto quiere decir que implementa clases preestablecidas con métodos
definidos que facilitan la interacción con la base de datos.
1.4.2. Instalación de Connector/Python
Existen dos formas de instalar el Conector.
Figura 1.23
Una vez instalado el conector podemos probar si la instalación fue exitosa importando el
módulo mysql.connector y conectándonos a un servidor MySQL, como veremos a
continuación.
1.4.3. Usando el Connector/Python
# prueba_mysql_connector.py
import mysql.connector
conn = mysql.connector.connect(user='juan',
password='contraseña',
host='127.0.0.1',
database='sakila')
conn.close()
En este simple script usamos un constructor del módulo connector, llamado connect(),
el cual toma los parámetros de la conexión, crea un nuevo objeto del tipo Connection, lo c
onecta con la base de datos y lo devuelve. Este objeto representa la conexión al servidor e i
mplementa diferentes métodos para ejecutar consultas sobre él.
En el script simplemente creamos una conexión con las credenciales de nuestro usuario e
indicamos qué host o servidor y qué base de datos vamos a usar.
• user y password son ambos str que deben ser las credenciales de un usuario
existente del servidor.
El constructor connect() puede recibir muchos más parámetros, para indicar puerto,
codificación de caracteres, etc., pero nosotros usaremos estos por ahora.
Luego, para concluir nuestra prueba, simplemente cerramos la conexión con el método
close(). Si este script se ejecuta sin errores, significa que la instalación fue correcta.
import mysql.connector
'password': 'password',
'host': '127.0.0.1',
'database': 'sakila'}
conn = mysql.connector.connect(**config)
conn.close()
Los pasos a seguir para trabajar con una base de datos desde Python, son:
• commit(). Al ejecutar este método los cambios que se hayan hecho en la base de
datos serán establecidos como permanentes. O sea, si ejecutamos sentencias que
modifiquen la base de datos y luego cerramos la conexión sin ejecutar el método
commit(), los cambios no tendrán efecto. Si la tabla estaba usando una clave
primaria numérica autogenerada, esta habrá generado los valores para los registros
que se insertaron, pero no se guardaron definitivamente, y esos valores ya no se
podrán usar.
• close(). Sirve para cerrar la conexión a la base de datos. Después de ejecutar este
método, el objeto Connection seguirá existiendo, pero no podrá comunicarse con la
base de datos si no se conecta nuevamente con el método connect().
Cursor
Este objeto es el que vamos a usar para ejecutar consultas a la base de datos. Se crea
mediante el método cursor() del objeto Connection. Sus métodos más importantes son:
Por ejemplo:
Vamos a escribir un script simple que acceda a la base de datos sakila y obtenga
información de ella. Usaremos la sección de la base de datos llamada Inventory, la cual
almacena los datos de las películas.
Figura 1.24
# actores_por_film.py
"""
"""
import mysql.connector
conn = mysql.connector.connect(user='usuario',
password='contraseña',
host='127.0.0.1',
database='sakila')
film_id = 100
FROM actor
ON actor.actor_id = film_actor.actor_id
cur = conn.cursor()
cur.execute(consulta, (film_id,))
resultado = cur.fetchall()
conn.close()
En este ejemplo podemos observar una práctica importante. No insertar los parámetros de
la consulta en la cadena, sino pasarlos por separado.
Podríamos pensar que es más sencillo directamente insertar el valor de film_id a la cadena
como un f-string: f"... WHERE film_actor.film_id = {film_id}". Y, si bien, en este
caso (en que directamente escribimos el valor 100) no habría problema, cuando escribamos
software que va a estar expuesto a usuarios que ingresen datos, nuestra base de datos va a
ser susceptible a ataques mediante inyección SQL.
Esto se evita si pasamos los parámetros por separado, ya que el Conector se encargará de
limpiar los parámetros de cualquier sentencia maliciosa, automáticamente. Por lo tanto, es
bueno acostumbrarnos a pasar siempre los parámetros por separado.
Veamos ahora otro ejemplo, más completo en el cual permitiremos al usuario seleccionar
de qué película buscar sus actores.
Primero creamos la GUI con tkinter, que mostrará un treeview con las películas
almacenadas en la base de datos. Para esto tenemos que hacer una consulta para obtener
todas las películas almacenadas.
Luego, cuando el usuario seleccione una película tenemos que buscar los actores que
participaron en ella, usando la misma consulta del ejemplo anterior. Esta vez el id de la
película vendrá de la fila seleccionada del treeview.
FROM actor
ON actor.actor_id = film_actor.actor_id
Por ejemplo, podríamos tener un método aparte que solo se conecte a la base de
datos y devuelva un objeto tipo Connection. Más aún, podríamos crear una clase
entera que se conecte a la base de datos en su método __init__ y que implemente
métodos que solo devuelvan los datos que le pedimos.
Muchos patrones de diseño de software aplican una separación por capas, en los que
cada capa se encarga de alguna sección independiente de la aplicación. Y casi
siempre existe una capa de acceso a datos en donde se centra toda la funcionalidad
que tenga que ver con la/las base/s de datos. Vamos a hacer eso en una próxima
iteración de la pequeña aplicación.
2. Por otro lado, la primera consulta a la base de datos devuelve 1000 registros de
películas, estos son todos los que existen en la tabla, ya que no restringimos la
consulta. Este no es un número tan grande y la aplicación lo puede soportar sin
problema. Pero hay que tener cuidado al hacer este tipo de consultas y usar el
método fetchall(), ya que si la tabla a la que estamos consultando tiene muchos
registros (millones) y solicitamos varias columnas, nuestra PC puede tener
problemas para cargar los datos ya que estos se almacenan en la memoria RAM
cuando los obtenemos, y esta es limitada.
Una solución para cuidar la memoria RAM es ir obteniendo resultados parciales con
fetchmany() y paginar los resultados.
Otra, es directamente no mostrar todos los registros de una sola vez y en su lugar
hacer un buscador para reducir la cantidad de registros que devuelve una consulta.
Implementando un buscador
En esta versión, hemos creado una clase que se va a encargar del acceso a datos. Tiene
métodos que directamente devuelve los result-sets como una lista de tuplas al programa
principal.
Además, hemos añadido un buscador que obliga al usuario a ingresar algún fragmento del
nombre de una película para recién obtener las coincidencias. De esta forma no tenemos
que obtener todos los registros.
FROM film
ORDER BY title;
Para poder hacer uso de LIKE a través del Conector, al formular la consulta, debemos dejar
solamente los caracteres %s donde irá el parámetro de búsqueda, sin incluir el comodín de
SQL %, ya que esto no funciona con el Conector. El carácter comodín de SQL % debe ser
agregado al parámetro antes de pasarlo al método execute(), como se puede ver en el
siguiente ejemplo.
a_buscar = "venger"
ORDER BY title"""
cursor.execute(consulta, (parametro,))
Notar que esta consulta de búsqueda no funcionará muy bien si se escriben palabras
separadas por espacios ya que el SGBD intentará encontrar la frase exacta, más allá de los
comodines al inicio y fin. Si queremos poder escribir varias palabras clave para una
búsqueda, como, por ejemplo: “avengers game” para intentar encontrar “Avengers: End
Game.”, vamos a tener que separar cada palabra y agregar una cláusula LIKE en la consulta
para cada una. Esto es más complicado de automatizar ya que pueden ser dos palabras, o
tres, o cualquier cantidad.
Cargando datos
Vamos a usar la base de datos que construimos en la teoría de SQL, la base de datos cine.
Para esta base de datos, vamos a crear una GUI que nos permita cargar datos de nuevas
películas.
Si bien podríamos dejar separada la carga de la película de la del director, también lo
podemos hacer en conjunto, lo cual es más intuitivo para el usuario. Lo único que debemos
tener en cuenta es que al momento de insertar el campo director en la tabla peliculas,
el director con ese id debe existir primero en la tabla directores. Ya que la restricción de
clave foránea ya está establecida en la base de datos.
Vamos a comenzar creando un formulario para cada dato de la película en nuestra GUI. Los
campos de título, duración y año deben ser ttkEntry, mientras que el campo de clave
foránea de directores lo vamos a llenar seleccionando el nombre de un director de los ya
cargados y pasando el id.
Vamos a usar un combobox para que el usuario seleccione al director de los que ya existen
en la base de datos, y obtendremos el id de allí. Pero dejaremos el combobox editable, así
puede cargar uno nuevo si este no existe.
Como se ve, en este ejemplo en particular, es imposible insertar una película sin director,
ya que la GUI no permite campos vacíos. Aunque, recordemos que, la base de datos sí
permite a la clave foránea como NULL.
Por otra parte, podemos ver que en ningún momento casteamos los valores a insertar en la
base de datos, simplemente tomamos los str que devuelve la GUI. Recordemos que el
título es una cadena y, la duración y el año son enteros en la base de datos. Esto es porque
el conector siempre trabaja con str, como vimos en la Figura 1.27 al comienzo de este tema.
Por lo tanto, el SGBD se encarga de interpretar y castear correctamente el valor del str que
le llega como una consulta SQL.
Esto no quiere decir que no debemos validar los datos. En nuestro ejemplo no lo hicimos por
una cuestión de minimizar el código. Pero la aplicación debería validar que, para la duración,
por ejemplo, se escriban solo valores numéricos enteros, por más de que se guarden en un
str. Ya que, si se pasa una cadena con caracteres alfabéticos para ese campo, el SGBD
fallará en el casteo interno y se producirá un error.
Finalmente, como se puede ver en el ejemplo, hemos hecho uso de manejo de errores por
posibles errores en la base de datos. Hablaremos de eso a continuación.
1.4.4. Manejo de errores
MySQL Connector/Python incluye en su librería el módulo mysql.connector.errors, en
el cual implementa distintas excepciones, las cuales se mapean a la numeración de los
códigos de error del servidor MySQL.
La mayoría de las clases definidas en este módulo están disponibles cuando se importa
mysql.connector. Por lo tanto, podemos escribir mysql.connector.Error, como lo
hicimos en el ejemplo.
De todas formas, si se quiere usar una única sentencia except como lo hicimos en nuestro
ejemplo, la excepción Error va a capturarlos a todos ya que es la clase base.
Si queremos manejar excepciones a un nivel más detallado, debemos usar alguna de las
subclases de DatabaseError, como se ve en la jerarquía de clases siguiente:
• Error: Esta excepción es la clase base para todas las demás excepciones en el
módulo errores.
– DatabaseError: Esta excepción es la predeterminada para cualquier error de
MySQL que no se ajuste a las otras excepciones.
• DataError: Esta excepción se lanza cuando hubo problemas con los
datos.
• OperationalError: Esta excepción se lanza para errores
relacionados con las operaciones de MySQL.
• NotSupportedError: Esta excepción se lanza cuando se utilizó
alguna función que no es compatible con la versión de MySQL que
devolvió el error.
• IntegrityError: Esta excepción se lanza cuando la integridad
relacional de los datos se ve afectada.
• ProgrammingError: Esta excepción se genera en errores de
programación, por ejemplo, cuando tiene un error de sintaxis en su
SQL o no se encontró una tabla.
• InternalError: Esta excepción se lanza cuando el servidor MySQL
encuentra un error interno, por ejemplo, cuando se produjo un
interbloqueo.
1.4.5. Mejores prácticas
1.4.5.1. Seguridad
Por razones de seguridad no es recomendable escribir (hardcode) los datos de conexión del
servidor y credenciales del usuario de la base de datos en el archivo principal de la
aplicación, como lo hicimos en los ejemplos.
Más seguro aún, es configurar las credenciales en variables de entorno del sistema y luego
obtenerlas desde la aplicación.
Por otro lado, recordamos la práctica de pasar los parámetros de las consultas por separado
de las sentencias SQL.
Tanto los objetos Connection, como los objetos Cursor están implementados como
Gestores de Contexto, esto quiere decir que los podemos usar con la sentencia with de
Python.
cur = conn.cursor()
resultado = cur.fetchall()
print(resultado)
Por otro lado, se puede crear una clase propia que implemente los protocolos de un Gestor
de Contexto para hacer su uso más cómodo aún.
1.4.5.3. Validación y verificación de datos
Como comentamos anteriormente, siempre debemos revisar que los valores que ingrese el
usuario sean compatibles con los campos de la tabla correspondiente. Si bien el Conector
se encarga de limpiar los datos de código malicioso, nosotros debemos asegurarnos de que
lo que se quiere almacenar corresponda al tipo de dato de cada campo de una tabla, ya sea
un entero, una cadena o una fecha.
Dado que los conjuntos de resultados de las consultas SQL pueden ser muy grandes, es
recomendable utilizar el método apropiado para recuperar elementos del result-set a
medida que los recorre. fetchone() recupera un solo elemento, cuando sabe que el
conjunto de resultados contiene una sola fila. fetchall() recupera todos los elementos,
cuando sabe que el conjunto de resultados contiene un número limitado de filas que caben
cómodamente en la memoria. fetchmany() es el método de propósito general cuando no
puede predecir el tamaño del conjunto de resultados: sigue llamándolo y recorriendo los
elementos devueltos, hasta que no haya más resultados para procesar.
1.4.5.4. Otros
Una recomendación es declarar una clave primaria numérica para cada tabla, que ofrece la
forma más rápida de buscar valores y puede actuar como un puntero a valores asociados en
otras tablas (una clave foránea).
Por otro lado, al momento de definir los campos de una tabla, hacer uso de los tipos de datos
de columna más compactos que cumplen con los requerimientos de la aplicación ayuda al
rendimiento y la escalabilidad porque eso permite que el servidor mueva menos datos entre
la memoria y el disco.
Sobre el manejo de errores, podemos decir que es útil la creación de una excepción
personalizada, para lanzar al momento de capturar una excepción de MySQL. Esto nos
permite implementar nuestro propio mensaje de error en nuestro idioma, en lugar de usar
los mensajes de MySQL que están en inglés.
Para entender qué se puede y qué no se puede hacer en una interfaz web debemos al menos
conocer los elementos de la misma y cómo se construye. Veremos todo esto en el próximo
módulo.
Bibliografía
1. https://dev.mysql.com/doc/connector-python/en/connector-python-installation-
binary.html
2. https://es.wikipedia.org/wiki/Loopback
3. https://es.wikipedia.org/wiki/Localhost
4. https://dev.mysql.com/doc/connector-python/en/connector-python-
reference.html
5. https://dev.mysql.com/doc/connector-python/en/connector-python-api-
errors.html
6. https://dev.mysql.com/doc/connector-python/en/connector-python-api-
errorcode.html