Módulo 2 · SQL Server / Procedures — Capítulo 14

Projeto Prático Final

Fechando o módulo com um sistema pequeno e completo — Pedidos de um E-commerce simplificado — usando cada técnica vista nos 13 capítulos anteriores.

1. O sistema: Pedidos

Vamos montar o núcleo de um módulo de pedidos, reaproveitando as tabelas Cliente, Pedido e PedidoItem já usadas ao longo do módulo, e amarrando tudo com uma única procedure de checkout que usa transação, TVP, tratamento de erro e retorno estruturado — o "produto final" de tudo que este módulo cobriu.

CapítuloTécnica aplicada no projeto
02–04CRUD completo de Cliente e Produto, com RETURN/OUTPUT
05–06Transação + TRY/CATCH no checkout do pedido
07Tabela temporária para validar estoque antes de confirmar
08TVP para receber a lista de itens do carrinho
10SQL dinâmico na listagem de pedidos com ordenação configurável
11Trigger de auditoria quando o status do pedido muda
12Nomenclatura, cabeçalho de documentação e CREATE OR ALTER
13Chamada final via C# com Dapper

2. Modelo de dados

SQL tabelas do projeto
CREATE TABLE dbo.Produto
(
    Id       INT IDENTITY(1,1) PRIMARY KEY,
    Nome     NVARCHAR(150) NOT NULL,
    Preco    DECIMAL(10,2) NOT NULL,
    Estoque  INT NOT NULL DEFAULT 0
);

CREATE TABLE dbo.Pedido
(
    Id          INT IDENTITY(1,1) PRIMARY KEY,
    ClienteId   INT NOT NULL REFERENCES dbo.Cliente(Id),
    Status      NVARCHAR(20) NOT NULL DEFAULT 'Pendente',
    ValorTotal  DECIMAL(12,2) NOT NULL DEFAULT 0,
    DataPedido  DATETIME2 NOT NULL DEFAULT SYSDATETIME()
);

CREATE TABLE dbo.PedidoItem
(
    Id         INT IDENTITY(1,1) PRIMARY KEY,
    PedidoId   INT NOT NULL REFERENCES dbo.Pedido(Id),
    ProdutoId  INT NOT NULL REFERENCES dbo.Produto(Id),
    Quantidade INT NOT NULL,
    PrecoUnit  DECIMAL(10,2) NOT NULL
);

CREATE TYPE dbo.ItemCarrinhoTableType AS TABLE
(
    ProdutoId  INT NOT NULL,
    Quantidade INT NOT NULL
);
GO

3. A procedure central: checkout

Esta é a procedure que sintetiza o módulo inteiro: recebe o carrinho como TVP, valida estoque com uma tabela temporária, roda tudo dentro de uma transação com XACT_ABORT, e devolve o Id do pedido criado via OUTPUT.

