Usando transacciones en bases de datos

Al escribir la lógica de una aplicación que interactúa con una base de datos relacional para almacenar la información, generalmente se puede tener varios pasos para guardar cada pedazo en un lugar específico de la base, las bases de datos denominadas ACID tienen una herramienta poderoza para evitar inconsistencias de datos hoy nos concentraremos en la A de ACID Atomicidad.

Por ejemplo cuando se crea una factura, no solo se guarda como un documento en la base, se tiene varias tablas como: factura, detalle_factura, inventario, producto, etc. Al enviar la información desde la interfaz de usuario hacia la aplicación podemos tener una sola estructura con todos los datos necesarios para crear la factura, pero a nivel de aplicación debemos analizar esa estructura y leerla para que cada parte se guarde en una tabla específica de la base de datos, de tal forma que tendremos un registro para factura con los datos de la cabecera, varios registros de detalle_factura y adicionalmente se debe modificar las cantidades de los productos en el detalle del inventario de cada producto.

Si tomamos la vía rápida de hacerlo todo esperando que nada falle, tendremos código cómo:

<?php

// ... solo el código que interesa para el ejemplo
$factura = new Factura($datosDeFactura);
$factura->save();
foreach ($detalleDeFactura as $detalle) {
    $detalle = new DetalleFactura($detalle);
    $detalle->factura = $factura;
    $detalle->producto = Producto::findOrFail($detalle['producto_id']);
    $detalle->save();

    $inventario = new Inventario($detalle);
    $inventario->producto = Producto::findOrFail($detalle['producto_id']);
    $inventario->save();
}

Como podemos ver estamos usando algún tipo de ORM en el que creamos los objetos con datos y guardamos cada uno, el problema sucede si digamos en la estructura de datos enviada, uno de los producto_id no está en la tabla de productos. De tal forma que si enviamos una lista con 3 productos y el último id no está definido, en este código se creará una factura con 2 detalles y 2 afectaciones a inventario, en el último producto el sistema dará un fallo y no podrá seguir, pero en la base de datos ya se escribió parte de la información.

Para solucionar este gran inconveniente debemos usar una herramienta que todas las bases de datos relacionales tienen, TRANSACCIONES.

A nivel de base de datos desde un cliente por ejemplo, es fácil comenzar una transacción con el comando:

BEGIN;

Luego se puede hacer varias operaciones de lectura y/o escritura:

INSERT INTO `factura` (numero, fecha, estado) 
VALUES ('001-001-1000', '2021-05-27', 'Facturada');

INSERT INTO `detalle_factura` (item, producto_id, cantidad, precio) 
VALUES ('1', 2000, 1, 500);

Al final se puede decidir si guardo todos los cambios o regreso al estado original antes de enviar el comando BEGIN. Si deseo escribir uso el comando COMMIT, si quiero descartar todos los cambios uso el comando ROLLBACK.

COMMIT;
--- o 
ROLLBACK;

Esto nos asegura que en el COMMIT todas las operaciones realizadas se ejecutan como una sola operación o no se ejecuta nada, a esto se le llama ATOMICIDAD.

En nuestro ejemplo de php, se puede implementar de varias formas, pero la que normalmente uso es hacer un try catch.

<?php

// ... solo el código que interesa para el ejemplo

try {
    $conexionBd->begin();

    $factura = new Factura($datosDeFactura);
    $factura->save();
    foreach ($detalleDeFactura as $detalle) {
        $detalle = new DetalleFactura($detalle);
        $detalle->factura = $factura;
        $detalle->producto = Producto::findOrFail($detalle['producto_id']);
        $detalle->save();

        $inventario = new Inventario($detalle);
        $inventario->producto = Producto::findOrFail($detalle['producto_id']);
        $inventario->save();
    }

    $conexionBd->commit();
} 
catch (\Exception $e)
{
    $conexionBd->rollback();
    throw $e;
}

Agregando estas líneas de código estamos asegurando que la factura se crea con los 3 productos enviados, o no se crea la factura, y la integridad de los datos está asegurada sin datos basura en la base.