Projetando um gatilho Microsoft T-SQL
Em ocasiões ao construir um projeto envolvendo um front-end do Access e um back-end do SQL Server, nos deparamos com essa pergunta. Devemos usar um gatilho para alguma coisa? Projetar um gatilho do SQL Server para o aplicativo Access pode ser uma solução, mas somente após considerações cuidadosas. Às vezes, isso é sugerido como uma maneira de manter a lógica de negócios no banco de dados, em vez do aplicativo. Normalmente, gosto de ter a lógica de negócios definida o mais próximo possível do banco de dados. Então, trigger é a solução que queremos para nosso front-end de Access?
Descobri que codificar um gatilho SQL requer considerações adicionais e, se não tomarmos cuidado, podemos acabar com uma bagunça maior do que começamos. O artigo visa cobrir todas as armadilhas e técnicas que podemos usar para garantir que, quando construímos um banco de dados com gatilhos, eles funcionem em nosso benefício, em vez de apenas adicionar complexidade por causa da complexidade.
Vamos considerar as regras...
Regra nº 1:não use um acionador!
Seriamente. Se você está alcançando o gatilho logo pela manhã, vai se arrepender à noite. O maior problema com gatilhos em geral é que eles podem efetivamente ofuscar sua lógica de negócios e interferir em processos que não deveriam precisar de um gatilho. Vi algumas sugestões para desativar os acionadores quando você está fazendo um carregamento em massa ou algo semelhante. Eu afirmo que este é um cheiro de código grande. Você não deve usar um gatilho se tiver que ser ligado ou desligado condicionalmente.
Por padrão, devemos escrever procedimentos armazenados ou visualizações primeiro. Para a maioria dos cenários, eles farão o trabalho muito bem. Não vamos adicionar mágica aqui.
Então, por que o artigo sobre gatilho então?
Porque os gatilhos têm seus usos. Precisamos reconhecer quando devemos usar gatilhos. Também precisamos escrevê-los de uma maneira que nos ajude mais do que nos machuque.
Regra nº 2:eu realmente preciso de um acionador?
Em teoria, os gatilhos soam bem. Eles nos fornecem um modelo baseado em eventos para gerenciar as alterações assim que forem modificadas. Mas se tudo o que você precisa é validar alguns dados ou garantir que algumas colunas ocultas ou tabelas de log sejam preenchidas…. Acho que você descobrirá que um procedimento armazenado faz o trabalho com mais eficiência e remove o aspecto mágico. Além disso, escrever um procedimento armazenado é fácil de testar; simplesmente configure alguns dados simulados e execute o procedimento armazenado, verifique se os resultados são o que você esperava. Espero que você esteja usando uma estrutura de teste como tSQLt.
E é importante observar que geralmente é mais eficiente usar restrições de banco de dados do que um gatilho. Portanto, se você precisar apenas validar que um valor é válido em outra tabela, use uma restrição de chave estrangeira. Validar que um valor está dentro de um determinado intervalo exige uma restrição de verificação. Essa deve ser sua escolha padrão para esse tipo de validações.
Então, quando realmente precisaremos de um gatilho?
Isso se resume aos casos em que você realmente deseja que a lógica de negócios esteja na camada SQL. Talvez porque você tenha vários clientes em diferentes linguagens de programação fazendo inserções/atualizações em uma tabela. Seria muito confuso duplicar a lógica de negócios em cada cliente em sua respectiva linguagem de programação e isso também significa mais bugs. Para cenários em que não é prático criar uma camada de camada intermediária, os gatilhos são o melhor curso de ação para impor a regra de negócios que não pode ser expressa como uma restrição.
Para usar um exemplo específico do Access. Suponha que queremos impor a lógica de negócios ao modificar dados por meio do aplicativo. Talvez tenhamos vários formulários de entrada de dados vinculados a uma mesma tabela, ou talvez precisemos oferecer suporte a formulários de entrada de dados complexos, onde várias tabelas base devem participar da edição. Talvez o formulário de entrada de dados precise oferecer suporte a entradas não normalizadas que, em seguida, recomponhamos em dados normalizados. Em todos esses casos, poderíamos apenas escrever código VBA, mas isso pode ser difícil de manter e validar para todos os casos. Os gatilhos nos ajudam a mover a lógica do VBA para o T-SQL. A lógica de negócios centrada em dados geralmente é a mais próxima possível dos dados.
Regra nº 3:o gatilho deve ser baseado em conjunto, não em linha
De longe, o erro mais comum cometido com um gatilho é executá-lo em linhas. Muitas vezes vemos código semelhante a este:
--Bad code! Do not use! CREATE TRIGGER dbo.SomeTrigger ON dbo.SomeTable AFTER INSERT AS BEGIN DECLARE @NewTotal money; DECLARE @NewID int; SELECT TOP 1 @NewID = SalesOrderID, @NewTotal = SalesAmount FROM inserted; UPDATE dbo.SalesOrder SET OrderTotal = OrderTotal + @NewTotal WHERE SalesOrderID = @SalesOrderID END;
A oferta deve ser o mero fato de que houve um SELECT TOP 1 de uma mesa inserido. Isso só funcionará enquanto inserirmos apenas uma linha. Mas quando há mais de uma linha, o que acontece com as linhas azaradas que vieram em 2º e depois? Podemos melhorar isso fazendo algo semelhante a isto:
--Still bad code! Do not use! CREATE TRIGGER dbo.SomeTrigger ON dbo.SomeTable AFTER INSERT AS BEGIN MERGE INTO dbo.SalesOrder AS s USING inserted AS i ON s.SalesOrderID = i.SalesOrderID WHEN MATCHED THEN UPDATE SET OrderTotal = OrderTotal + @NewTotal ; END;
Isso agora é baseado em conjuntos e, portanto, muito aprimorado, mas ainda tem outros problemas que veremos nas próximas regras…
Regra nº 4:use uma visualização.
Uma visualização pode ter um gatilho anexado a ela. Isso nos dá a vantagem de evitar problemas associados a triggers de tabela. Poderíamos importar facilmente dados limpos em massa para a tabela sem precisar desabilitar nenhum gatilho. Além disso, um acionador em exibição o torna uma opção de aceitação explícita. Se você tiver funcionalidades relacionadas à segurança ou regras de negócios que exijam a execução de gatilhos, você pode simplesmente revogar as permissões na tabela diretamente e, assim, afunilá-las para a nova visualização. Isso garante que você passará pelo projeto e observará onde as atualizações da tabela são necessárias para que você possa rastreá-las em busca de possíveis bugs ou problemas.
A desvantagem é que uma visão só pode ter gatilhos INSTEAD OF anexados, o que significa que você deve executar explicitamente as modificações equivalentes na tabela base dentro do gatilho. No entanto, costumo pensar que é melhor assim porque também garante que você saiba exatamente qual será a modificação e, portanto, fornece o mesmo nível de controle que você normalmente tem em um procedimento armazenado.
Regra nº 5:o gatilho deve ser simples e estúpido.
Lembre-se do comentário sobre depuração e teste de um procedimento armazenado? O melhor favor que podemos fazer a nós mesmos é manter a lógica de negócios em um procedimento armazenado e fazer com que o gatilho a invoque. Você nunca deve escrever lógica de negócios diretamente no gatilho; que está efetivamente despejando concreto no banco de dados. Ele agora está congelado na forma e pode ser problemático testar adequadamente a lógica. Seu equipamento de teste agora deve envolver alguma modificação na tabela base. Isso não é bom para escrever testes simples e repetíveis. Isso deve ser o mais complicado, pois seu gatilho deve ser permitido:
CREATE TRIGGER [dbo].[SomeTrigger] ON [dbo].[SomeView] INSTEAD OF INSERT, UPDATE, DELETE AS BEGIN DECLARE @SomeIDs AS SomeIDTableType --Perform the merge into the base table MERGE INTO dbo.SomeTable AS t USING inserted AS i ON t.SomeID = i.SomeID WHEN MATCHED THEN UPDATE SET t.SomeStuff = i.SomeStuff, t.OtherStuff = i.OtherStuff WHEN NOT MATCHED THEN INSERT ( SomeStuff, OtherStuff ) VALUES ( i.SomeStuff, i.OtherStuff ) OUTPUT inserted.SomeID INTO @SomeIDs(SomeID); DELETE FROM dbo.SomeTable OUTPUT deleted.SomeID INTO @SomeIDs(SomeID) WHERE EXISTS ( SELECT NULL FROM deleted AS d WHERE d.SomeID = SomeTable.SomeID ) AND NOT EXISTS ( SELECT NULL FROM inserted AS i WHERE i.SomeID = SomeTable.SomeID ); EXEC dbo.uspUpdateSomeStuff @SomeIDs; END;
A primeira parte do gatilho é basicamente realizar as modificações reais na tabela base porque é um gatilho INSTEAD OF, então devemos realizar todas as modificações que serão diferentes dependendo das tabelas que precisamos gerenciar. Vale ressaltar que as modificações devem ser principalmente textuais. Não recalculamos ou transformamos nenhum dos dados. Salvamos todo esse trabalho extra no final, onde tudo o que estamos fazendo no gatilho é preencher uma lista de registros que foram modificados pelo gatilho e fornecer a um procedimento armazenado usando um parâmetro com valor de tabela. Observe que não estamos considerando quais registros foram alterados nem como foram alterados. Tudo o que pode ser feito dentro do procedimento armazenado.
Regra nº 6:o gatilho deve ser idempotente sempre que possível.
De um modo geral, os acionadores PRECISAM ser idempotente. Isso se aplica independentemente de ser um gatilho baseado em tabela ou em exibição. Aplica-se especialmente àqueles que precisam modificar os dados nas tabelas base de onde o trigger está monitorando. Por quê? Porque se os humanos estiverem modificando os dados que serão coletados pelo gatilho, eles podem perceber que cometeram um erro, editaram novamente ou talvez simplesmente editar o mesmo registro e salvá-lo 3 vezes. Eles não ficarão felizes se descobrirem que os relatórios mudam toda vez que fazem uma edição que não deveria modificar a saída do relatório.
Para ser mais explícito, pode ser tentador tentar otimizar o gatilho fazendo algo semelhante a isto:
WITH SourceData AS ( SELECT OrderID, SUM(SalesAmount) AS NewSaleTotal FROM inserted GROUP BY OrderID ) MERGE INTO dbo.SalesOrder AS o USING SourceData AS d ON o.OrderID = d.OrderID WHEN MATCHED THEN UPDATE SET o.OrderTotal = o.OrderTotal + d.NewSaleTotal;
Podemos evitar recalcular o novo total apenas revisando as linhas modificadas na tabela inserida, certo? Mas quando o usuário edita o registro para corrigir um erro de digitação no nome do cliente, o que acontecerá? Acabamos com um total falso, e o gatilho agora está trabalhando contra nós.
Até agora, você deve ver por que a regra nº 4 nos ajuda ao enviar apenas as chaves primárias para o procedimento armazenado, em vez de tentar passar quaisquer dados para o procedimento armazenado ou fazê-lo diretamente dentro do gatilho, como o exemplo teria feito .
Em vez disso, queremos ter algum código semelhante a este dentro de um procedimento armazenado:
CREATE PROCEDURE dbo.uspUpdateSalesTotal ( @SalesOrders SalesOrderTableType READONLY ) AS BEGIN WITH SourceData AS ( SELECT s.OrderID, SUM(s.SalesAmount) AS NewSaleTotal FROM dbo.SalesOrder AS s WHERE EXISTS ( SELECT NULL FROM @SalesOrders AS x WHERE x.SalesOrderID = s.SalesOrderID ) GROUP BY OrderID ) MERGE INTO dbo.SalesOrder AS o USING SourceData AS d ON o.OrderID = d.OrderID WHEN MATCHED THEN UPDATE SET o.OrderTotal = d.NewSaleTotal; END;
Usando o @SalesOrders, ainda podemos atualizar seletivamente apenas as linhas que foram afetadas pelo gatilho e também podemos recalcular o novo total e torná-lo o novo total. Portanto, mesmo que o usuário tenha cometido um erro de digitação no nome do cliente e o editado, cada salvamento produzirá o mesmo resultado para essa linha.
Mais importante, essa abordagem também nos fornece uma maneira fácil de corrigir os totais. Suponha que temos que fazer uma importação em massa, e a importação não contém o total, então devemos calculá-lo nós mesmos. Podemos escrever o procedimento armazenado para gravar diretamente na tabela. Podemos então invocar o procedimento armazenado acima passando os IDs da importação, e tudo bem. Assim, a lógica que usamos não está vinculada ao gatilho por trás da exibição. Isso ajuda quando a lógica é desnecessária para a importação em massa que estamos realizando.
Se você tiver problemas para tornar seu gatilho idempotente, é uma forte indicação de que talvez seja necessário usar um procedimento armazenado e chamá-lo diretamente de seu aplicativo em vez de depender de gatilhos. Uma exceção notável a essa regra é quando o gatilho se destina principalmente a ser um gatilho de auditoria. Nesse caso, você deseja gravar uma nova linha na tabela de auditoria para cada edição, incluindo todos os erros de digitação que o usuário fizer. Isso está correto porque, nesse caso, não há alterações nos dados com os quais o usuário está interagindo. Do ponto de vista do usuário, ainda é o mesmo resultado. Mas sempre que o gatilho precisar manipular os mesmos dados com os quais o usuário está trabalhando, é muito melhor quando for idempotente.
Encerrando
Espero que agora você possa ver o quão mais difícil pode ser projetar um gatilho bem comportado. Por esse motivo, você deve considerar cuidadosamente se pode evitá-lo completamente e usar invocações diretas com procedimento armazenado. Mas se você concluiu que deve ter triggers para gerenciar as modificações feitas via views, espero que as regras te ajudem. Fazer o gatilho baseado em conjunto é bastante fácil com alguns ajustes. Torná-lo idempotente geralmente requer mais reflexões sobre como você implementará seus procedimentos armazenados.
Se você tiver mais sugestões ou regras para compartilhar, dispare nos comentários!