sql >> Base de Datos >  >> RDS >> Access

Diseño de un disparador de Microsoft T-SQL

Diseño de un disparador de Microsoft T-SQL

En ocasiones, cuando construimos un proyecto que involucra un front-end de Access y un back-end de SQL Server, nos hemos encontrado con esta pregunta. ¿Deberíamos usar un gatillo para algo? El diseño de un disparador de SQL Server para la aplicación de Access puede ser una solución, pero solo después de consideraciones cuidadosas. En algún momento, esto se sugiere como una forma de mantener la lógica comercial dentro de la base de datos, en lugar de la aplicación. Normalmente, me gusta tener la lógica empresarial definida lo más cerca posible de la base de datos. Entonces, ¿trigger es la solución que queremos para nuestro front-end de Access?

Descubrí que la codificación de un activador de SQL requiere consideraciones adicionales y, si no tenemos cuidado, podemos terminar con un lío mayor de lo que comenzamos. El artículo tiene como objetivo cubrir todas las trampas y técnicas que podemos usar para garantizar que cuando construimos una base de datos con disparadores, funcionen para nuestro beneficio, en lugar de solo agregar complejidad por el bien de la complejidad.

Consideremos las reglas...

Regla n.° 1:¡No uses un disparador!

En serio. Si busca el gatillo a primera hora de la mañana, se arrepentirá por la noche. El mayor problema con los disparadores en general es que pueden ofuscar efectivamente su lógica comercial e interferir con procesos que no deberían necesitar un disparador. He visto algunas sugerencias para desactivar los activadores cuando realiza una carga masiva o algo similar. Afirmo que este es un gran olor a código. No debería usar un activador si tiene que activarse o desactivarse condicionalmente.

Por defecto, primero deberíamos escribir procedimientos almacenados o vistas. Para la mayoría de los escenarios, harán bien el trabajo. No agreguemos magia aquí.

Entonces, ¿por qué el artículo sobre el gatillo entonces?

Porque los disparadores tienen sus usos. Necesitamos reconocer cuándo debemos usar disparadores. También debemos escribirlas de manera que nos ayuden más que lastimarnos.

Regla n.° 2:¿Realmente necesito un activador?

En teoría, los disparadores suenan bien. Nos proporcionan un modelo basado en eventos para gestionar los cambios tan pronto como se modifican. Pero si todo lo que necesita es validar algunos datos o asegurarse de que se completen algunas columnas ocultas o tablas de registro... Creo que encontrará que un procedimiento almacenado hace el trabajo de manera más eficiente y elimina el aspecto mágico. Además, escribir un procedimiento almacenado es fácil de probar; simplemente configure algunos datos simulados y ejecute el procedimiento almacenado, verifique que los resultados sean los que esperaba. Espero que esté utilizando un marco de prueba como tSQLt.

Y es importante tener en cuenta que, por lo general, es más eficiente usar restricciones de base de datos que un disparador. Entonces, si solo necesita validar que un valor es válido en otra tabla, use una restricción de clave externa. Validar que un valor está dentro de cierto rango requiere una restricción de verificación. Esas deberían ser su elección predeterminada para ese tipo de validaciones.

Entonces, ¿cuándo necesitaremos realmente un disparador?

Se reduce a casos en los que realmente desea que la lógica comercial esté en la capa SQL. Tal vez porque tiene varios clientes en diferentes lenguajes de programación que realizan inserciones/actualizaciones en una tabla. Sería muy complicado duplicar la lógica comercial en cada cliente en su respectivo lenguaje de programación y esto también significa más errores. Para escenarios en los que no es práctico crear una capa de nivel intermedio, los disparadores son su mejor curso de acción para hacer cumplir la regla comercial que no se puede expresar como una restricción.

