Translate

domingo, 28 de junio de 2015

Python y MySQL CONNECTOR

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