Es posible que estés desarrollando un programa en PYTHON y debas acceder a una base de datos MySQL.
Si haces una búsqueda en GOOGLE, seguramente obtendrás muchas respuestas que te informan sobre MySQLdb como la mejor librería para acceder desde PYTHON a MySQL, además de una serie de pruebas de velocidad donde verás la clasificación por librerías y tiempo para 500, 1000 o 10000 inserciones, actualizaciones... .Estas mejoras en velocidad estan soportadas en código C, que debe compilarse para cada plataforma. Incluso existen paquetes de instalación para la mayoría de las plataformas.
Pero siendo fieles a nuestra incorregible conducta de mirar hasta debajo de la alfombra hemos buscado otras soluciones. Encontramos una librería desarrollada en código PYTHON y soportada con todas las garantías por el propio soporte de MySQL (INCLUSO Tiene instalador MSI para Windows). Se Trata de MySQL Connector . Si compruebas los test de velocidad antes mencionados, seguramente no es la más veloz a la hora de hacer grandes operaciones de inserción y actualización, pero todo es relativo, alguna vez has tenido la tentación de comprar el ordenador más potente que podías pagar? Y cuando has escrito en su teclado compruebas que escribe a la misma velocidad que lo has hecho siempre. Lo que nos lleva a reivindicar que cualquier librería es buena siempre y cuando cubra nuestras necesidades.
A partir de aquí mostraré algo de código necesario para utilizar MySQL Connector:
#!/usr/bin/python
# -*- coding: utf-8 *-*
import sys
import os
import re
import time
import logging
import importlib
import mysql.connector
from mysql.connector import errorcode
class SQLdb(object):
instance = None
#Tratamiento de errores en LOG
__name__ = "SQLdb"
sys.stdout = sys.stderr
logPath = ''
LOG = logging.getLogger(__name__)
hdlr = logging.FileHandler(logPath+__name__+'.log')
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
hdlr.setFormatter(formatter)
LOG.addHandler(hdlr)
LOG.info("SQLdb modo local INICIO")
_module = None
_errorcode = None
cnx = None
cursor = None
Para utilizar la librería debe importar mysql.connector y errorCode, que podrás identificar en el código anterior en color azul.
def __new__(cls, *args, **kargs):
if cls.instance is None:
cls.instance = object.__new__(cls, *args, **kargs)
return cls.instance
El código anterior es una forma de escribir un patrón SINGLETON que es algo fuera de este artículo pero para explicarlo de una forma breve, diré que su finalidad en garantizar que una clase solo tenga una instancia ( en un hilo ) y proporcionará un punto de acceso global para toda la aplicación.
def __init__(self,*args, **kwargs):
"""Abrir acceso a bbdd"""
self.cnx = None;
self.cursor = None;
self._host = kwargs['host'] if kwargs.has_key('host') else '127.0.0.1';
self._port = kwargs['port'] if kwargs.has_key('port') else '3306';
self._username = kwargs['username']
self._password = kwargs['password']
self._database = kwargs['database']
self._driver = kwargs['driver'] if kwargs.has_key('driver') else 'mysql';
self._max_idle_time = kwargs['max_idle_time'] if kwargs.has_key('max_idle_time') else 2*3600;
self._time_zone = kwargs['time_zone'] if kwargs.has_key('time_zone') else "+2:00";
self._sql_mode = kwargs['sql_mode'] if kwargs.has_key('sql_mode') else "TRADITIONAL";
self._charset = kwargs['charset'] if kwargs.has_key('charset') else "utf8";
self._collation = kwargs['collation'] if kwargs.has_key('collation') else "utf8_spanish_ci";
self._conection_timeout = kwargs['connection_timeout'] if kwargs.has_key('connection_timeout') else 100;
self._compress = kwargs['compress'] if kwargs.has_key('compress') else False;
self._autocommit = kwargs['autocommit'] if kwargs.has_key('autocommit') else False;
self._use_pure = kwargs['use_pure'] if kwargs.has_key('use_pure') else True;
#encloser characters for system identifiers(key_delim) and strings(str_delim)
self._key_delim = '"';
self._str_delim = "'";
if self._driver == 'mysql':
#self._module = importlib.import_module('mysql.connector')
#self._errorcode = importlib.import_module("mysql.connector","errorcode")
self._key_delim = '`';
elif self._driver == 'pgsql':
self._module = importlib.import_module('psycopg2')
else:
self.LOG.error('Driver Error Unknown database driver !!!')
raise Exception("Unknown database driver")
# self.rows permite obtener las filas afectadas
self.rows = 0
self.lastId = 0
self._last_use_time = time.time()
try:
if self.reconnect():
if getattr(self,"cursor",None) is not None:
self.cursor.close()
self.cursor = None
except Exception,e:
tb = sys.exc_info()[2]
self.LOG.error('Error <%s> en linea %s !!!' % (str(e),tb.tb_lineno))
En el código anterior obtenemos todos los parámetros para acceder a la base de datos y en azul, tendriamos el código necesario si queremos realizar una clase que pueda acceder a más de un tipo de base de datos - incluso podría realizarse la importación de librerías de forma personalizada para cada tipo de base de datos - , siempre que cumplan la especificación DB V2.0 API ( aunque en este código solo se indica como un mero apunte para quien desee profundizar más ).
Y ahora comenzamos la conexión o mejor dicho, la reconexión. Si una aplicación tiene una conexión abierta demasiado tiempo, es posible que el servidor MySQL corte la comunicación por timeout, si no se ha previsto este aspecto en el código, intentaremos abrir el cursor (sobre la conexión ya cerrada ) y obtendremos un error. Como en nuestro código ya hemos previsto este aspecto, más adelante podrá comprobar nuestro método 'ensure_connected' .
def __del__(self):
self.close()
def close(self):
"""Cierra la conexion a la base de datos"""
try:
if getattr(self,"cnx",None) is not None:
if getattr(self,"cursor",None) is not None:
self.cursor.close()
self.cursor = None
self.cnx.close()
self.cnx = None
except mysql.connector.Error as err:
self.LOG.error(err)
print 'error %s' % err
def reconnect(self):
"""Cierra la conexion con la base de datos y vuelve a abrirla"""
try:
self.close()
config = {'host': self._host, 'port': self._port, 'user': self._username};
if self._driver == 'mysql':
config['password'] = self._password
config['database'] = self._database
config['time_zone']= self._time_zone
config['charset'] = self._charset
config['collation'] = self._collation
config['sql_mode']= self._sql_mode
config['compress']= self._compress
config['autocommit']= self._autocommit
config['raise_on_warnings']= True
#config['use_pure']= self._use_pure
elif self._driver == 'pgsql':
# SI EL
config['database'] = self._dbname
config['password'] = self._password
self.cnx = mysql.connector.connect(**config)
#self.cnx('connection_timeout') = self._connection_timeout
self.cursor = self.cnx.cursor()
return True
except mysql.connector.Error as err:
if err.errno == self._errorcode.errorcode.ER_ACCESS_DENIED_ERROR:
self.LOG.error('Something is wrong with your user name or password !!!')
print 'Something is wrong with your user name or password'
elif err.errno == self._errorcode.errorcode.ER_BAD_DB_ERROR:
self.LOG.error('Database does not exist !!!')
print 'Database does not exist'
else:
self.LOG.error(err)
print 'error %s' % err
return False
Y a continuación, un ejemplo de cómo se construyen SELECT, INSERT, UPDATE, DELETE con los parámetros pasados. Básicamente, en el parámetro 'sql' enviaremos nuestro SQL p.e. :
SELECT * FROM products WHERE code='%s';
y 'parameters' puede ser una tupla o solo un dato o nada (si en 'sql' hubieramos completado todos los datos necesarios), en este caso sería solo un datos p.e. : 03456
a partir de estos parámetros el método execute construye la sentencia SQL completa, y dependiendo de qué tipo de sentencia se trate, nos devolverá el id del registro INSERT, el número de registros afectados por UPDATE o DELETE, o los datos solicitados mediante SELECT.
def execute(self, sql=None , parameters=None):
"""Ejecuta consultas teniendo en cuenta transacciones
sql parametro con el codigo
parameters son las variables que se pueden incorporar al codigo sql
"""
self.ensure_connected()
try:
if sql:
if parameters is not None:
if type(parameters[0]).__name__ == 'tuple':
self.rows = self.cursor.executemany(sql, parameters)
else:
self.rows = self.cursor.execute(sql, parameters)
else:
self.rows = self.cursor.execute(sql)
if sql[0:6].upper()=='INSERT':
# Informa de nº de última fila autoincremental
self.lastId = self.cursor.lastrowid
return self.lastId
elif sql[0:6].upper()=='UPDATE' or sql[0:6].upper()=='DELETE':
# Informa de nº filas afectadas
self.rows = self.cursor.rowcount
return self.rows
elif sql[0:6].upper()=='SELECT':
# Devuelve informacion de todas las filas seleccionadas
return self.cursor.fetchall()
except mysql.connector.Error as err:
self.LOG.error(err)
finally:
if hasattr(self.cnx,"commit"):
self.cnx.commit()
if getattr(self,"cursor",None) is not None:
self.cursor.close()
self.cursor = None
Y por último, el método que permite comprobar y abrir la conexión, si fuera necesario tras un timeout, o solo abrir el cursor.
def ensure_connected(self):
""" Mysql por defecto cierra las conexiones de clientes que están inactivos por
8 horas, pero la biblioteca de cliente no reporta este hecho hasta
intentar realizar una nueva consulta y en ese momento falla.
Preventivamente se cierra y vuelve a abrir la conexion si no se ha utilizado
durante mucho tiempo"""
try:
if (getattr(self,"cursor",None) is not None or (time.time() - self._last_use_time > self._max_idle_time)):
self.reconnect()
else:
self.cursor = self.cnx.cursor()
self._last_use_time = time.time()
except mysql.connector.Error as err:
self.LOG.error(err)
Esto proporciona un punto de partida para utilizar MySQL CONNECTOR en su codificación PYTHON y solo debería trabajar un poquito más para aplicar TRANSACTION, COMMIT y ROLLBACK. Salvo errores tipográficos, he probado este código y funciona correctamente.
Espero que haya sido de utilidad.
No hay comentarios:
Publicar un comentario