Para usar un ejemplo específico de Access. Supongamos que queremos hacer cumplir la lógica comercial al modificar datos a través de la aplicación. Tal vez tengamos múltiples formularios de entrada de datos vinculados a una misma tabla, o tal vez necesitemos admitir formularios de entrada de datos complejos donde varias tablas base deben participar en la edición. Tal vez el formulario de entrada de datos necesite admitir entradas no normalizadas que luego recompongamos en datos normalizados. En todos esos casos, podríamos simplemente escribir código VBA, pero eso puede ser difícil de mantener y validar para todos los casos. Triggers nos ayuda a mover la lógica de VBA a T-SQL. La lógica empresarial centrada en los datos generalmente se ubica mejor cerca de los datos como sea posible.

Regla n.º 3:el activador debe basarse en conjuntos, no en filas

Con mucho, el error más común que se comete con un disparador es hacer que se ejecute en filas. A menudo vemos un código similar 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;

El obsequio debe ser el mero hecho de que hubo un SELECT TOP 1 de una mesa insertado. Esto solo funcionará mientras insertemos solo una fila. Pero cuando se trata de más de una fila, ¿qué sucede con esas filas desafortunadas que llegaron en segundo lugar y después? Podemos mejorar eso haciendo algo similar a esto:

--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;

Esto ahora está basado en conjuntos y, por lo tanto, ha mejorado mucho, pero todavía tiene otros problemas que veremos en las próximas reglas...

Regla #4:Use una vista en su lugar.

Una vista puede tener un activador adjunto. Esto nos da la ventaja de evitar los problemas asociados con los disparadores de una tabla. Podríamos importar fácilmente datos limpios en masa en la tabla sin tener que deshabilitar ningún activador. Además, un activador a la vista lo convierte en una opción de suscripción explícita. Si tiene funcionalidades relacionadas con la seguridad o reglas comerciales que requieren la ejecución de disparadores, simplemente puede revocar los permisos en la tabla directamente y, por lo tanto, canalizarlos hacia la nueva vista. Eso garantiza que revisará el proyecto y observará dónde se necesitan actualizaciones en la tabla para que luego pueda rastrearlas en busca de posibles errores o problemas.

La desventaja es que una vista solo puede tener un disparador INSTEAD OF adjunto, lo que significa que usted mismo debe realizar explícitamente las modificaciones equivalentes en la tabla base dentro del disparador. Sin embargo, tiendo a pensar que es mejor así porque también asegura que sepa exactamente cuál será la modificación y, por lo tanto, le brinda el mismo nivel de control que normalmente tiene dentro de un procedimiento almacenado.

Regla n.° 5:el disparador debe ser simple y tonto.

¿Recuerda el comentario sobre la depuración y prueba de un procedimiento almacenado? El mejor favor que podemos hacernos a nosotros mismos es mantener la lógica empresarial en un procedimiento almacenado y hacer que el activador la invoque en su lugar. Nunca debe escribir la lógica comercial directamente en el disparador; eso efectivamente está vertiendo concreto en la base de datos. Ahora está congelado en la forma y puede ser problemático probar adecuadamente la lógica. Su arnés de prueba ahora debe implicar alguna modificación en la tabla base. Esto no es bueno para escribir pruebas simples y repetibles. Este debería ser el más complicado, ya que se debe permitir que su disparador sea:

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;

La primera parte del disparador es básicamente realizar las modificaciones reales en la tabla base porque es un disparador EN LUGAR DE, por lo que debemos realizar todas las modificaciones que serán diferentes según las tablas que necesitemos administrar. Vale la pena enfatizar que las modificaciones deben ser principalmente textuales. No volvemos a calcular ni transformamos ninguno de los datos. Guardamos todo ese trabajo extra al final, donde todo lo que estamos haciendo dentro del disparador es completar una lista de registros que fueron modificados por el disparador y proporcionar a un procedimiento almacenado usando un parámetro con valores de tabla. Tenga en cuenta que ni siquiera estamos considerando qué registros se cambiaron ni cómo se cambiaron. Todo eso se puede hacer dentro del procedimiento almacenado.

