Transacciones

Nota: Se recomienda enfáticamente a los desarrolladores que compilan aplicaciones nuevas que usen la biblioteca cliente de NDB, ya que tiene muchas ventajas en comparación con esta biblioteca cliente, como el almacenamiento en caché automático de entidades mediante la API de Memcache. Si por el momento usas la biblioteca cliente de DB anterior, lee la Guía de migración de DB a NDB.

Datastore admite transacciones. Una transacción es una operación (o un conjunto de operaciones) atómica: se producen todas las operaciones de la transacción o no se produce ninguna. Una aplicación puede realizar varias operaciones y cálculos en una única transacción.

Usa transacciones

Una transacción es un conjunto de operaciones de Datastore en una o más entidades. Se garantiza que toda transacción es atómica, es decir, que nunca se aplica de forma parcial. Se aplican todas las operaciones de la transacción o ninguna de ellas. Las transacciones tienen una duración máxima de 60 segundos, con un tiempo de caducidad por inactividad de 10 segundos una vez transcurridos 30 segundos.

Las operaciones pueden fallar cuando se presenta alguna de las siguientes situaciones:

  • Se intentan aplicar demasiadas modificaciones simultáneas en el mismo grupo de entidades.
  • La transacción supera un límite de recursos.
  • Se produce un error interno en Datastore.

En todos estos casos, la API de Datastore muestra una excepción.

Las transacciones son una función opcional de Datastore; no es necesario que las uses para realizar operaciones de Datastore.

Una aplicación puede ejecutar un conjunto de instrucciones y operaciones de almacenamiento de datos en una sola transacción, de modo que, si alguna operación o instrucción genera una excepción, no se aplica ninguna de las operaciones de Datastore del conjunto. La aplicación define las acciones que se deben realizar en la transacción mediante una función de Python. La aplicación inicia la transacción mediante uno de los métodos run_in_transaction, según si la transacción accede a entidades dentro de un solo grupo de entidades o si es una transacción entre grupos.

Para el caso de uso común de una función que solo se usa dentro de transacciones, usa el decorador @db.transactional:

from google.appengine.ext import db

class Accumulator(db.Model):
    counter = db.IntegerProperty(default=0)

@db.transactional
def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

increment_counter(acc.key(), 5)

Si a veces se llama a la función sin una transacción, en lugar de decorarla, llama a db.run_in_transaction() con la función como un argumento:

from google.appengine.ext import db

class Accumulator(db.Model):
    counter = db.IntegerProperty(default=0)

def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

db.run_in_transaction(increment_counter, acc.key(), 5)

db.run_in_transaction() toma el objeto de la función y los argumentos posicionales y de palabras clave para pasarlos a la función. Si la función muestra un valor, db.run_in_transaction() muestra ese valor.

Si la función se muestra, la transacción se confirma, y se aplican todos los efectos de las operaciones de Datastore. Si la función genera una excepción, la transacción se “revierte” y no se aplican los efectos. Consulta la nota anterior sobre excepciones.

Cuando se llama a la función de una transacción desde otra transacción, @db.transactional y db.run_in_transaction() tienen comportamientos predeterminados diferentes. @db.transactional permitirá esto, y la transacción interna se convertirá en la misma transacción que la transacción externa. Cuando se llama a db.run_in_transaction(), se intenta “anidar” otra transacción dentro de la transacción existente, pero este comportamiento aún no se admite y genera un error db.BadRequestError. Puedes especificar otro comportamiento. Consulta la referencia de las funciones sobre las opciones de transacciones para obtener más información.

Uso de transacciones entre grupos (XG)

Las transacciones entre grupos, que operan entre varios grupos de entidades, se comportan como transacciones de un solo grupo, pero no fallan si el código intenta actualizar entidades de más de un grupo de entidades. Para invocar una transacción entre grupos, usa las opciones de transacciones.

Usa @db.transactional de la siguiente manera:

from google.appengine.ext import db

@db.transactional(xg=True)
def make_things():
  thing1 = Thing(a=3)
  thing1.put()
  thing2 = Thing(a=7)
  thing2.put()

make_things()

Usa db.run_in_transaction_options de la siguiente manera:

from google.appengine.ext import db

xg_on = db.create_transaction_options(xg=True)

def my_txn():
    x = MyModel(a=3)
    x.put()
    y = MyModel(a=7)
    y.put()

db.run_in_transaction_options(xg_on, my_txn)

¿Qué se puede hacer en una transacción?

Datastore impone restricciones sobre lo que se puede hacer dentro de una sola transacción.

