pycamp ORM, haciendo fácil usar sqlalchemy
|
|
¿De que se trata este articulo?
Esencialmente de tres cosas:
- ORM / Object Relation Mapper, ¿que es?, ¿para que sirve?
- SQLAlchemy, un ORM en Python
- pycamp.orm, 140 lineas (aproximadamente) que facilitan usar SQLAlchemy
¿Para que son estas tres cosas? Para trabajar con bases de datos relacionales, de diferente fabricantes como son:
- PostgreSQL
- MySQL
- Oracle
- SQLite
Nuestro propósito
¿Conquistar el mundo? Si, podría ser, pero no lo es en este artículo ;). Nuestro propósito es poder trabajar con bases de datos relacionales utilizando Python y para tener un panorama general de los diferentes actores que intervienen en esta escena, pues debemos conocer a:
- Jenna Jameson, erghh digo, SQL / Structured Query Language
- Base de Datos (PostgreSQL, MySQL, etc)
- Conector / Driver de Python para conectarnos contra la Base de Datos
- Python
El lenguaje SQL / Structured Query Language es un estándar ANSI (American National Standars Institute) el cual nos permite consultar, definir y modificar datos en una DB relacional. Ahora, esto esta buenísimo!, tenemos un estándar!!! Osea: las bases de datos que implementen este estándar podrán entender los comandos que programemos en este lenguaje.
Segundo: Base de Datos que implementan en PARTE el SQL. Esto quiere decir que pueden tener otras palabras agregadas al lenguaje SQL que implementan, por ejemplo MySQL puede tener sentencias que no existen en PostgreSQL.
Conector / Driver (ejemplos son psycopg2, MySQLdb) es código que permite que Python se comunique con la Base de Datos. Las tareas que contempla un conector es manejar por ejemplo la parte de sockets, y otras muchas operaciones de las cuales no tengo ni idea ;).
Python lenguaje de programación todopoderoso con el cual podríamos conquistar el mundo, pero en este articulo lo utilizaremos para trabajar con bases de datos relacionales.
Conocidos los actores, podríamos crear un súper diagrama en ASCII que represente como se relacionan estos actores entre si:
MySQL <------------> sql_lang_1 <---> MySQLdb <----> Python PostgreSQL <-------> sql_lang_2 <---> psycopg2 <---> python Oracle <-----------> sql_lang_3 <---> cx_Oracle <--> Python
Explicando el primer caso de nuestro súper diagrama en ASCII, y leyéndolo desde derecha a izquierda: contamos con nuestro todopoderoso lenguaje Python, cual mediante el conector MySQLdb, realiza consultas utilizando sql_lang_1 a la base de datos MySQL. ¿Simple no?
Mismas consultas, diferentes bases de datos
Vamos a plantear una hipotética situación en la cual debemos crear un programa el cual precisa guardar los datos que genera en una base de datos relacional.
Resulta además que la empresa donde trabajamos esta utilizando MySQL, con lo cual nos disponemos a escribir nuestras consultas utilizando el conector MySQLdb.
Luego de 1337 meses de desarrollo tenemos nuestro programa completamente funcional, pero resulta que nuestro/a jefe/a se ha levantado con un humor para nada compatible con MySQL y da la directiva de que debemos utilizar PostgreSQL y que todo software que utilice una base de datos distinta de PostgreSQL sera condenado a jugar en la Arena de TRON!
Ok, nosotros y nuestro programa se encuentran en una situación complicada, hemos de cambiar todas las consultas que hemos realizado para MySQL para que funciones con PostgreSQL o conseguirn rápidamente un frisbees (http://en.wikipedia.org/wiki/Flying_disc) y comenzar a practicar para cuando nos toque combatir en la Arena de TRON (http://en.wikipedia.org/wiki/Tron_(film)).Al margen de la quizá cómica situación y para ajustarme un poco a la charla que se dio en el PyDay de Córdoba, podríamos decir que:
- Las bases de datos tienen cada una su propio dialecto.
- Con Python, utilizamos drivers/conectores para poder hablar el dialecto de la DB a la cual nos conectamos y utilizamos.
- El escribir en nuestro código SQL utilizando el dialecto de la DB parece no ser una buena idea, puesto que al cambiar de motor / engine de DB nos vemos obligados a tener que pasar todas nuestras queries al dialecto de la nueva DB.
SQLAlchemy, el ORM utilizado por MacGyver y Mario Baracus
A toda la problemática planteada anteriormente de que veníamos escribiendo queries según el engine/dialecto de nuestra DB, existe una solución y esta se llama SQLAlchemy.
SQLAlchemy es un ORM y un ORM es un O*bject *R*elation *M*apper, osea: un *mapeador/conversor de las relaciones de los datos de nuestra DB a objetos. Por sino quedo claro, veamos un ejemplo:
Supóngase una tabla "Persona" en nuestra DB cual tiene las columnas:
- Nombre de tipo VARCHAR.
- FechaNacimiento de tipo DATE.
Hasta aquí es la parte relacional, ahora si nosotros representáramos esta estructura de datos con objetos en python, bien podríamos tener lo siguiente:
import datetime class Persona(object) """ I'm not only a cute face, i'm a person! """ nombre = '' borndate = datetime.datetime.now()
El trabajo del ORM es justamente leer las tablas, columnas, propiedades y las relaciones entre datos para expresarlos utilizando objetos Python. Esto para nosotros es muy bueno, puesto que como veremos mas adelante, vamos a trabajar con objetos Python y no vamos a estar escribiendo SQL. Será entonces responsabilidad del ORM traducit todo a dicho lenguaje para interactuar con la DB. Veamos esto con un súper gráfico ASCII iluminador del entendimiento:
DATABASE <-----> SQLALCHEMY(ORM) <------> PYTHON
Entonces: Escribimos python y SQLAlchemy traduce lo traduce al dialecto que utiliza nuestra DB. Por ejemplo: Un select en nuestra tabla Persona se escribe:
>>> from edo import Session >>> from limbo import Persona >>> session = Session() >>> first_person = session.query(Persona).first() >>> first_person.name 'Lady Gaga'
En el ejemplo hemos importado nuestra clase Persona y un ficticio Session con el cual hemos realizado nuestra consulta. Note-se que se trabajó con Python y no con el dialecto de nuestra DB (de ello se encarga SQLAlchemy ;)).
Con esto queremos decir que el código python escrito anteriormente, gracias a SQLAlchemy, funciona en PostgreSQL, MySQL o en Oracle sin tener que modificar nada, ¿No es bonito eso ? ^^'
Haciendo Alquimia, ingredientes
Para comenzar a utilizar SQLAlchemy es preciso entender los siguientes cuatro elementos: Los cuatro elementos clásicos griegos (tierra, agua, fuego y aire) datan de los tiempos presocráticos y perduraron a través de la Edad Media hasta el Renacimiento influenciando profundamente la cultura y el pensamiento europeo, pero no así en SQLAlchemy; puesto que los cuatro elementos fundamentales a entender son:
- Sesiones
- Mapper
- MetaData
- Engine
Sesiones: Las sesiones están hechas para abrir y cerrar conexiones contra la DB. Esto en si no es verdad, puesto que SQLAlchemy mantiene un pool de conexiones persistentes contra la DB con lo cual cuando creamos una sesión, estaremos tomando un elemento de este pool de conexiones y al cerrar nuestra sesión, estaremos retornando la conexión al pool de conexiones. Por lo dicho anteriormente, crear sesiones es muy rápido y barato a nivel recursos, porque las conexiones ya están previamente hechas y disponibles en nuestro pool de conexiones.
Mapper: Es un código de tecnología de punta, encargado justamente de mapear las estructuras y propiedades de nuestra DB a objetos Python y viceversa.
MetaData: Por ahora, lo dejaremos como un objeto oscuro.
Engine: Es un objeto en el cual seteamos las propiedades de la conexión contra nuestra base de datos, como ser: el usuario, password, nombre de la base de datos y motor de la base de datos (Oracle, MySQL, PostgreSQL, etc). También podemos setear la cantidad de conexiones persistentes a tener en nuestro pool de conexiones y muchas chucherías mas.
Orden de los ingredientes
Como en todo procedimiento alquimístico, el orden es importante y es por ello que a continuación detallamos cual es el orden de los ingredientes de SQLAlchemy:
- Crear el engine
- Bindear contra nuestro engine
- Listo :)
Lo primero que hacemos es declarar el engine. Y pues claro! Es ahí donde definimos el usuario, password, host al que conectarnos, tipo de DB y todas esas cosas; así que tiene algo de sentido que esto sea lo primero a realizar.
[1] from sqlalchemy import create_engine [2] url = 'mysql://user:passwd@host/pet' [3] engine = create_engine(url)
En la linea [1] importamos create_engine desde sqlalchemy, en la linea [2] creamos una url cual define los datos de conexión siendo los mismos:
url = 'TIPODEDATABASE://USUARIO:PASSWD@IPHOST/DATABASENAME'
Y, por último en el paso [3] llamamos al método create_engine con nuestra url como parámetro
Ahora veamos esto con un ejemplo de código real.
Para ello voy a estar utilizando un virtualenv y dentro del mismo instalaré sqlalchemy (para saber mas acerca de crear virtualenvs, ver: http://pypi.python.org/pypi/virtualenvwrapper)
edvm@Yui:~$ mkvirtualenv --no-site-packages pet New python executable in pet/bin/python Installing distribute......................................................done. virtualenvwrapper.user_scripts creating /home/edvm/.venvs/pet/bin/ predeactivate virtualenvwrapper.user_scripts creating /home/edvm/.venvs/pet/bin/postdeactivate virtualenvwrapper.user_scripts creating /home/edvm/.venvs/pet/bin/preactivate virtualenvwrapper.user_scripts creating /home/edvm/.venvs/pet/bin/postactivate virtualenvwrapper.user_scripts creating /home/edvm/.venvs/pet/bin/get_env_details (pet)edvm@Yui:~$ pip install ipython Downloading/unpacking ipython ... ... Successfully installed ipython Cleaning up... (pet)edvm@Yui:~$ pip install sqlalchemy Downloading/unpacking sqlalchemy .... .... Successfully installed sqlalchemy Cleaning up...
Ahora ejecuto ipython y comenzamos a trabajar:
(pet)edvm@Yui:~$ ipython .... In [1]: import os In [2]: from sqlalchemy import create_engine In [3]: db = os.path.join(os.path.abspath(os.path.curdir), 'db.sql') In [4]: engine = create_engine('sqlite:///%s' % db) In [5]: engine Out[5]: Engine(sqlite:////home/edvm/db.sql)
Como podemos ver, ya hemos obtenido el primer item que precisábamos de nuestra quest, ahora pues nos quedan las sesiones, el mapper y el metadata. A por ellos!:
In [6]: from sqlalchemy import MetaData In [7]: meta = MetaData(bind=engine) In [8]: meta Out[8]: MetaData(Engine(sqlite:////home/edvm/db.sql)) In [9]: from sqlalchemy.orm import mapper In [10]: from sqlalchemy.orm import sessionmaker In [11]: Session = sessionmaker(bind=engine)
Explicando lo anterior, importamos MetaData y guardamos en una variable meta la configuración de nuestro MetaData con nuestro engine. También hemos importado sessionmaker y le hemos pasado como parámetro nuestro engine ¿recuerdan que el primer paso era crear el engine y el segundo paso bindear nuestro metadata y sesiones a nuestro engine?. Ahora, ya podemos crear sesiones llamando a nuestro Session, esto tomara una de las conexiones persistentes del pool de conexiones creados para que podamos realizar consultas. Si sobre esa sesión ejecutamos el método close() devolveremos dicha conexión nuevamente a nuestro pool de conexiones, ej:
In [12]: session = Session() In [13]: session.query(...).all() In [14]: session.close()
Pues aun nos queda el mapper, peguémosle una mirada a lo que dice el docstring del mismo:
In [15]: mapper? Type: function Base Class: <type 'function'> String Form: <function mapper at 0xa3cabfc> Namespace: Interactive File: /home/edvm/.venvs/pet/.... Definition: mapper(class_, local_table=None, *args, **params) Docstring: Return a new :class:`~.Mapper` object. **:param class\_: The class to be mapped.** **:param local_table: The table to which the class is mapped, or None if this mapper inherits from another mapper using concrete table inheritance.**
La documentación de mapper nos dice que toma dos parámetros siendo el primero una clase y el segundo un local_table. Para los que vieron Dragon Ball, entender esto les va a resultar bastante sencillo ... mapper es como la fusión entre Goku y Vegeta para formar a Vegito (http://dragonball.wikia.com/wiki/Vegito) o Gogeta (http://dragonball.wikia.com/wiki/Gogeta). Metes un Goku, metes un Vegeta y sacas un Vegito/Gogeta. ¿Fácil no?
Ok, para los que no vieron Dragon Ball, esto seria como preparar un exprimido de naranjas: Metes un exprimidor de naranjas con naranjas y sacas un exprimido de naranjas. ¿Vieron que era sencillo el tema del mapper? ;)
Comentando un poquito mas, los local_table son las tablas de nuestra DB y la class es una clase cualquiera que sera unida/asociada contra la tabla de nuestra DB y lo que obtenemos como resultado de pasarle a mapper nuestra clase y nuestra local_table es pues: un objeto de tipo Mapper. Tratemos de ver esto con un ejemplo:
In [1]: from sqlalchemy import create_engine In [2]: url = 'mysql://grids:grids@localhost/grids' In [3]: engine = create_engine(url) In [4]: from sqlalchemy import MetaData In [5]: meta = MetaData(bind=engine, reflect=true) In [6]: meta.tables.keys() Out[6]: [u'django_admin_log', u'auth_permission', u'auth_group', In [7]: type(meta.tables['django_admin_log']) Out[7]: <class 'sqlalchemy.schema.Table'>
Con este ejemplo nos hemos conectado a una base de datos de un proyecto Django, y prestemos atención a la linea [5] y la linea [7].
En la linea [5] definimos nuestro metadata pasándole como parámetros nuestro engine y luego hemos pasado un segundo parámetro reflect=True que hace que SQLAlchemy se transforme en Super Saiayin (y NO en un exprimido de naranjas) y se conecte a nuestra base de datos auto descubriendo mágicamente todas nuestras tablas y metiéndolas en un diccionario dentro de meta.tables siendo las claves del mismo los nombres de las tablas y sus values, objetos de tipo sqlalchemy.schema.Table.
Ya contamos con las local_tables de nuestra base de datos, ahora nos falta crear una clase por cada tabla, para poder tener los dos parámetros necesarios de mapper, por lo que a iterar:
In [11]: class DB(object): ....: """ ....: Dummy DB Object to store stuff ....: """ ....: pass ....: In [13]: db = DB() In [17]: from sqlalchemy.orm import mapper In [18]: for tablename in meta.tables.keys(): ....: obj = type(str(tablename), (object,), {}) ....: setattr(db, tablename, obj) ....: mapper(obj, meta.tables[tablename]) # pasamos la clase y la local_table Out[18]: <Mapper at 0xaa4d76c; django_admin_log> Out[18]: <Mapper at 0xaa4db6c; auth_permission> Out[18]: <Mapper at 0xaa524ec; auth_group> Out[18]: <Mapper at 0xaa529ec; auth_group_permissions> In [19]: db.auth_user Out[19]: <class '__main__.auth_user'>
En las lineas anteriores creamos una clase DB que la vamos a utilizar para guardar las tablas pasadas por mapper de nuestra DB, en [13] instanciamos DB, luego importamos en [17] a mapper e iteramos en [18] por cada elemento del diccionario de meta.tables. Osea, estamos iterando por cada tabla automágicamente descubierta por el reflect de MetaData, creando al vuelo en obj un nuevo tipo con el método type, asignándole a esta nuevo tipo el nombre de la tabla. Luego, con setattr metemos el objeto creado recientemente en nuestra instancia db y finalmente llamamos a mapper pasándole como primer parámetro el objeto que creamos con type y como segundo parámetro el objeto de tipo sqlalchemy.schema.Table, osea nuestro local_table.
Para terminar, nos encontramos con que tenemos en [19] como atributo de db a nuestra tabla ya mapeada auth_user.
Es mas, ahora podríamos hacer una consulta como ser:
In [28]: from sqlalchemy.orm import sessionmaker In [29]: Session = sessionmaker(bind=engine) In [30]: session = Session() In [36]: qobj = session.query(db.auth_user).first() In [37]: qobj.username Out[37]: 'admin' In [38]: qobj.password Out[38]: 'sha1$04a19$2559e5f16eb58cab606c18443b552831748187ac0' In [39]: session.close()
¿Y pycamp.orm?
Bueno, pycamp.orm son unas 140 lineas aprox que al escribir este articulo y revisar nuevamente el fuente, veo que pueden ser muchísimas menos.
Hace exactamente lo que se muestra en este articulo (jejeje). Por lo que ya sabes como funciona pycamp.orm ;)
Para comentarles un poco, este módulo sale del PyCamp que fue organizado en La Falda, Córdoba, Argentina; el cual que fue un campamento donde se juntan personas que programan en Python
Para mi fue mi primer PyCamp y la pase bárbaro, muy buena onda entre todos. Además esta bueno conocer personalmente la gente que hablas por IRC, conocer nuevas personas, jugar juegos de rol, que te traten constantemente de mutante o comunista (en el juego de rol digo ;), perder alevosamente en torneos de mete-gol, aprender a hacer malabares y ah, cierto ! programar cosas que TE GUSTAN! En fin, veamos como utilizar pycamp.orm al día de hoy:
(pet)edvm@Yui:~$ hg clone https://edvm@bitbucket.org/edvm/pycamp.orm (pet)edvm@Yui:~$ cd pycamp.orm (pet)edvm@Yui:~/pycamp.orm$ ls README buildout pycamp setup.py (pet)edvm@Yui:~/pycamp.orm$ python setup.py install ... ... (pet)edvm@Yui:~/pycamp.orm$ ipython
In [1]: from pycamp.orm.mapper import Database In [2]: from pycamp.orm.mapper import DatabaseManager In [3]: mydb = Database('grids', user='grids', passwd='grids', engine='mysql') In [4]: manager = DatabaseManager() In [5]: manager.add(mydb) In [6]: auth_user = manager.mysql.grids.auth_user In [7]: sesion = manager.mysql.grids.session() In [8]: qobj = sesion.query(auth_user).first() In [9]: qobj.username Out[9]: 'admin' In [10]: qobj.password Out[10]: 'sha1$04a19$2559e5f16eb58cab606c18443b552831748187ac0' In [11]: sesion.close()
Explicando un poco lo que hace pycamp.orm: De mapper importamos un Database que es donde seteamos el usuario, password, el tipo de base de datos, etc. Los datos que storeamos en Database son los que luego utilizamos con el método create_engine de SQLAlchemy ;), en fin, en [4] creamos un DatabaseManager que es un BORG (http://code.activestate.com/recipes/66531-singleton-we-dont-need-no-stinkin-singleton-the-bo/) el cual tiene un método add() que recibe como parámetro un Databas del cual saca los datos para crear el metadata; llama a sessionmaker, mapper y nos deja todo listo para comenzar a realizar queries contra la DB :).
Bueno, esto es todo, para terminar el articulo:
El código esta por acá:
https://bitbucket.org/edvm/pycamp.orm/src/385aeb2f6e12/pycamp/orm/mapper.py
Podes ver el vídeo de este articulo que es un poquito diferente por acá :
http://python.org.ar/pyar/PycampORM
Te podes bajar las filminas de la charla por acá:
http://xip.piluex.com/PYCAMP_ORM.pdf
Y eso es todo, espero que haya gustado!
Help PET: Donate
blog comments powered by Disqus