Transacciones

Nota: Se recomienda encarecidamente a los desarrolladores que creen aplicaciones nuevas que usen la biblioteca de cliente de NDB, que ofrece varias ventajas en comparación con esta biblioteca de cliente, como el almacenamiento automático en caché de entidades mediante la API Memcache. Si actualmente usas la biblioteca de cliente de DB anterior, consulta 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 que es atómico: o se realizan todas las operaciones de la transacción o no se realiza ninguna. Una aplicación puede realizar varias operaciones y cálculos en una sola transacción.

Usar transacciones

Una transacción es un conjunto de operaciones de Datastore en una o varias entidades. Se garantiza que cada transacción es atómica, lo que significa que las transacciones nunca se aplican parcialmente. Se aplican todas las operaciones de la transacción o no se aplica ninguna. Las transacciones tienen una duración máxima de 60 segundos y un tiempo de caducidad por inactividad de 10 segundos después de 30 segundos.

Una operación puede fallar en los siguientes casos:

  • Se han intentado realizar demasiadas modificaciones simultáneas en el mismo grupo de entidades.
  • La transacción supera un límite de recursos.
  • Datastore detecta un error interno.

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

Las transacciones son una función opcional de Datastore. No es obligatorio usarlas para realizar operaciones de Datastore.

Una aplicación puede ejecutar un conjunto de instrucciones y operaciones de almacén de datos en una sola transacción, de forma que, si alguna instrucción u operación genera una excepción, no se aplique 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 de run_in_transaction, en función de si la transacción accede a entidades de un solo grupo de entidades o si es una transacción entre grupos.

En el caso habitual de una función que solo se usa en 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 la función se llama a veces sin una transacción, en lugar de decorarla, llama a db.run_in_transaction() con la función como 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 función y los argumentos posicionales y de palabras clave que se van a pasar a la función. Si la función devuelve un valor, db.run_in_transaction() devuelve ese valor.

Si la función devuelve un valor, 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 los efectos no se aplican. Consulta la nota anterior sobre las excepciones.

Cuando se llama a una función de transacción desde otra transacción, @db.transactional y db.run_in_transaction() tienen un comportamiento predeterminado diferente. @db.transactional permitirá esto y la transacción interna se convertirá en la misma transacción que la externa. Al llamar a db.run_in_transaction(), se intenta "anidar" otra transacción dentro de la transacción actual, pero este comportamiento aún no se admite y genera db.BadRequestError. Puedes especificar otro comportamiento. Consulta los detalles en la referencia de la función sobre las opciones de transacción.

Usar transacciones entre grupos

Las transacciones entre grupos, que operan en varios grupos de entidades, se comportan como las 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 transacción.

Usar @db.transactional:

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()

Usar db.run_in_transaction_options:

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 de una transacción deben operar en entidades del mismo grupo de entidades si la transacción es de un solo grupo, o en entidades de un máximo de 25 grupos de entidades si la transacción es entre grupos. Esto incluye consultar entidades por ancestro, obtener entidades por clave, actualizar entidades y eliminar entidades. Ten en cuenta que cada entidad raíz pertenece a un grupo de entidades independiente, por lo que una sola transacción no puede crear 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 simultáneamente entidades en uno o más grupos de entidades comunes, solo la primera transacción que confirme sus cambios puede completarse correctamente. El resto fallarán al confirmar. Debido a este diseño, el uso de grupos de entidades limita el número de escrituras simultáneas que puedes realizar en cualquier entidad de los grupos. Cuando se inicia una transacción, Datastore usa el control de concurrencia optimista comprobando la hora de la última actualización de los grupos de entidades usados en la transacción. Cuando se confirma una transacción de los grupos de entidades, Datastore vuelve a comprobar la hora de la última actualización de los grupos de entidades utilizados en la transacción. Si ha cambiado desde la comprobación inicial, se produce una excepción.

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

En una función de transacción se puede utilizar todo el código Python. Puedes determinar si el ámbito actual está anidado en una función de transacción mediante db.is_in_transaction(). La función de transacción no debe tener efectos secundarios que no sean operaciones de Datastore. La función de transacción se puede llamar varias veces si una operación de Datastore falla porque otro usuario actualiza entidades del grupo de entidades al mismo tiempo. Cuando esto ocurre, la API de Cloud Datastore vuelve a intentar la transacción un número fijo de veces. Si todas fallan, db.run_in_transaction() genera una TransactionFailedError. Puedes ajustar el número de veces que se reintenta la transacción con db.run_in_transaction_custom_retries() en lugar de db.run_in_transaction().