Todas las operaciones de Datastore en una transacción deben operar en entidades dentro del mismo grupo de entidades si la transacción es de un solo grupo, o en entidades en un máximo de veinticinco grupos de entidades si se trata de una transacción entre grupos. Esto incluye las consultas de entidades por entidad principal, la recuperación de entidades por clave y la actualización y eliminación de entidades. Ten en cuenta que cada entidad raíz pertenece a un grupo de entidades separado, de modo que una única transacción no puede crearse ni operar en más de una entidad raíz, a menos que sea una transacción entre grupos.

Cuando dos o más transacciones intentan modificar entidades de forma simultánea en uno o más grupos de entidades comunes, solo la primera transacción que confirme los cambios se realizará correctamente, y la confirmación de las demás fallará. Debido a este diseño, el uso de grupos de entidades limita la cantidad de escrituras simultáneas que puedes realizar en cualquier entidad de los grupos. Cuando se inicia una transacción, Datastore usa el control de simultaneidad optimista mediante la verificación de la última hora de actualización de los grupos de entidades que se usan en la transacción. Luego de confirmar una transacción en los grupos de entidades, Datastore verifica una vez más la última hora de actualización de los grupos de entidades que se usan en la transacción. Si cambió desde la revisión inicial, se genera una excepción.

Una aplicación puede realizar una consulta durante una transacción, pero solo si incluye un filtro de principales. Una aplicación también puede obtener entidades de Datastore por clave durante una transacción. Puedes preparar las claves antes de la transacción o puedes compilar claves dentro de la transacción con los nombres de clave o ID.

Dentro de la función de una transacción se permite el uso del resto del código de Python. Puedes determinar si el permiso actual está anidado en la función de una transacción mediante db.is_in_transaction(). La función de la transacción no debe tener efectos secundarios además de las operaciones de Datastore. Se puede llamar a la función de la transacción varias veces si falla una operación de Datastore debido a que otro usuario actualiza entidades en el grupo de entidades al mismo tiempo. Cuando esto sucede, la API de Datastore vuelve a intentar la transacción una cantidad fija de veces. Si todos los intentos fallan, db.run_in_transaction() genera un TransactionFailedError. Puedes ajustar la cantidad de veces que se reintenta la transacción mediante db.run_in_transaction_custom_retries() en lugar de db.run_in_transaction().

De manera similar, la función de la transacción no debe tener efectos secundarios que dependan del éxito de la transacción, a menos que el código que llama a la función de la transacción sepa cómo deshacer esos efectos. Por ejemplo, si la transacción almacena una entidad de Datastore nueva, guarda el ID de la entidad creada para su uso posterior y, luego, la transacción falla, el ID guardado no hace referencia a la entidad deseada porque su creación se revirtió. Debería verificarse que en el código de la llamada no se use el ID guardado en ese caso.

Aislamiento y coherencia

Fuera de las transacciones, el nivel de aislamiento de Datastore es el más cercano al de las lecturas confirmadas. Dentro de las transacciones, se aplica el aislamiento serializable. Esto significa que una transacción no puede modificar de forma simultánea los datos que lee o modifica otra transacción.

En una transacción, todas las operaciones de lectura reflejan el estado coherente actual de Datastore en el momento en que comenzó la transacción. Se garantiza que las consultas y las operaciones GET dentro de una transacción vean una instantánea única y coherente de Datastore desde el comienzo de la transacción. Las filas de las entidades y de los índices en el grupo de entidades de la transacción se actualizan por completo para que las consultas muestren el conjunto completo y correcto de las entidades del resultado, sin los falsos positivos ni falsos negativos que pueden generarse en las consultas realizadas fuera de las transacciones.

La vista de esta instantánea coherente también se extiende a las lecturas posteriores a las escrituras dentro de las transacciones. A diferencia de la mayoría de las bases de datos, las consultas y las operaciones GET en una transacción de Datastore no ven los resultados de las operaciones de escritura anteriores dentro de esa transacción. En concreto, si se modifica o borra una entidad dentro de una transacción, una operación GET o una consulta mostrará la versión original de la entidad según su estado al comienzo de la transacción, o no mostrará nada si la entidad no existía en ese momento.

Usos de las transacciones

En este ejemplo, se muestra un uso de las transacciones: la actualización de una entidad con un valor de propiedad nuevo relativo a su valor actual.

def increment_counter(key, amount):
    obj = db.get(key)
    obj.counter += amount
    obj.put()