SQL usp_PedidoCheckout
-- =============================================
-- Procedure:  usp_PedidoCheckout
-- Descrição:  Cria um pedido a partir de um carrinho, validando
--             estoque e debitando os produtos, tudo atomicamente.
-- Parâmetros: @ClienteId - cliente que está finalizando a compra
--             @Itens     - carrinho (ProdutoId, Quantidade)
--             @PedidoId  - OUTPUT com o Id do pedido criado
-- Retorno:    0 = sucesso; lança erro se algum item não tiver estoque
-- Autor:      Wellington Marunaka
-- =============================================
CREATE OR ALTER PROCEDURE dbo.usp_PedidoCheckout
    @ClienteId INT,
    @Itens     dbo.ItemCarrinhoTableType READONLY,
    @PedidoId  INT OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    BEGIN TRY
        -- 1. valida estoque insuficiente ANTES de tocar em qualquer tabela
        CREATE TABLE #Insuficientes (ProdutoId INT, Disponivel INT, Solicitado INT);

        INSERT INTO #Insuficientes (ProdutoId, Disponivel, Solicitado)
        SELECT p.Id, p.Estoque, i.Quantidade
        FROM @Itens i
        INNER JOIN dbo.Produto p ON p.Id = i.ProdutoId
        WHERE p.Estoque < i.Quantidade;

        IF EXISTS (SELECT 1 FROM #Insuficientes)
            THROW 50010, 'Um ou mais produtos não têm estoque suficiente.', 1;

        BEGIN TRANSACTION;

        -- 2. cria o pedido
        INSERT INTO dbo.Pedido (ClienteId, Status)
        VALUES (@ClienteId, 'Confirmado');

        SET @PedidoId = SCOPE_IDENTITY();

        -- 3. copia os itens do carrinho, travando o preço atual
        INSERT INTO dbo.PedidoItem (PedidoId, ProdutoId, Quantidade, PrecoUnit)
        SELECT @PedidoId, i.ProdutoId, i.Quantidade, p.Preco
        FROM @Itens i
        INNER JOIN dbo.Produto p ON p.Id = i.ProdutoId;

        -- 4. debita o estoque
        UPDATE p
        SET p.Estoque = p.Estoque - i.Quantidade
        FROM dbo.Produto p
        INNER JOIN @Itens i ON i.ProdutoId = p.Id;

        -- 5. calcula e grava o valor total
        UPDATE dbo.Pedido
        SET ValorTotal = (
            SELECT SUM(Quantidade * PrecoUnit)
            FROM dbo.PedidoItem
            WHERE PedidoId = @PedidoId
        )
        WHERE Id = @PedidoId;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0
            ROLLBACK TRANSACTION;

        INSERT INTO dbo.ErrorLog (Numero, Mensagem, Procedure_, DataOcorrencia)
        VALUES (ERROR_NUMBER(), ERROR_MESSAGE(), ERROR_PROCEDURE(), SYSDATETIME());

        THROW;
    END CATCH
END;
GO

4. Trigger de auditoria de status

SQL trg_Pedido_AuditarStatus
CREATE TABLE dbo.PedidoStatusHistorico
(
    Id           INT IDENTITY(1,1) PRIMARY KEY,
    PedidoId     INT NOT NULL,
    StatusAntigo NVARCHAR(20),
    StatusNovo   NVARCHAR(20),
    AlteradoEm   DATETIME2 NOT NULL DEFAULT SYSDATETIME()
);
GO

CREATE OR ALTER TRIGGER trg_Pedido_AuditarStatus
ON dbo.Pedido
AFTER UPDATE
AS
BEGIN
    SET NOCOUNT ON;

    IF NOT UPDATE(Status)
        RETURN;

    INSERT INTO dbo.PedidoStatusHistorico (PedidoId, StatusAntigo, StatusNovo)
    SELECT d.Id, d.Status, i.Status
    FROM deleted d
    INNER JOIN inserted i ON i.Id = d.Id
    WHERE d.Status <> i.Status;
END;
GO

5. Chamando o checkout a partir do C#

C# finalizando a compra com Dapper
public async Task<int> FinalizarCompraAsync(int clienteId, List<ItemCarrinho> itens)
{
    var tabela = new DataTable();
    tabela.Columns.Add("ProdutoId", typeof(int));
    tabela.Columns.Add("Quantidade", typeof(int));
    foreach (var item in itens)
        tabela.Rows.Add(item.ProdutoId, item.Quantidade);

    var parametros = new DynamicParameters();
    parametros.Add("@ClienteId", clienteId);
    parametros.Add("@Itens", tabela.AsTableValuedParameter("dbo.ItemCarrinhoTableType"));
    parametros.Add("@PedidoId", dbType: DbType.Int32, direction: ParameterDirection.Output);

    await using var conexao = new SqlConnection(_connectionString);

    try
    {
        await conexao.ExecuteAsync(
            "dbo.usp_PedidoCheckout",
            parametros,
            commandType: CommandType.StoredProcedure);

        return parametros.Get<int>("@PedidoId");
    }
    catch (SqlException ex) when (ex.Number == 50010)
    {
        throw new EstoqueInsuficienteException(ex.Message);
    }
}
Dica

catch (SqlException ex) when (ex.Number == 50010) usa exception filters do C# para capturar especificamente o erro de negócio lançado pelo THROW 50010 da procedure, traduzindo-o para uma exceção própria do domínio da aplicação — o número do erro (definido na procedure) vira o "contrato" entre o banco e o C#.

6. O que praticar a partir daqui

Este projeto toca a maior parte do que o módulo cobriu, mas um sistema real cresce em outras direções: paginação nas listagens, cancelamento de pedido (com estorno de estoque), relatórios agregados por período. São todas extensões diretas dos padrões já vistos — o próximo salto real de conteúdo vem nos módulos seguintes, quando essas procedures passam a ser consumidas por uma API ASP.NET Core (Módulo 4) construída sobre uma arquitetura MVC (Módulo 3).

📌 Resumo do capítulo

  • Uma procedure de checkout bem escrita combina TVP (entrada), tabela temporária (validação), transação + TRY/CATCH (atomicidade) e OUTPUT (retorno) — cada peça vista isoladamente nos capítulos anteriores.
  • Validar regras de negócio (estoque insuficiente) antes de abrir a transação evita rollback desnecessário.
  • O número de um erro customizado (THROW 50010) pode servir de contrato estável entre a procedure e o tratamento de exceção no C#.

✏️ Praticando

  1. Implemente o modelo de dados completo e usp_PedidoCheckout.
  2. Teste um checkout com estoque insuficiente e confirme, consultando as tabelas, que nada foi alterado (nem pedido, nem estoque).
  3. Implemente usp_PedidoCancelar, que muda o status para 'Cancelado' e devolve o estoque debitado — reaproveitando transação e trigger de auditoria já existentes.
  4. Escreva o método C# completo (como na seção 5) e teste os dois cenários (sucesso e estoque insuficiente) num pequeno projeto console.