Del mismo modo, la función de 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 transacción sepa cómo deshacer esos efectos. Por ejemplo, si la transacción almacena una nueva entidad de Datastore, guarda el ID de la entidad creada para usarlo más adelante y, a continuación, la transacción falla, el ID guardado no hace referencia a la entidad prevista porque se ha revertido la creación de la entidad. El código de llamada tendría que tener cuidado de no usar el ID guardado en este caso.

Aislamiento y coherencia

Fuera de las transacciones, el nivel de aislamiento de Datastore es el más parecido al de lectura confirmada. Dentro de las transacciones, se aplica el aislamiento serializable. Esto significa que otra transacción no puede modificar simultáneamente los datos que lee o modifica esta transacción.

En una transacción, todas las lecturas reflejan el estado actual y coherente de Datastore en el momento en que se inició la transacción. Se garantiza que las consultas y las lecturas dentro de una transacción verán una única instantánea coherente de Datastore a partir del inicio de la transacción. Las entidades y las filas de índice del grupo de entidades de la transacción se actualizan por completo para que las consultas devuelvan el conjunto completo y correcto de entidades de resultados, sin los falsos positivos ni los falsos negativos que pueden producirse en las consultas fuera de las transacciones.

Esta vista de la captura coherente también se aplica 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 de obtención dentro de una transacción de Datastore no ven los resultados de las escrituras anteriores dentro de esa transacción. En concreto, si se modifica o elimina una entidad en una transacción, una consulta o una obtención devuelve la versión original de la entidad al principio de la transacción o nada si la entidad no existía entonces.

Usos de las transacciones

En este ejemplo se muestra un uso de las transacciones: actualizar una entidad con un nuevo valor de propiedad en relación con su valor actual.

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

Esto requiere una transacción porque otro usuario podría actualizar el valor después de que este código obtenga el objeto, pero antes de que guarde el objeto modificado. Si no hay ninguna transacción, la solicitud del usuario usará el valor de count anterior a la actualización del otro usuario y la acción de guardar sobrescribirá el nuevo valor. Con una transacción, la aplicación recibe información sobre la actualización del otro usuario. Si la entidad se actualiza durante la transacción, se vuelve a intentar la transacción hasta que se completen todos los pasos sin interrupciones.

Otro uso habitual de las transacciones es obtener 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, es necesaria una transacción para gestionar el caso en el que otro usuario intente crear o actualizar una entidad con el mismo ID de cadena. Sin una transacción, si la entidad no existe y dos usuarios intentan crearla, el segundo sobrescribe al primero sin saber que ha ocurrido. En una transacción, el segundo intento vuelve a probar, detecta que la entidad ya existe y la actualiza.

Cuando falla una transacción, puedes hacer que tu aplicación vuelva a intentarla hasta que se complete correctamente o puedes dejar que tus usuarios gestionen el error propagándolo al nivel de la interfaz de usuario de tu aplicación. No es necesario que crees un bucle de reintentos en torno a cada transacción.

Get-or-create es tan útil que tiene un método integrado: Model.get_or_insert() toma un nombre de clave, un elemento superior opcional y argumentos para pasar al constructor del modelo si no existe una entidad con ese nombre y esa ruta. El intento de obtener y la creación se producen en una sola transacción, por lo que (si la transacción se realiza correctamente) el método siempre devuelve 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 lecturas para renderizar una página o exportar datos que deben ser coherentes. Este tipo de transacciones se suelen denominar transacciones de solo lectura, ya que no realizan ninguna escritura. Las transacciones de un solo grupo de solo lectura nunca fallan debido a modificaciones simultáneas, por lo que no tienes que implementar reintentos en caso de fallo. Sin embargo, las transacciones entre grupos pueden fallar debido a modificaciones simultáneas, por lo que deberían tener reintentos. Confirmar y revertir una transacción de solo lectura son operaciones nulas.

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

Puesta en cola de tareas transaccionales

Puedes poner en cola una tarea como parte de una transacción de Datastore, de forma que la tarea solo se ponga en cola si la transacción se confirma correctamente. 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 puesta en cola, la tarea no se ejecutará inmediatamente, por lo que no será atómica con la transacción. Sin embargo, una vez en la cola, la tarea se irá probando hasta que logre ejecutarse. Esto se aplica a cualquier tarea puesta en cola durante una función run_in_transaction().

Las tareas transaccionales son útiles porque te permiten combinar acciones que no son de Datastore en una transacción que depende de que la transacción se realice correctamente (por ejemplo, enviar un correo para confirmar una compra). También puedes asociar acciones de Datastore a la transacción, como confirmar los cambios en grupos de entidades fuera de la transacción si y solo si la transacción se realiza correctamente.

Una aplicación no puede insertar más de cinco tareas transaccionales en colas de tareas 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, ....)