Esta técnica requiere una transacción porque otro usuario puede actualizar el valor luego de que este código recupere el objeto, pero guarda el objeto modificado antes. Sin una transacción, la solicitud del usuario usa el valor que tenía count antes de la actualización del otro usuario, y el valor guardado reemplaza el valor nuevo. Mediante una transacción, se le notifica a la aplicación que otro usuario realizó una actualización. Si se actualiza la entidad durante la transacción, se realizan los reintentos necesarios de la transacción hasta que todos los pasos se hayan completado sin interrupción.

Las transacciones también se suelen usar para recuperar una entidad con una clave con nombre o crearla si aún no existe:

class SalesAccount(db.Model):
    address = db.PostalAddressProperty()
    phone_number = db.PhoneNumberProperty()

def get_or_create(parent_key, account_id, address, phone_number):
    obj = db.get(db.Key.from_path("SalesAccount", account_id, parent=parent_key))
    if not obj:
        obj = SalesAccount(key_name=account_id,
                           parent=parent_key,
                           address=address,
                           phone_number=phone_number)
        obj.put()
    else:
        obj.address = address
        obj.phone_number = phone_number

Al igual que antes, se necesita una transacción para resolver el caso en el que otro usuario intenta crear o actualizar una entidad con el mismo ID de string. Sin una transacción, si la entidad no existe y dos usuarios intentan crearla, el segundo reemplazará los datos del primero sin saberlo. Mediante una transacción, se reintenta la segunda creación, se descubre que la entidad ahora existe y se actualiza esa entidad.

Cuando falla una transacción, puedes hacer que tu aplicación la reintente hasta que se complete, o bien dejar que los usuarios se ocupen del error. Para ello, deberás propagarlo al nivel de la interfaz de usuario de la aplicación. No es necesario crear un bucle de reintento en cada transacción.

El método Get-or-create es tan útil que existe un método integrado para él, Model.get_or_insert(), que usa un nombre de clave, una entidad superior opcional y argumentos que se pasarán al constructor del modelo si no existe una entidad con ese nombre y ruta de acceso. El intento de la operaciones get y create sucede en una transacción, de modo que (si la transacción se completa) el método siempre muestra una instancia de modelo que representa una entidad real.

Por último, puedes usar una transacción para leer una instantánea coherente de Datastore. Esto puede ser útil cuando se necesitan varias operaciones lecturas para renderizar una página o exportar datos que deben ser coherentes. Estos tipos de transacciones suelen llamarse de solo lectura, ya que no realizan operaciones de escritura. Las transacciones de solo lectura que se realizan en un solo grupo no fallan nunca debido a modificaciones simultáneas, por lo que no debes implementar reintentos en caso de fallas. Sin embargo, las transacciones que se realizan entre grupos pueden fallar debido a modificaciones simultáneas, por lo que deben incluir reintentos. Tanto confirmar como revertir transacciones de solo lectura constituyen operaciones no-ops.

class Customer(db.Model):
    user = db.StringProperty()

class Account(db.Model):
    """An Account has a Customer as its parent."""
    address = db.PostalAddressProperty()
    balance = db.FloatProperty()

def get_all_accounts():
    """Returns a consistent view of the current user's accounts."""
    accounts = []
    for customer in Customer.all().filter('user =', users.get_current_user().user_id()):
        accounts.extend(Account.all().ancestor(customer))
    return accounts

Pon tareas transaccionales en cola

Puedes poner una tarea en cola como parte de una transacción de Datastore para que la tarea solo esté en cola si la transacción se confirma de forma correcta. Así, si la transacción no se confirma, la tarea no se pone en cola. Si la transacción se confirma, la tarea se pone en cola. Una vez en cola, la tarea no se ejecutará de inmediato; por lo tanto, la tarea no es atómica con la transacción. Sin embargo, una vez en cola, la tarea se reintentará hasta completarse. Esto se aplica a cualquier tarea en cola durante una función run_in_transaction().

Las tareas transaccionales son útiles porque te permiten combinar acciones que no son de Datastore con una transacción que depende de su realización con éxito (por ejemplo, enviar un correo electrónico para confirmar una compra). También puedes vincular acciones de Datastore a la transacción, por ejemplo, para confirmar los cambios en los grupos de entidades fuera de la transacción, siempre y cuando la transacción tenga éxito.

Una aplicación no puede insertar más de cinco tareas transaccionales en las listas de tareas en cola durante una sola transacción. Las tareas transaccionales no deben tener nombres especificados por el usuario.

def do_something_in_transaction(...)
    taskqueue.add(url='/path/to/my/worker', transactional=True)
  ...

db.run_in_transaction(do_something_in_transaction, ....)