Introducción:

Este artículo pertenece a la serie Python’s ORMs, en donde revisaremos la manera de trabajar con tres ORMs y algunos de sus alcances. Asumiré que sabemos que un ORM nos permite mapear tablas de bases de datos con Objetos (mediante clases), haciendo que trabajar con ellas sea similar al resto de nuestros proyectos.

Para poder compararlos bien realizaremos la misma tarea con todos. En este caso la estructura de una aplicación web de blogging, así podremos ver como se trabaja con tablas relacionales e ir un poco más allá del básico ejemplo de tablas independientes.

No desarrollaremos la aplicación por completo, simplemente mostraremos cómo estructurar y trabajar con los modelos desde los distintos ORMs.

La aplicación será una web donde los usuarios pueden crear posts, hacer comments a dichos posts y por último hacer replies a esos comments.

Utilizando Pony ORM: Entity Relationship Diagram Editor definimos nuestro modelo de la siguiente manera:

 

Vamos a asumir que queremos un usuario con nickname y password, y que este usuario(s) será capaz de escribir posts, comments y replies.

A su vez cada post tendrá sus propios comments y cada comment sus propios replies.

Esto nos da una estructura bastante estándar de blog. No haremos replies a los replies para no complicar demasiado el modelo, y porque en general nunca me han gustado los foros o blogs en que los comments acaban escalonadamente hasta el infinito, me da la impresión de que facilitan que se pierda el punto de la discusión, como sucede a veces en Reddit o Hacker News

PonyORM

Pony es un ORM de licencia tipo “multi-licencia”. Esto significa que según el caso de uso se aplica un costo o no.

Es gratuito para proyectos open source y de uso personal, y tiene un costo que varía desde los $30 hasta los $2000 dólares para licencias comerciales.
Es por esto que lo utilizo en todos mis proyectos personales, pero aún no lo he utilizado en proyectos comerciales. Si quieren leer un poco más sobre sus licencias pueden dirigirse a su tarifario.

Como definen en su web:

“Utiliza Python puro para hablar con tus datos”

Y es que este es el encanto de Pony. Como veremos en la sección de queries Pony permite traducir generadores de Python en instrucciones SQL de una manera muy simple.

Todo lo desarrollado aquí se encuentra en el Repositorio de Github .

Ventajas

  • Sintaxis pythonica para componer queries
  • Optimización de los queries automática (Si no eres experto en SQL probablemente escriba mejores queries que tú, maneja los LEFT JOIN de manera inteligente)
  • Solución elegante al problema de búsquedas de magnitud n+1
  • Una herramienta gráfica para modelar esquemas de bases de datos (jamás la he usado aún…)

Desventajas

  • NO SOPORTA MIGRACIONES
  • Licencia con costo para proyectos no opensource o personales.

Update (25.02.2015):

Hace unas horas Alexander Kozlovsky ha escrito al mailing list de Pony informado que están en las bases finales de prueba de Pony.js que pretende permitir realizar el mismo tipo de búsquedas desde el frontend, Por ahora soporta knockout, pero pronto soportará AngularJS.

También mencionó que las migraciones son el siguiente punto en su agenda, y que deberían estar disponibles para finales de agosto.

Comencemos

Comencemos importando Pony y definiendo nuestra base de datos.
Ya que es más sencillo y conveniente para la fase de desarrollo, utilizaremos Sqlite como base de datos.
En un archivo llamado pony_example.py escribimos:


#bultin library
import os

#external libraries
import pony.orm as pony


basedir = os.path.abspath(os.path.dirname(__file__))
PONY_DATABASE_URI = os.path.join(basedir, 'pony.db')

database = pony.Database(
    "sqlite",
    PONY_DATABASE_URI,
    create_db=True
)

Importamos el orm de Pony, prefiero utilizar un alias ya que así hay que escribir menos y creo que queda bastante claro que Pony cumplirá la funcionalidad de ORM.
Luego paso a realizar una tarea bastante estándar, definiendo mediante la librería os el directorio donde se encuentra mi archivo de trabajo, y utilizamos esta variable basedir para definir donde queremos que se guarde nuestra base de datos Sqlite.

A continuación creamos la conexión a la base de datos mediante pony.Database, y le pasamos tres argumentos. El primero es en este caso el tipo de base de datos que utilizaremos. El segundo parámetro es la ruta donde guardaremos esta base de datos, y el tercer parámetro, create_db define si Pony debe intentar crear la base de datos al realizar la conexión o no. Lo bueno de Pony es que,  si la base de datos ya existe, simplemente nos avisará de esto y continuará su labor.

Ahora pasemos a definir nuestro modelo de usuario, post, comment y reply:



class User(database.Entity):
    """User, it is asociated with the posts, comments and replies he makes"""

    nickname = pony.Required(str, 20, unique=True)
    password = pony.Required(str, 40)
    email = pony.Required(str, 40, unique=True)

    
    def __repr__(self):
        return ''.format(self.nickname, self.email)

class Post(database.Entity):

    title = pony.Required(str)
    body = pony.Required(pony.LongStr)
   

    def __repr__(self):
        return ''.format(self.id, self.title)

class Comment(database.Entity):

    title = pony.Required(str)
    body = pony.Required(pony.LongStr)
  

    def __repr__(self):
        return ''.format(self.id, self.title)


class Reply(database.Entity):

    body = pony.Required(pony.LongStr)    

    def __repr__(self):
        return 'Reply {}, to comment {}'.format(self.id, self.comment)

Ok, veamos que sucede aquí.

Comenzamos declarando la clase User como descendiente de database.Entity. Recordemos que esta database es la conexión que definimos anteriormente.

Luego pasamos a definir las columnas de nuestra tabla. Tengamos en cuenta que a diferencia de otros ORM, Pony automáticamente define la columna id.

Definimos las tres columnas como requeridas para poder agregar un elemento a la base de datos utilizando el método Required (pony también cuenta con el objeto Optional). El tipo de data que almacenaremos en esta columna se define en los parámetros. Hemos elegido el parámetro str ya que a partir de su versión 0.6, Pony es totalmente compatible con Python 2 & 3, manejando por debajo todas las complejidades de unicode. str es un alias para lo que antes eran los campos de tipo unicode.
El segundo argumento define el largo máximo de texto a almacenar en dichos campos, y el tercero,  si queremos que la data introducida en esas columnas sea unique o no. Esto significa que esa data no puede estar repetida, así nos aseguramos que no haya dos usuarios con el mismo nickname o correo electrónico.

Por último redefinimos __repr__, para que nos muestre el nombre de usuario y su email, ya que en estos casos me gusta tomar control de lo que devuelven los objetos de los ORM.

Lo mismo sucede en todos los otros casos, con la excepción de las columnas de tipo LongStr. Al igual que str ,  LongStr maneja unicode para Python 2 y 3, pero en vez de tratarse de columnas de tipo VARCHAR se trata de BLOB.

Relacionando las tablas

Hasta ahora hemos definido nuestras tablas y sus columnas, ha sido fácil. Pero. ¿Cómo vamos a definir ahora la relación entre estas?
Pony implementa las relaciones de la siguiente manera:

Una relación entre dos entidades es definida al utilizar dos atributos que especifican ambos extremos de una relación.

En la práctica esto simplemente significa que hay que definir un atributo en cada objeto que se desea relacionar.
En nuestro caso User debe tener relacionados todos los posts que ha escrito. Post debe saber que esto es así, por lo que no se debería poder crear un Post si no hay un usuario asociuado. Veamos cómo se hace:



class User(database.Entity):
    """User, it is asociated with the posts, comments and replies he makes"""

    nickname = pony.Required(str, unique=True)
    password = pony.Required(str)
    email = pony.Required(str, unique=True)
    
    # Relación con la tabla Post
    posts = pony.Set("Post")

    def __repr__(self):
        return ''.format(self.nickname, self.email)

Y agregamos a Post:



class Post(database.Entity):

    title = pony.Required(str)
    body = pony.Required(pony.LongStr)

    # Relación con la tabla User
    user = pony.Required(User)    

    def __repr__(self):
        return ''.format(self.id, self.title)

Como vemos es bastante sencillo. User define su relación con Post mediante un set. Y Post simplemente define el campo relacionado pasando a User como argumento del metodo pony.Required.

Vamos entonces a actualizar nuestro archivo relacionando todas las tablas como planeamos:



#bultin library
import os

#external libraries
import pony.orm as pony

basedir = os.path.abspath(os.path.dirname(__file__))
PONY_DATABASE_URI = os.path.join(basedir, 'pony.db')

database = pony.Database(
    "sqlite",
    PONY_DATABASE_URI,
    create_db=True
)

class User(database.Entity):
    """User, it is asociated with the posts, comments and replies he makes"""

    nickname = pony.Required(str, unique=True)
    password = pony.Required(str)
    email = pony.Required(str, unique=True)

    posts = pony.Set("Post")
    comments = pony.Set("Comment")
    replies = pony.Set("Reply")

    def __repr__(self):
        return ''.format(self.nickname, self.email)


class Post(database.Entity):

    title = pony.Required(str)
    body = pony.Required(pony.LongStr)

    user = pony.Required(User)

    comments  = pony.Set("Comment")

    def __repr__(self):
        return ''.format(self.id, self.title)