Regla n.º 6:el activador debe ser idempotente siempre que sea posible.

En términos generales, los disparadores DEBEN ser idempotente. Esto se aplica independientemente de si se trata de un disparador basado en tablas o basado en vistas. Se aplica especialmente a aquellos que necesitan modificar los datos en las tablas base desde donde se monitorea el disparador. ¿Por qué? Porque si los humanos están modificando los datos que recogerá el disparador, es posible que se den cuenta de que cometieron un error, lo editaron nuevamente o tal vez simplemente editaron el mismo registro y lo guardaron 3 veces. No estarán contentos si descubren que los informes cambian cada vez que realizan una edición que no debería modificar la salida del informe.

Para ser más explícito, podría ser tentador intentar optimizar el disparador haciendo algo similar a esto:

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 volver a calcular el nuevo total simplemente revisando las filas modificadas en la tabla insertada, ¿verdad? Pero cuando el usuario edita el registro para corregir un error tipográfico en el nombre del cliente, ¿qué sucederá? Terminamos con un total falso, y el disparador ahora está trabajando en nuestra contra.

A estas alturas, debería ver por qué la regla n. ° 4 nos ayuda al extraer solo las claves principales del procedimiento almacenado, en lugar de intentar pasar cualquier dato al procedimiento almacenado o hacerlo directamente dentro del activador como lo habría hecho la muestra. .

En cambio, queremos tener un código similar a este dentro de un procedimiento almacenado:

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 @SalesOrders, aún podemos actualizar selectivamente solo las filas que se vieron afectadas por el activador, y también podemos recalcular el nuevo total por completo y convertirlo en el nuevo total. Entonces, incluso si el usuario cometió un error tipográfico en el nombre del cliente y lo editó, cada vez que guarde obtendrá el mismo resultado para esa fila.

Más importante aún, este enfoque también nos proporciona una manera fácil de corregir los totales. Supongamos que tenemos que hacer una importación masiva y la importación no contiene el total, por lo que debemos calcularlo nosotros mismos. Podemos escribir el procedimiento almacenado para escribir en la tabla directamente. Luego podemos invocar el procedimiento almacenado anterior pasando los ID de la importación, y todo está bien. Por lo tanto, la lógica que usamos no está ligada al disparador detrás de la vista. Eso ayuda cuando la lógica es innecesaria para la importación masiva que estamos realizando.

Si tiene problemas para hacer que su disparador sea idempotente, es una fuerte indicación de que es posible que necesite usar un procedimiento almacenado en su lugar y llamarlo directamente desde su aplicación en lugar de depender de los disparadores. Una excepción notable a esta regla es cuando el activador está destinado principalmente a ser un activador de auditoría. En este caso, desea escribir una nueva fila en la tabla de auditoría para cada edición, incluidos todos los errores tipográficos que comete el usuario. Esto está bien porque, en ese caso, no hay cambios en los datos con los que interactúa el usuario. Desde el punto de vista del usuario, sigue siendo el mismo resultado. Pero siempre que el disparador necesite manipular los mismos datos con los que está trabajando el usuario, es mucho mejor cuando es idempotente.

Conclusión

Con suerte, ahora puede ver cuánto más difícil puede ser diseñar un disparador que se comporte bien. Por ese motivo, debe considerar detenidamente si puede evitarlo por completo y utilizar invocaciones directas con procedimientos almacenados. Pero si ha llegado a la conclusión de que debe tener disparadores para administrar las modificaciones realizadas a través de las vistas, espero que las reglas lo ayuden. Hacer que el disparador se base en un conjunto es bastante fácil con algunos ajustes. Hacerlo idempotente generalmente requiere pensar más en cómo implementará sus procedimientos almacenados.

Si tiene más sugerencias o reglas para compartir, ¡déjelas en los comentarios!