1. O problema: inserir vários registros de uma vez
Até agora, toda procedure de escrita processa uma linha por chamada. Mas e se a aplicação C# tem uma lista de 50 itens de pedido para inserir de uma vez? A solução ingênua — chamar a procedure 50 vezes num loop — funciona, mas gera 50 viagens de rede (o mesmo problema do capítulo 02, fig. 1). Table-Valued Parameters (TVPs) resolvem isso: você manda a lista inteira como um único parâmetro.
2. Criando um tipo de tabela
TVPs exigem um passo a mais: definir a "forma" da tabela como um tipo nomeado, com
CREATE TYPE, antes de usá-la como parâmetro.
CREATE TYPE dbo.ItemPedidoTableType AS TABLE
(
ProdutoId INT NOT NULL,
Quantidade INT NOT NULL,
PrecoUnit DECIMAL(10,2) NOT NULL
);
GO
3. Usando o tipo como parâmetro
CREATE PROCEDURE dbo.usp_PedidoInserirComItens
@ClienteId INT,
@Itens dbo.ItemPedidoTableType READONLY
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
DECLARE @PedidoId INT;
INSERT INTO dbo.Pedido (ClienteId, DataPedido)
VALUES (@ClienteId, SYSDATETIME());
SET @PedidoId = SCOPE_IDENTITY();
INSERT INTO dbo.PedidoItem (PedidoId, ProdutoId, Quantidade, PrecoUnit)
SELECT @PedidoId, ProdutoId, Quantidade, PrecoUnit
FROM @Itens;
COMMIT TRANSACTION;
SELECT @PedidoId AS PedidoId;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
THROW;
END CATCH
END;
GO
READONLY é obrigatório
Todo TVP precisa do modificador READONLY — você pode ler o conteúdo
(SELECT ... FROM @Itens), mas não pode fazer
INSERT/UPDATE/DELETE
diretamente nele dentro da procedure.
4. Testando direto no T-SQL
DECLARE @MeusItens dbo.ItemPedidoTableType;
INSERT INTO @MeusItens (ProdutoId, Quantidade, PrecoUnit)
VALUES (1, 2, 49.90),
(5, 1, 199.00),
(7, 3, 15.50);
EXEC dbo.usp_PedidoInserirComItens
@ClienteId = 42,
@Itens = @MeusItens;
5. Enviando de C# com ADO.NET
Do lado do C#, um TVP é passado como uma DataTable (ou uma lista de
SqlDataRecord), atribuída a um SqlParameter
com SqlDbType.Structured:
var tabela = new DataTable();
tabela.Columns.Add("ProdutoId", typeof(int));
tabela.Columns.Add("Quantidade", typeof(int));
tabela.Columns.Add("PrecoUnit", typeof(decimal));
tabela.Rows.Add(1, 2, 49.90m);
tabela.Rows.Add(5, 1, 199.00m);
tabela.Rows.Add(7, 3, 15.50m);
using var comando = new SqlCommand("dbo.usp_PedidoInserirComItens", conexao);
comando.CommandType = CommandType.StoredProcedure;
comando.Parameters.AddWithValue("@ClienteId", 42);
var parametroItens = comando.Parameters.AddWithValue("@Itens", tabela);
parametroItens.SqlDbType = SqlDbType.Structured;
parametroItens.TypeName = "dbo.ItemPedidoTableType";
conexao.Open();
var pedidoId = (int)comando.ExecuteScalar();
Pense num TVP como o equivalente T-SQL de passar uma List<ItemPedido>
inteira como argumento de um método C# — só que atravessando a fronteira rede/banco em uma única
chamada, em vez de serializar item por item.
6. Onde isso te leva
Com TVPs você processa listas em lote sem sacrificar a estrutura de uma procedure única. O próximo capítulo trata do outro extremo — quando você realmente precisa processar linha por linha dentro do T-SQL — e por que isso deveria ser sua última opção.