Insertar un pedido y sus partidas de forma segura

Situación

Un pedido y sus partidas representan una sola operación. Una transacción permite confirmar todos sus cambios o revertirlos como una unidad. Si el encabezado se guarda pero una partida falla, no queremos dejar información incompleta.

Insertaremos el pedido, recuperaremos su identidad y utilizaremos la misma transacción para agregar dos productos.

Datos relevantes

Dato Valor solicitado
Cliente Bruno Díaz, identificador 2
Partida 1 Producto 1, cantidad 2
Partida 2 Producto 2, cantidad 1
Estado inicial Pendiente

La operación debe confirmar tres filas relacionadas: un encabezado y dos partidas. Si falta el cliente, un producto está inactivo o una restricción —una regla que la tabla hace cumplir— rechaza alguna partida, ninguna de las tres debe permanecer.

La interacción con el usuario ya terminó antes de abrir la transacción. Dentro de ella hacemos solamente el trabajo necesario para validar, insertar y confirmar.

Construcciones que participan

SCOPE_IDENTITY() devuelve el último valor IDENTITY generado dentro del mismo alcance. IDENTITY produce automáticamente un número para cada fila nueva. En este caso, el alcance es el lote que inserta el encabezado; convertir el resultado a BIGINT lo alinea con el tipo de IdPedido.

SYSDATETIME() obtiene la fecha y hora actuales del servidor. @@ROWCOUNT conserva el número de filas afectadas por la instrucción inmediatamente anterior. XACT_STATE() informa si existe una transacción y si todavía puede confirmarse: 1 permite confirmar, 0 indica que no hay una activa y -1 obliga a revertirla.

Consulta final

DECLARE @IdCliente INT = 2;
DECLARE @IdPedido BIGINT;
DECLARE @ConfirmarCambios BIT = 0;

SET XACT_ABORT ON;

BEGIN TRY
    BEGIN TRANSACTION;

    IF NOT EXISTS
    (
        SELECT 1
        FROM dbo.Cliente
        WHERE IdCliente = @IdCliente
          AND Activo = 1
    )
        THROW 50001, 'El cliente no existe o está inactivo.', 1;

    INSERT INTO dbo.Pedido
    (
        IdCliente,
        FechaPedido,
        Estado
    )
    VALUES
    (
        @IdCliente,
        SYSDATETIME(),
        'Pendiente'
    );

    SET @IdPedido = CONVERT(BIGINT, SCOPE_IDENTITY());

    INSERT INTO dbo.PedidoDetalle
    (
        IdPedido,
        NumeroPartida,
        IdProducto,
        Cantidad,
        PrecioUnitario,
        Descuento
    )
    SELECT
        @IdPedido,
        v.NumeroPartida,
        p.IdProducto,
        v.Cantidad,
        p.Precio,
        0
    FROM
    (
        VALUES
            (CONVERT(SMALLINT, 1), 1, 2),
            (CONVERT(SMALLINT, 2), 2, 1)
    ) AS v (NumeroPartida, IdProducto, Cantidad)
    INNER JOIN dbo.Producto AS p
        ON p.IdProducto = v.IdProducto
       AND p.Activo = 1;

    IF @@ROWCOUNT <> 2
        THROW 50002, 'Falta un producto válido para completar el pedido.', 1;

    IF @ConfirmarCambios = 1
    BEGIN
        COMMIT TRANSACTION;
        SELECT @IdPedido AS IdPedidoCreado, N'Confirmado' AS Resultado;
    END
    ELSE
    BEGIN
        SELECT
            @IdPedido AS IdPedidoEnsayado,
            COUNT(*) AS PartidasEnsayadas
        FROM dbo.PedidoDetalle
        WHERE IdPedido = @IdPedido;

        ROLLBACK TRANSACTION;
    END;
END TRY
BEGIN CATCH
    IF XACT_STATE() <> 0
        ROLLBACK TRANSACTION;

    THROW;
END CATCH;

Recorrido

La validación del cliente ocurre dentro de la transacción. Después del primer INSERT, recuperamos la identidad generada y la usamos para relacionar las partidas.

La tabla construida con VALUES representa las partidas solicitadas. El INNER JOIN obtiene el precio actual y excluye productos inexistentes o inactivos. @@ROWCOUNT se guarda conceptualmente justo después del INSERT; si no se insertaron las dos filas esperadas, THROW transfiere el control al CATCH.

XACT_ABORT ON hace que determinados errores de ejecución aborten la transacción completa. El bloque mantiene además TRY...CATCH para revertir cualquier transacción que siga activa y volver a lanzar el error mediante THROW.

El ejemplo comienza con @ConfirmarCambios = 0: ejecuta y comprueba la operación, pero termina con ROLLBACK. Solo después de revisar el resultado se cambia conscientemente a 1; entonces COMMIT confirma el encabezado y las partidas.

Resultado esperado

En modo de ensayo, la consulta devuelve una identidad y el número de partidas localizadas dentro de la transacción, por ejemplo:

IdPedidoEnsayado PartidasEnsayadas
6 2

Después del ROLLBACK, esa identidad no aparece en Pedido ni en PedidoDetalle. El contador IDENTITY puede conservar un hueco aunque la transacción se revierta; una identidad identifica filas, pero no garantiza una secuencia sin saltos.

Comprobación en SSMS

La prueba se ejecutó con ROLLBACK: dentro de la transacción se creó un identificador y se localizaron dos partidas, pero ninguna fila quedó guardada.

Resultado real en SSMS del ensayo reversible de inserción

Variación: obtener la identidad con OUTPUT

OUTPUT inserted.IdPedido puede capturar identidades de varias filas. Para una sola inserción, SCOPE_IDENTITY() conserva una lectura sencilla; no utilizamos @@IDENTITY, porque puede devolver una identidad generada por un disparador.

Límite o precaución

El ejemplo todavía no descuenta existencia. Si el pedido debe reservar inventario, la validación y el movimiento pertenecen a la misma transacción y requieren una estrategia de concurrencia, es decir, reglas para coordinar ejecuciones simultáneas sobre los mismos datos.

Una transacción no sustituye reglas de unicidad ni validaciones del esquema. También debe mantenerse breve: esperar interacción del usuario mientras permanece abierta aumenta bloqueos y riesgo de contención, cuando varias sesiones esperan por los mismos recursos.

La comprobación de existencia y el uso posterior del cliente o los productos pueden competir con otras sesiones. En una operación empresarial se define también qué cambios concurrentes deben permitirse y qué restricciones del esquema sostienen la integridad.

Relacionado con

Índice del recorrido

Casos aplicados

10 artículos de Casos aplicados
  1. 01Consultar clientes con sus pedidos confirmados
  2. 02Encontrar clientes que todavía no tienen pedidos
  3. 03Mostrar el primer teléfono disponible de cada cliente
  4. 04Obtener ventas totales por cliente y filtrar las más altas
  5. 05Insertar un pedido y sus partidas de forma segura
  6. 06Actualizar precios de una categoría con validación previa
  7. 07Combinar pedidos actuales e históricos
  8. 08Obtener el último pedido de cada cliente
  9. 09Calcular venta diaria y total acumulado
  10. 10Construir una búsqueda paginada con filtros opcionales