Contenido
Esto es para aplicaciones que ejecutan Django con PSQL u otras bases de datos (DB) basadas en SQL (SQLite, MySQL, etc.) similares; las reglas subyacentes en torno a las transacciones pueden no aplicarse a otras DB. En esencia, el código de administración de transacciones de Django hace lo siguiente (esto se copió de la documentación de Django -desplácese hacia abajo- con mis propios puntos para mayor claridad):
Obtiene o crea una conexión a la base de datos y la establece como una transacción.
Todas las operaciones de bases de datos requieren bloqueos: algunos bloqueos son flexibles y permiten que otras conexiones ejecuten operaciones en el mismo recurso, pero otros bloqueos son más estrictos; son exclusivos y bloquearán tanto las operaciones de lectura como las de escritura. La creación, actualización y eliminación de registros, así como la modificación de tablas y columnas, adquirirán bloqueos más estrictos que impedirán que otras conexiones ejecuten operaciones. Estos bloqueos se mantienen hasta el FINAL de la transacción.
Esto se hace automáticamente cuando, por ejemplo, se insertan registros en una tabla: algunas operaciones del modelo se envolverán automáticamente en bloques atómicos.
Esto se puede hacer manualmente llamando a una función que esté envuelta por atomic() o usando atomic() en una declaración with (como un administrador de contexto).
Los puntos de guardado utilizarán la misma conexión que la transacción externa: si observa el código de Django para puntos de guardado, es solo una llamada recursiva para crear otra transacción.
Los puntos de guardado se pueden anidar dentro de otros puntos de guardado: una pila realiza un seguimiento de los puntos de guardado.
Después de la inserción, el punto de guardado se elimina confirmando los cambios y haciéndolos disponibles para el resto de la transacción (pero NO para otras conexiones) o revirtiendo los cambios.
Los bloqueos en la base de datos se mantienen mientras se ejecuta la lógica que no es de la base de datos, por ejemplo, si enviamos una solicitud HTTP en el cuerpo de una transacción, los bloqueos se mantendrán hasta que se envíe esa solicitud y se devuelva la respuesta. Django no libera los bloqueos cuando dejamos de ejecutar operaciones de la base de datos. Mantiene los bloqueos HASTA que finalice la transacción externa.
Los bloqueos adquiridos en transacciones anidadas también se mantienen hasta que finaliza la transacción externa o se revierte la transacción anidada.
La transacción se detiene, se revierte y se liberan los bloqueos.
Si el error se produce en una transacción anidada, la transacción externa no se ve afectada. Se produce la reversión, pero la transacción externa puede continuar sin interrupciones (siempre que se detecte y se gestione el error generado).
Las transacciones no detectan errores, pero dependen de ellos para saber cuándo revertir los cambios. Por lo tanto, no envuelva las operaciones de la base de datos en un try/except, en su lugar, envuélvalas en una transacción y luego envuelva la transacción en un try/except. Al hacerlo, la operación de la base de datos falla y se revierte automáticamente sin afectar la transacción externa.
Finalmente, se confirma toda la transacción, lo que hace que los cambios estén disponibles para todos los usuarios y conexiones de la base de datos. O bien, se revierte y la base de datos queda en el mismo estado en el que se encontraba cuando se encontró la transacción.
En el próximo blog, escribiré sobre lo que debes o no debes poner en una transacción para evitar interrupciones causadas por la falta de bloqueo.
Las transacciones son mecanismos que permiten que las bases de datos ACID (PSQL, SQL, la última versión de MongoDB , etc.) sean ATÓMICAS (la A de ACID). Esto significa que todas las escrituras en la base de datos se realizan como una sola operación, sin importar cuán complejas sean. Si la escritura se realiza correctamente, todos los cambios en la base de datos persisten y están disponibles para todas las conexiones simultáneamente (no hay escrituras parciales). Si la escritura falla, todos los cambios se revierten; nuevamente, no hay cambios parciales.
Las transacciones garantizan estas reglas:
Estas reglas permiten al usuario de la BD agrupar operaciones complejas y ejecutarlas como una sola operación. Por ejemplo, ejecutar una transferencia de una cuenta bancaria a otra; si no tuviéramos transacciones, entonces en medio de estas dos operaciones de escritura, podría haber un retiro o incluso una operación de cierre de cuenta que invalide la transferencia. Las transacciones permiten al usuario algún tipo de control de tráfico. Podemos bloquear todas las demás operaciones conflictivas mientras la transacción está en curso.
Las operaciones se bloquean mediante una serie de bloqueos en tablas y filas. En PSQL y otras variantes de SQL, las transacciones se crean con BEGIN; luego, los bloqueos se adquieren cuando se ejecuta una operación como select/insert/delete/alter y las transacciones finalizan con COMMIT o ROLLBACK. Los bloqueos se liberan cuando se ejecuta COMMIT o ROLLBACK. Afortunadamente, Django nos permite crear transacciones sin tener que usar estas tres sentencias (pero aún debemos preocuparnos por los bloqueos; más sobre eso en la próxima publicación).
-- Start a new transaction BEGIN; SELECT … INSERT INTO … UPDATE … DELETE FROM … -- Save the changes COMMIT;
from django.db import transaction from app.core.models import Accounts, Fee, NotificationJob def do_migration(): overdrawn_accounts = Accounts.objects.filter(type='overdrawn') for acount in overdrawn_accounts: create_notification(acount) @transaction.atomic def create_notification(acount: Accounts): # $5 fee for overdraft - 500 because we never store money as float!!! recall = Fee.objects.create(acount=acount, description='Fee for overdraft', amount=500) NotificationJob.objects.create(recall=recall, notification_type='all') acount.status = 'awaiting_payment' acount.save() def do_migration2(): overdawn_account = Accounts.objects.filter(type='overdrawn') for account in overdawn_account: with transaction.atomic(): recall = Fee.objects.create(acount=account, description='Fee for overdraft', amount=500) NotificationJob.objects.create(recall=recall, notification_type='all') account.status = 'awaiting_payment' account.save()
Django envuelve automáticamente cada operación de la base de datos en una transacción cuando se ejecuta en modo de confirmación automática (más sobre esto a continuación). Se pueden crear transacciones explícitas utilizando el decorador atomic() o el administrador de contexto (con atomic()): cuando se utiliza atomic(), las operaciones individuales de la base de datos NO se envuelven en una transacción ni se confirman inmediatamente en la base de datos (se ejecutan, pero los cambios en la base de datos no son visibles para otros usuarios). En cambio, la función completa se envuelve en un bloque de transacción y se confirma al final (si no se generan errores): se ejecuta COMMIT.
Si hay errores, los cambios en la base de datos se revierten: se ejecuta ROLLBACK. Por eso las transacciones conservan los bloqueos hasta el final; no queremos liberar un bloqueo en una tabla, que otra conexión cambie la tabla o la lea y luego verse obligados a revertir lo que se acaba de cambiar o leer.
ANGRY_CUSTOMER_THRESHOLD = 3 @transaction.atomic def create_notification(acount: Accounts): recall = Fee.objects.create(acount=acount, description='Fee for overdraft', amount=500) NotificationJob.objects.create(recall=recall, notification_type='all') try: # when this completes successfully, the changes will be available to the outer transaction with transaction.atomic(): owner = acount.owner fees = Fee.objects.filter(owner=owner).count() if fees >= ANGRY_CUSTOMER_THRESHOLD: for fee in fees: fee.amount = 0 fee.save() owner.status = 'angry' owner.save() # as long as we catch the error, the outer transaction will not be rolled back except Exception as e: logger.error(f'Error while removings fees for account {acount.id}: {e}') acount.status = 'awaiting_payment' acount.save()
También podemos anidar transacciones llamando a otra función que esté envuelta por atomic() o usando el administrador de contexto dentro de una transacción. Esto nos permite crear puntos de guardado donde podemos intentar operaciones riesgosas sin afectar el resto de la transacción: cuando se detecta una transacción interna, se crea un punto de guardado, se ejecuta la transacción interna y, si la transacción interna falla, los datos se revierten al punto de guardado y la transacción externa continúa. Si la transacción externa falla, todas las transacciones internas se revierten junto con la transacción externa. La anidación de transacciones se puede evitar configurando durable=True en atomic(); esto hará que la transacción genere un RuntimeError si se detecta alguna transacción interna.
Lo que debes recordar sobre las transacciones anidadas:
DATABASES = { 'default': { # True by default 'AUTOCOMMIT': False, # ...rest of configs } }
De forma predeterminada, Django se ejecuta en modo de confirmación automática , lo que significa que cada operación de la base de datos se envuelve en una transacción y se ejecuta inmediatamente (se confirma, por lo tanto, se confirma automáticamente), excepto si el usuario envuelve explícitamente la operación en una transacción. Esto ofusca la lógica de transacción de la base de datos subyacente: algunas bases de datos, como PSQL, envuelven automáticamente todas las operaciones en transacciones, mientras que otras no lo hacen. Si dejamos que Django haga esto por nosotros, no tenemos que preocuparnos por la lógica subyacente de la base de datos.
Podemos desactivar el modo de confirmación automática, pero eso nos dejaría dependiendo de la base de datos para gestionar las operaciones y garantizar que sean ATÓMICAS, lo que es riesgoso e inconveniente para los desarrolladores. No sería divertido si, al crear una API, tuviéramos que averiguar qué operaciones de escritura están en las transacciones y, por lo tanto, se escribirán en su totalidad y no parcialmente.
Sin embargo, es posible que queramos desactivar el modo de confirmación automática si conocemos la cantidad de conexiones en la base de datos y la secuencia de operaciones que están ejecutando. Si descubrimos cómo eliminar los conflictos entre estas operaciones, podemos desactivar el modo de confirmación automática y ganar en eficiencia:
Las desventajas son los mayores riesgos de corrupción de datos. No vale la pena, pero tal vez exista un caso de uso que no se me ocurre.