1. Quando SQL "fixo" não é suficiente
Todas as procedures até aqui têm uma query com estrutura fixa: os parâmetros mudam, mas as
tabelas, colunas e cláusulas WHERE/ORDER BY
são sempre as mesmas. Às vezes isso não é suficiente — por exemplo, permitir que o usuário escolha
qual coluna ordenar, ou montar um WHERE com um número
variável de condições. É para isso que existe SQL dinâmico: montar o comando como uma string e
executá-lo.
2. sp_executesql — a forma correta
Existem duas formas de executar SQL dinâmico: EXEC(string) e
sp_executesql. Use sempre a segunda.
CREATE PROCEDURE dbo.usp_ClienteListarOrdenado
@OrdenarPor NVARCHAR(50) = 'Nome'
AS
BEGIN
SET NOCOUNT ON;
DECLARE @ColunasPermitidas TABLE (Coluna NVARCHAR(50));
INSERT INTO @ColunasPermitidas VALUES ('Nome'), ('Email'), ('DataCadastro');
IF NOT EXISTS (SELECT 1 FROM @ColunasPermitidas WHERE Coluna = @OrdenarPor)
THROW 50000, 'Coluna de ordenação inválida.', 1;
DECLARE @Sql NVARCHAR(MAX);
SET @Sql = N'SELECT Id, Nome, Email, DataCadastro FROM dbo.Cliente ORDER BY '
+ QUOTENAME(@OrdenarPor);
EXEC sp_executesql @Sql;
END;
GO
Se @OrdenarPor viesse de um campo de tela sem essa validação, alguém
poderia enviar algo como Nome; DROP TABLE Cliente;--. Isso é
SQL injection — a mesma classe de vulnerabilidade que existe em qualquer
linguagem que monta comandos como texto. As três defesas usadas no exemplo acima —
lista de valores permitidos, QUOTENAME() e
sp_executesql — são a forma correta de mitigar isso.
3. QUOTENAME — obrigatório para identificadores dinâmicos
Nomes de tabela/coluna não podem ser parametrizados como valores (você não pode fazer
ORDER BY @Coluna diretamente — T-SQL trataria isso como um valor
literal, não como nome de coluna). Por isso, quando o nome de um identificador entra numa string
dinâmica, ele precisa passar por QUOTENAME(), que o envolve em colchetes
e neutraliza qualquer caractere especial:
SELECT QUOTENAME('Nome'); -- [Nome]
SELECT QUOTENAME('Nome]; DROP TABLE X;--'); -- [Nome]]; DROP TABLE X;--]
-- o resultado malicioso vira um NOME DE COLUNA inválido e inofensivo,
-- em vez de ser interpretado como comando
4. Parâmetros de valor continuam parametrizados
Diferente de nomes de coluna, valores (um Id, um texto de busca) devem sempre ser passados como
parâmetros de verdade para sp_executesql — nunca concatenados na string:
CREATE PROCEDURE dbo.usp_ClienteBuscarDinamico
@OrdenarPor NVARCHAR(50) = 'Nome',
@Nome NVARCHAR(100) = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE @ColunasPermitidas TABLE (Coluna NVARCHAR(50));
INSERT INTO @ColunasPermitidas VALUES ('Nome'), ('Email'), ('DataCadastro');
IF NOT EXISTS (SELECT 1 FROM @ColunasPermitidas WHERE Coluna = @OrdenarPor)
THROW 50000, 'Coluna de ordenação inválida.', 1;
DECLARE @Sql NVARCHAR(MAX);
SET @Sql = N'SELECT Id, Nome, Email FROM dbo.Cliente '
+ N'WHERE (@NomeParam IS NULL OR Nome LIKE ''%'' + @NomeParam + ''%'') '
+ N'ORDER BY ' + QUOTENAME(@OrdenarPor);
EXEC sp_executesql
@Sql,
N'@NomeParam NVARCHAR(100)',
@NomeParam = @Nome;
END;
GO
Note a diferença: @OrdenarPor (um identificador) passa
por QUOTENAME e é concatenado direto na string, porque não existe outra
forma de parametrizar nome de coluna. Já @Nome (um valor)
é passado como parâmetro real de sp_executesql, com tipo declarado —
exatamente como um parâmetro normal de procedure, ganhando a mesma proteção contra injection e o
mesmo reaproveitamento de plano de execução.
5. Onde isso te leva
SQL dinâmico bem escrito resolve casos que o SQL estático não cobre, sem abrir brecha de segurança. O próximo capítulo volta a um assunto mais estrutural: triggers, e como (e quando) elas se relacionam com procedures.