class Comment(database.Entity):

    title = pony.Required(str)
    body = pony.Required(pony.LongStr)

    user = pony.Required(User)
    post = pony.Required(Post)

    replies = pony.Set("Reply")

    def __repr__(self):
        return ''.format(self.id, self.title)


class Reply(database.Entity):

    body = pony.Required(pony.LongStr)

    user = pony.Required(User)
    comment = pony.Required(Comment)

    def __repr__(self):
        return 'Reply {}, to comment {}'.format(self.id, self.comment)

# enciende el debug
pony.sql_debug(True)

# crea la tabla si no existe
database.generate_mapping(create_tables=True)

Al final de nuestro archivo hemos pasado dos comandos más a Pony.

pony.slq_debug(True) activa el debuging de SQL, lo increible de esto lo veremos al crear la tabla, y el segundo comando database.generate_mappng(create_table=True) genera el esquema de SQL si la tabla no existe.

Para ver si todo funciona ejecutamos nuestro archivo:



$ python pony_examples.py

Y gracias a pony.sql_debug veremos todos los comandos de SQL ejecutándose. Esto, al menos en mi caso, me permite refrescar y aprender SQL.

Si buscan el archivo pony.db en donde debió guardarse lo encontrarán.

Poblando data

Pony permite trabajar con las tablas de manera bastante intuitiva. Vamos a ver ahora como,  por ejemplo, podemos poblar algo de data usando un decorador que manejará el contexto.



import pony.orm as pony

from models import User, Post, Comment, Reply


@pony.db_session
def add_data():

	new_user = User(
		nickname = "Dieguito",
		password = "mystrongpassword",
		email = "dieguito@athelas.pe"
	)

	new_user2 = User(
		nickname = "Ithilnaur",
		password = "mystrongpassword2",
		email = "ithilnaur@athelas.pe"
	)

	new_post = Post(
		title = "Como la vida misma",
		body="It's a dangerous business, Frodo, going out your door. You step onto the road, and if you don't keep your feet, there's no knowing where you might be swept off to.  ",
		user = new_user
	)

	new_comment = Comment(
		title = "Tu no sabes",
		body = "Roads go ever ever on,Over rock and under tree,By caves where never sun has shone,By streams that never find the sea;Over snow by winter sown,And through the merry flowers of June,Over grass and over stone, And under mountains in the moon.",
		user = new_user2,
		post = new_post
	)

	new_reply = Reply(
		body = "Some who have read the book, or at any rate have reviewed it, have found it boring, absurd, or contemptible, and I have no cause to complain, since I have similar opinions of their works, or of the kinds of writing that they evidently prefer.",
		user = new_user,
		comment = new_comment
	)

if __name__ == '__main__':
	add_data()

El decorador @pony.db_session es una de las mejores cosas que ofrece Pony. Este gestiona las conexiones con la base de datos, terminando la sesión automáticamente.

Pony también nos ofrece la manera de trabajar con with statements:


with pony.db_session:
    new_user = User(
		nickname = "Dieguito",
		password = "mystrongpassword",
		email = "dieguito@athelas.pe"
	)

Personalmente para proyectos con Flask utilizo el decorador para gestionar cada ruta.

Para poder usar mejores ejemplos con los queries en Pony necesitamos poblar la data un poco más, en este caso yo voy a utilizar Sqliteman. Es una herramienta open source que nos permite trabajar visualmente con bases de datos Sqlite. Esta herramienta nos permite poblar las tablas de nuestra base de datos de manera automática.
He creado 10 users, cada user tiene un post, cada post un comment y cada comment un reply además de los que creamos en el paso anterior. (Al ser data random realmente no es muy útil ni legible, pero nos permite trabajar en los ejemplos=).

Pueden poblar esta base de datos con cualquier herramienta que crean conveniente. Mockaroo es otra buena opción.

Pythonic Queries

Ya hemos visto como Pony genera relaciones entre tablas de manera muy sencilla, como maneja el contexto de las bases de datos de forma automática, ahora veremos lo que hace a Pony mi preferido, los queries.

Vamos a realizar distintos ejemplos de como trabajar con Pony. En el ejemplo de poblar las tablas utilizamos el decorador @pony.db_session para gestionar las conexiones al ejecutar una función, ahora lo haremos dentro de la función con with .



import pony.orm as pony

from models import User, Post, Comment, Reply



def printUsers():
	""" Imprimimos en pantalla todos los objetos user de la BBDD """
	with pony.db_session:
	    users = pony.select(user for user in User)
	    for user in users:
	        print user

