44.7. Subtransacciones explícitas #

44.7.1. Gestores de contexto de subtransacción

Recuperarse de errores causados por el acceso a la base de datos como se describe en Section 44.6.2 puede llevar a una situación no deseada donde algunas operaciones tienen éxito antes de que una de ellas falle, y después de recuperarse de ese error los datos quedan en un estado inconsistente. PL/Python ofrece una solución a este problema en forma de subtransacciones explícitas.

44.7.1. Gestores de contexto de subtransacción #

Considera una función que implementa una transferencia entre dos cuentas:

CREATE FUNCTION transfer_funds() RETURNS void AS $$
try:
    plpy.execute("UPDATE accounts SET balance = balance - 100 WHERE account_name = 'joe'")
    plpy.execute("UPDATE accounts SET balance = balance + 100 WHERE account_name = 'mary'")
except plpy.SPIError as e:
    result = "error transferring funds: %s" % e.args
else:
    result = "funds transferred correctly"
plan = plpy.prepare("INSERT INTO operations (result) VALUES ($1)", ["text"])
plpy.execute(plan, [result])
$$ LANGUAGE plpython3u;

Si la segunda sentencia UPDATE produce una excepción, esta función informará del error, pero el resultado de la primera UPDATE se confirmará (commit) a pesar de todo. En otras palabras, los fondos se retirarán de la cuenta de Joe, pero no se transferirán a la cuenta de Mary.

Para evitar tales problemas, puedes envolver tus llamadas a plpy.execute en una subtransacción explícita. El módulo plpy proporciona un objeto auxiliar para gestionar subtransacciones explícitas que se crea con la función plpy.subtransaction(). Los objetos creados por esta función implementan la interfaz de gestor de contexto. Usando subtransacciones explícitas podemos reescribir nuestra función como:

CREATE FUNCTION transfer_funds2() RETURNS void AS $$
try:
    with plpy.subtransaction():
        plpy.execute("UPDATE accounts SET balance = balance - 100 WHERE account_name = 'joe'")
        plpy.execute("UPDATE accounts SET balance = balance + 100 WHERE account_name = 'mary'")
except plpy.SPIError as e:
    result = "error transferring funds: %s" % e.args
else:
    result = "funds transferred correctly"
plan = plpy.prepare("INSERT INTO operations (result) VALUES ($1)", ["text"])
plpy.execute(plan, [result])
$$ LANGUAGE plpython3u;

Ten en cuenta que el uso de try/except sigue siendo necesario. De lo contrario, la excepción se propagaría a la parte superior de la pila de Python y causaría que toda la función aborte con un error de PostgreSQL, por lo que en la tabla operations no se insertaría ninguna fila. El gestor de contexto de subtransacción no captura errores, solo asegura que todas las operaciones de la base de datos ejecutadas dentro de su ámbito se confirmen (commit) o se reviertan (rollback) de forma atómica. La reversión del bloque de subtransacción ocurre ante cualquier tipo de salida por excepción, no solo las causadas por errores originados en el acceso a la base de datos. Una excepción común de Python lanzada dentro de un bloque de subtransacción explícito también causaría que la subtransacción se revierta.