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.

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.