En este primer ejemplo utilizamos el método pony.select() para realizar un query. Notemos que la sintaxis es la misma que utilizamos para trabajar con list comprehensions.
Selecciona a cada usuario dentro de la tabla User . 

Al ejecutar esta función el resultado se imprime en la consola


>>> import queries
>>> queries.printUsers()

GET NEW CONNECTION
SWITCH TO AUTOCOMMIT MODE
SELECT "user"."id", "user"."nickname", "user"."password", "user"."email"
FROM "User" "user"



....
RELEASE CONNECTION


La primera parte es el query en SQL puro, y debajo vemos la representación de nuestros objetos User según los definimos en models.py

Veamos que más cosas podemos hacer con pony:



def getUserById(_id):
	""" Imprimimos en pantalla el objeto User seleccionado por su id """
	with pony.db_session:
		user = User.get(id=_id)
		print user

En este caso utilizamos el método get() que heredan todos nuestros modelos. Get nos permite aplicar ciertas reglas al modelo con el que trabajamos.

El resultado de aplicar esta función sería:



>>> import queries
>>> queries.getUserById(1)

GET CONNECTION FROM THE LOCAL POOL
SWITCH TO AUTOCOMMIT MODE
SELECT "id", "nickname", "password", "email"
FROM "User"
WHERE "id" = ?
[1]


RELEASE CONNECTION

Ahora probemos realizar una búsqueda filtrando los Post según su autor



def getUserPosts(user):
	""" Imprimimos en pantalla todos los objetos Post del user con el Id especificado """
	with pony.db_session:
		posts = pony.select(post for post in Post if post.user.id == user)
		for post in posts:
			print post

Vemos como pony.select() nuevamente acepta la sintaxis de un list comprehension aunque esta aumente. Notemos también como al haber determinado la relación de User con Post user puede ser accedido como una propiedad del mismo.

El resultado:



>>> import queries
>>> queries.getUserPosts(1)
GET CONNECTION FROM THE LOCAL POOL
SWITCH TO AUTOCOMMIT MODE
SELECT "post"."id", "post"."title", "post"."user"
FROM "Post" "post"
WHERE "post"."user" = ?
[1]


RELEASE CONNECTION

Pony también nos permite trabajar directamente con SQL:



def getComments(user):
	""" Imprime todos los objetos comment del user con el ID especificado """
	with pony.db_session:
		comments = Comment.select_by_sql("SELECT * FROM comment WHERE user = " + str(user) )
		for comment in comments:
			print comment

El método select_by_sql() acepta strings con órdenes de SQL, pero solamente strings. Es por esto que debo convertir el parámetro user en string dentro del método.

El resultado:



>>> import queries
>>> queries.getComments(2)
GET CONNECTION FROM THE LOCAL POOL
SWITCH TO AUTOCOMMIT MODE
SELECT * FROM comment WHERE user = 2


RELEASE CONNECTION

Pony trae muchas funcionalidades para trabajar con nuestras bases de datos, podemos encontrar muchos ejemplos en su documentación.

Veamos un ejemplo más:



def limitReplies(limit):
	""" Imprime el numero de replies pasado como argumento y ordenalos segun el User que los creo """
	with pony.db_session:
		replies = pony.select(reply for reply in Reply).order_by(Reply.user).limit(limit)

		for reply in replies:
			print reply

Aquí vemos dos funcionalidades más, encadenamos la búsqueda de select(), la pasamos a order_by()  que, como su nombre indica, permite ordenar los resultados de una búsqueda según algún parámetro. Por último le pedimos que limite el número de resultados con el método limit().

Cómo funciona esto:


>>> import queries
>>> queries.limitReplies(1)
GET CONNECTION FROM THE LOCAL POOL
SWITCH TO AUTOCOMMIT MODE
SELECT "reply"."id", "reply"."user", "reply"."comment"
FROM "Reply" "reply"
ORDER BY "reply"."user"
LIMIT 1

SELECT "id", "title", "user", "post"
FROM "Comment"
WHERE "id" = ?
[1]

Reply 1, to comment 
RELEASE CONNECTION

Como vemos, Pony hace que los queries SQL se puedan realizar de manera mucho más pythonica.

Este artículo cubre lo básico del trabajo con Pony ORM, no dejen de revisar su documentación.

 

Update (25.02.2015):

Hace unas horas Alexander Kozlovsky de PonyORM ha escrito al mailing list de Pony informado que están en las fases finales de prueba de Pony.js que pretende permitir realizar el mismo tipo de búsquedas desde el frontend, por ahora soporta knockout, pero pronto soportará AngularJS.

También mencionó que las migraciones son el siguiente punto en su agenda, y deberían estar disponibles para finales de agosto.