El problema:
Google App Engine mola, y la verdad es que programar en Python se me está empezando a hacer agradable, aunque me sigue gustando más la indentación con llaves…
En fin, la base de datos que usa Google es superpotente y supersencilla, si bien tiene algunas limitaciones con las que hay que aprender a vivir. Con el SDK que Google da, puedes indicar fácilmente qué atributos indexar (indexed=True), puedes forzar a que un atributo sea definido cuando vas a añadir un elemento a la base de datos (required=True), pero no puedes forzar a que un atributo o conjunto de atributos sean únicos (no hay un unique=True).
Por poner un ejemplo, es el típico campo de nick o e-mail en una base de datos de usuario SQL de toda la vida. No quieres que dos usuarios puedan tener el mismo e-mail o el mismo nick porque quieres identificar univocamente a un usuario por su nick o su e-mail. Es de cajón :p
¿Putada? El SDK de Google App Engine no te permite hacer esto… sin embargo la solución es trivial
La solución:
Lo único que hay que hacer es redefinir la función put dentro de nuestro modelo y lanzar una excepción cuando se cree un elemento cuyas restricciones no se cumplan.
Por simplificar, creemos un modelo User con los atributos name, email, language y creation_date:
class User (db.Model): creation_date = db.DateTimeProperty (auto_now_add=True) name = db.StringProperty (required = True) email = db.EmailProperty (required = True) language = db.StringProperty (default="en") |
Tal y como está definida la clase, si ejecutáramos el siguiente código, Google App Engine crearía dos registros distintos:
User (name="Mark Johnson", email="test@codigomanso.com").put() User (name="JohnMarkinson", email="test@codigomanso.com", language="es").put() |
No queremos dos usuarios compartiendo el mismo e-mail!! El segundo put debería fallar. ¿Cómo lo arreglamos? Tan simple como parece, redefinimos put para que cuando se vaya a crear un elemento con un atributo duplicado en la base de datos lance una excepción. Por lo tanto, la nueva clase una vez añadido el método put quedaría:
class User (db.Model): creation_date = db.DateTimeProperty (auto_now_add=True) name = db.StringProperty (required = True) email = db.EmailProperty (required = True) language = db.StringProperty (default="en") def put (self): # Make sure e-mails are unique for each user if (not self.is_saved()) and (User.gql ('WHERE email = :1', self.email).count() > 0): raise DuplicatedInstanceError ('User.email', self.email) # call the parent method db.Model.put (self) |
Si te das cuenta, son sólo 3 lineas de código, 4 contando la linea del def… si es que más fácil no se puede.
Problemas varios
Después de meditarlo un poco, esta implementación no es perfecta. Yo le veo principalmente dos inconvenientes:
- Hay que realizar un query extra cuando se pretende insertar un elemento (si actualizas un elemento la función se comporta como siempre).
- Aún con esta solución puede ocurrir que acabes con dos usuarios con el mismo nick en la base de datos. ¿Cómo puede ser? La operación count-put no es atómica, por lo tanto, pueden llegar dos usuarios al mismo tiempo, realizar un count, la función devolver cero en ambos casos, y luego se insertan los dos.
El primer inconveniente, no lo es tanto, pero es una cosa a tener en cuenta.
El segundo inconveniente puede ser una putada. Para solucionarlo se me ocurre que se podría tratar de ejecutar el count-put dentro de una transacción, o bien crear un mutex (se puede?) y que el count-put se realicen dentro del mútex. En fin, a mi ese caso de momento no me preocupa, pero está bien saber que existe.
Apéndice: DuplicatedInstanceError
Si eres avispado te habrás dado cuenta que he lanzado una excepción de tipo DuplicatedInstanceError. Esta clase no existe. La he definido yo para poder detectar cuando ocurre un caso de este tipo.
class DuplicatedInstanceError (Exception): def __init__(self, attrpath, value = None): ''' Accept a dict of attribute and value pairs, or just one attribute and a value: Example: # user unique constraint has been violated (User.nick to be unique) DuplicatedInstanceError ('User.nick', 'jsmith') # student unique constraint has been violated (student nick's are fin for different schools) DuplicatedInstanceError ({ 'Student.nick' : 'jsmith', 'Student.school' : 'demo-primary-school' }) ''' self.attrpath = '' # accept a dict as the only parameter if isinstance (attrpath, dict): for (k, v) in attrpath.items(): self.attrpath += ', ' + str (k) + ' = ' + str(v) self.attrpath = self.attrpath[2:] # accept a couple of strings else: self.attrpath = str(attrpath) + ' = ' + str(value) self.attrpath = '(' + self.attrpath + ')' return def __str__ (self): return str (self.attrpath) |
Ahora a traducir el texto a inglés… Ale, otro día más!


English