Compartir a través de


Desencadenadores CLR

Debido a la integración de SQL Server con Common Language Runtime (CLR) de .NET Framework, puede usar cualquier lenguaje de .NET Framework para crear desencadenadores CLR. En esta sección se trata información específica de los desencadenadores implementados con la integración clR. Para obtener una explicación completa de los desencadenadores, consulte Desencadenadores DDL.

¿Qué son los desencadenadores?

Un desencadenador es un tipo especial de procedimiento almacenado que se ejecuta automáticamente cuando se ejecuta un evento de lenguaje. SQL Server incluye dos tipos generales de desencadenadores: desencadenadores de lenguaje de manipulación de datos (DML) y lenguaje de definición de datos (DDL). Los desencadenadores DML se pueden usar cuando INSERTlas instrucciones , UPDATEo DELETE modifican datos en una tabla o vista especificadas. Los desencadenadores DDL activan procedimientos almacenados en respuesta a una variedad de instrucciones DDL, que son principalmente instrucciones que comienzan por CREATE, ALTERy DROP. Los desencadenadores DDL se pueden usar para tareas administrativas, como la auditoría y la regulación de las operaciones de base de datos.

Funcionalidades únicas de desencadenadores CLR

Los desencadenadores escritos en Transact-SQL tienen la capacidad de determinar qué columnas de la vista de activación o tabla se han actualizado mediante las UPDATE(column) funciones y COLUMNS_UPDATED() .

Los desencadenadores escritos en un lenguaje CLR difieren de otros objetos de integración CLR de varias maneras significativas. Los desencadenadores CLR pueden:

  • Datos de referencia en las INSERTED tablas y DELETED

  • Determinar qué columnas se han modificado como resultado de una UPDATE operación

  • Obtenga acceso a información sobre los objetos de base de datos afectados por la ejecución de instrucciones DDL.

Estas funcionalidades se proporcionan inherentemente en el lenguaje de consulta o en la SqlTriggerContext clase . Para obtener información sobre las ventajas de la integración clR y elegir entre código administrado y Transact-SQL, consulte Introducción a la integración clR.

Uso de la clase SqlTriggerContext

La SqlTriggerContext clase no se puede construir públicamente y solo se puede obtener accediendo a la SqlContext.TriggerContext propiedad dentro del cuerpo de un desencadenador CLR. La SqlTriggerContext clase se puede obtener del activo SqlContext llamando a la SqlContext.TriggerContext propiedad :

SqlTriggerContext myTriggerContext = SqlContext.TriggerContext;

La SqlTriggerContext clase proporciona información de contexto sobre el desencadenador. Esta información contextual incluye el tipo de acción que provocó que el desencadenador se desencadenara, qué columnas se modificaron en una operación UPDATE y, en el caso de un desencadenador DDL, una estructura XML EventData que describe la operación de desencadenamiento. Para obtener más información, vea EVENTDATA (Transact-SQL).

Determinar la acción del desencadenador

Una vez que haya obtenido un SqlTriggerContext, puede usarlo para determinar el tipo de acción que provocó que se desencadenara el desencadenador. Esta información está disponible a través de la TriggerAction propiedad de la SqlTriggerContext clase .

Para los desencadenadores DML, la TriggerAction propiedad puede ser uno de los siguientes valores:

  • TriggerAction.Update (0x1)

  • TriggerAction.Insert (0x2)

  • TriggerAction.Delete(0x3)

  • En el caso de los desencadenadores DDL, la lista de posibles valores TriggerAction es considerablemente más larga. Consulte "TriggerAction Enumeration" en el SDK de .NET Framework para obtener más información.

Uso de las tablas insertadas y eliminadas

Se usan dos tablas especiales en instrucciones de desencadenador DML: la tabla insertada y la tabla eliminada . SQL Server crea y administra automáticamente estas tablas. Puede usar estas tablas temporales para probar los efectos de determinadas modificaciones de datos y establecer condiciones para acciones de desencadenador DML; sin embargo, no se pueden modificar los datos de las tablas directamente.

Los desencadenadores CLR pueden acceder a las tablas insertadas y eliminadas a través del proveedor de CLR en proceso. Para ello, se obtiene un SqlCommand objeto del objeto SqlContext. Por ejemplo:

C#

SqlConnection connection = new SqlConnection ("context connection = true");  
connection.Open();  
SqlCommand command = connection.CreateCommand();  
command.CommandText = "SELECT * from " + "inserted";  

Visual Basic

Dim connection As New SqlConnection("context connection=true")  
Dim command As SqlCommand  
connection.Open()  
command = connection.CreateCommand()  
command.CommandText = "SELECT * FROM " + "inserted"  

Determinar las columnas actualizadas

Puede determinar el número de columnas modificadas por una operación UPDATE mediante la ColumnCount propiedad del SqlTriggerContext objeto . Puede usar el IsUpdatedColumn método , que toma el ordinal de columna como parámetro de entrada, para determinar si la columna se actualizó. Un True valor indica que la columna se ha actualizado.

Por ejemplo, este fragmento de código (del desencadenador EmailAudit más adelante en este tema) enumera todas las columnas actualizadas:

C#

reader = command.ExecuteReader();  
reader.Read();  
for (int columnNumber = 0; columnNumber < triggContext.ColumnCount; columnNumber++)  
{  
   pipe.Send("Updated column "  
      + reader.GetName(columnNumber) + "? "  
   + triggContext.IsUpdatedColumn(columnNumber).ToString());  
 }  
  
 reader.Close();  

Visual Basic

reader = command.ExecuteReader()  
reader.Read()  
Dim columnNumber As Integer  
  
For columnNumber=0 To triggContext.ColumnCount-1  
  
   pipe.Send("Updated column " & reader.GetName(columnNumber) & _  
   "? " & triggContext.IsUpdatedColumn(columnNumber).ToString() )  
  
Next  
  
reader.Close()  

Acceso a EventData para desencadenadores DDL de CLR

Los desencadenadores DDL, como los desencadenadores normales, activan procedimientos almacenados en respuesta a un evento. Pero a diferencia de los desencadenadores DML, no se activan en respuesta a instrucciones UPDATE, INSERT o DELETE en una tabla o vista. En su lugar, se activan en respuesta a una variedad de instrucciones DDL, que son principalmente instrucciones que comienzan por CREATE, ALTER y DROP. Los desencadenadores DDL se pueden usar para tareas administrativas, como auditoría y supervisión de operaciones de base de datos y cambios de esquema.

La información sobre un evento que desencadena un desencadenador DDL está disponible en la EventData propiedad de la SqlTriggerContext clase . Esta propiedad contiene un xml valor. El esquema xml incluye información sobre:

  • La hora del evento.

  • Identificador de proceso del sistema (SPID) de la conexión durante la cual se ejecutó el desencadenador.

  • Tipo de evento que desencadenó el desencadenador.

A continuación, dependiendo del tipo de evento, el esquema incluye información adicional, como la base de datos en la que se produjo el evento, el objeto en el que se produjo el evento y el comando Transact-SQL del evento.

En el ejemplo siguiente, el siguiente desencadenador DDL devuelve la propiedad raw EventData .

Nota:

El envío de resultados y mensajes a través del SqlPipe objeto se muestra aquí solo con fines ilustrativos y, por lo general, se desaconseja para el código de producción al programar desencadenadores CLR. Los datos adicionales devueltos pueden ser inesperados y provocar errores de aplicación.

C#

using System;  
using System.Data;  
using System.Data.Sql;  
using Microsoft.SqlServer.Server;  
using System.Data.SqlClient;  
using System.Data.SqlTypes;  
using System.Xml;  
using System.Text.RegularExpressions;  
  
public class CLRTriggers  
{  
   public static void DropTableTrigger()  
   {  
       SqlTriggerContext triggContext = SqlContext.TriggerContext;             
  
       switch(triggContext.TriggerAction)  
       {  
           case TriggerAction.DropTable:  
           SqlContext.Pipe.Send("Table dropped! Here's the EventData:");  
           SqlContext.Pipe.Send(triggContext.EventData.Value);  
           break;  
  
           default:  
           SqlContext.Pipe.Send("Something happened! Here's the EventData:");  
           SqlContext.Pipe.Send(triggContext.EventData.Value);  
           break;  
       }  
   }  
}  

Visual Basic

Imports System  
Imports System.Data  
Imports System.Data.Sql  
Imports System.Data.SqlTypes  
Imports Microsoft.SqlServer.Server  
Imports System.Data.SqlClient  
  
'The Partial modifier is only required on one class definition per project.  
Partial Public Class CLRTriggers   
  
    Public Shared Sub DropTableTrigger()  
        Dim triggContext As SqlTriggerContext  
        triggContext = SqlContext.TriggerContext  
  
        Select Case triggContext.TriggerAction  
           Case TriggerAction.DropTable  
              SqlContext.Pipe.Send("Table dropped! Here's the EventData:")  
              SqlContext.Pipe.Send(triggContext.EventData.Value)  
  
           Case Else  
              SqlContext.Pipe.Send("Something else happened! Here's the EventData:")  
              SqlContext.Pipe.Send(triggContext.EventData.Value)  
  
        End Select  
    End Sub  
End Class     

La siguiente salida de ejemplo es el EventData valor de propiedad después de que un evento desencadena CREATE TABLE un desencadenador DDL:

<EVENT_INSTANCE><PostTime>2004-04-16T21:17:16.160</PostTime><SPID>58</SPID><EventType>CREATE_TABLE</EventType><ServerName>MACHINENAME</ServerName><LoginName>MYDOMAIN\myname</LoginName><UserName>MYDOMAIN\myname</UserName><DatabaseName>AdventureWorks</DatabaseName><SchemaName>dbo</SchemaName><ObjectName>UserName</ObjectName><ObjectType>TABLE</ObjectType><TSQLCommand><SetOptions ANSI_NULLS="ON" ANSI_NULL_DEFAULT="ON" ANSI_PADDING="ON" QUOTED_IDENTIFIER="ON" ENCRYPTED="FALSE" /><CommandText>create table dbo.UserName ( UserName varchar(50), RealName varchar(50) ) </CommandText></TSQLCommand></EVENT_INSTANCE>

Además de la información accesible a través de la SqlTriggerContext clase , las consultas todavía pueden hacer referencia a COLUMNS_UPDATED y insertarse o eliminar dentro del texto de un comando ejecutado en proceso.

Desencadenador CLR de ejemplo

En este ejemplo, considere el escenario en el que se permite al usuario elegir cualquier identificador que desee, pero desea conocer los usuarios que especificaron específicamente una dirección de correo electrónico como identificador. El siguiente desencadenador detectaría esa información y la registraría en una tabla de auditoría.

Nota:

El envío de resultados y mensajes a través del SqlPipe objeto se muestra aquí solo con fines ilustrativos y, por lo general, se desaconseja para el código de producción. Los datos adicionales devueltos pueden ser inesperados y provocar errores de aplicación

using System;  
using System.Data;  
using System.Data.Sql;  
using Microsoft.SqlServer.Server;  
using System.Data.SqlClient;  
using System.Data.SqlTypes;  
using System.Xml;  
using System.Text.RegularExpressions;  
  
public class CLRTriggers  
{  
   [SqlTrigger(Name = @"EmailAudit", Target = "[dbo].[Users]", Event = "FOR INSERT, UPDATE, DELETE")]  
   public static void EmailAudit()  
   {  
      string userName;  
      string realName;  
      SqlCommand command;  
      SqlTriggerContext triggContext = SqlContext.TriggerContext;  
      SqlPipe pipe = SqlContext.Pipe;  
      SqlDataReader reader;  
  
      switch (triggContext.TriggerAction)  
      {  
         case TriggerAction.Insert:  
         // Retrieve the connection that the trigger is using  
         using (SqlConnection connection  
            = new SqlConnection(@"context connection=true"))  
         {  
            connection.Open();  
            command = new SqlCommand(@"SELECT * FROM INSERTED;",  
               connection);  
            reader = command.ExecuteReader();  
            reader.Read();  
            userName = (string)reader[0];  
            realName = (string)reader[1];  
            reader.Close();  
  
            if (IsValidEMailAddress(userName))  
            {  
               command = new SqlCommand(  
                  @"INSERT [dbo].[UserNameAudit] VALUES ('"  
                  + userName + @"', '" + realName + @"');",  
                  connection);  
               pipe.Send(command.CommandText);  
               command.ExecuteNonQuery();  
               pipe.Send("You inserted: " + userName);  
            }  
         }  
  
         break;  
  
         case TriggerAction.Update:  
         // Retrieve the connection that the trigger is using  
         using (SqlConnection connection  
            = new SqlConnection(@"context connection=true"))  
         {  
            connection.Open();  
            command = new SqlCommand(@"SELECT * FROM INSERTED;",  
               connection);  
            reader = command.ExecuteReader();  
            reader.Read();  
  
            userName = (string)reader[0];  
            realName = (string)reader[1];  
  
            pipe.Send(@"You updated: '" + userName + @"' - '"  
               + realName + @"'");  
  
            for (int columnNumber = 0; columnNumber < triggContext.ColumnCount; columnNumber++)  
            {  
               pipe.Send("Updated column "  
                  + reader.GetName(columnNumber) + "? "  
                  + triggContext.IsUpdatedColumn(columnNumber).ToString());  
            }  
  
            reader.Close();  
         }  
  
         break;  
  
         case TriggerAction.Delete:  
            using (SqlConnection connection  
               = new SqlConnection(@"context connection=true"))  
               {  
                  connection.Open();  
                  command = new SqlCommand(@"SELECT * FROM DELETED;",  
                     connection);  
                  reader = command.ExecuteReader();  
  
                  if (reader.HasRows)  
                  {  
                     pipe.Send(@"You deleted the following rows:");  
                     while (reader.Read())  
                     {  
                        pipe.Send(@"'" + reader.GetString(0)  
                        + @"', '" + reader.GetString(1) + @"'");  
                     }  
  
                     reader.Close();  
  
                     //alternately, to just send a tabular resultset back:  
                     //pipe.ExecuteAndSend(command);  
                  }  
                  else  
                  {  
                     pipe.Send("No rows affected.");  
                  }  
               }  
  
               break;  
            }  
        }  
  
     public static bool IsValidEMailAddress(string email)  
     {  
         return Regex.IsMatch(email, @"^([\w-]+\.)*?[\w-]+@[\w-]+\.([\w-]+\.)*?[\w]+$");  
     }  
}  

Visual Basic

Imports System  
Imports System.Data  
Imports System.Data.Sql  
Imports System.Data.SqlTypes  
Imports Microsoft.SqlServer.Server  
Imports System.Data.SqlClient  
Imports System.Text.RegularExpressions  
  
'The Partial modifier is only required on one class definition per project.  
Partial Public Class CLRTriggers   
  
    <SqlTrigger(Name:="EmailAudit", Target:="[dbo].[Users]", Event:="FOR INSERT, UPDATE, DELETE")> _  
    Public Shared Sub EmailAudit()  
        Dim userName As String  
        Dim realName As String  
        Dim command As SqlCommand  
        Dim triggContext As SqlTriggerContext  
        Dim pipe As SqlPipe  
        Dim reader As SqlDataReader    
  
        triggContext = SqlContext.TriggerContext      
        pipe = SqlContext.Pipe    
  
        Select Case triggContext.TriggerAction  
           Case TriggerAction.Insert  
              Using connection As New SqlConnection("context connection=true")  
                 connection.Open()  
                 command = new SqlCommand("SELECT * FROM INSERTED;", connection)  
  
                 reader = command.ExecuteReader()  
                 reader.Read()  
  
                 userName = CType(reader(0), String)  
                 realName = CType(reader(1), String)  
  
                 reader.Close()  
  
                 If IsValidEmailAddress(userName) Then  
                     command = New SqlCommand("INSERT [dbo].[UserNameAudit] VALUES ('" & _  
                       userName & "', '" & realName & "');", connection)  
  
                    pipe.Send(command.CommandText)  
                    command.ExecuteNonQuery()  
                    pipe.Send("You inserted: " & userName)  
  
                 End If  
              End Using  
  
           Case TriggerAction.Update  
              Using connection As New SqlConnection("context connection=true")  
                 connection.Open()  
                 command = new SqlCommand("SELECT * FROM INSERTED;", connection)  
  
                 reader = command.ExecuteReader()  
                 reader.Read()  
  
                 userName = CType(reader(0), String)  
                 realName = CType(reader(1), String)  
  
                 pipe.Send("You updated: " & userName & " - " & realName)  
  
                 Dim columnNumber As Integer  
  
                 For columnNumber=0 To triggContext.ColumnCount-1  
  
                    pipe.Send("Updated column " & reader.GetName(columnNumber) & _  
                      "? " & triggContext.IsUpdatedColumn(columnNumber).ToString() )  
  
                 Next  
  
                 reader.Close()  
              End Using  
  
           Case TriggerAction.Delete  
              Using connection As New SqlConnection("context connection=true")  
                 connection.Open()  
                 command = new SqlCommand("SELECT * FROM DELETED;", connection)  
  
                 reader = command.ExecuteReader()  
  
                 If reader.HasRows Then  
                    pipe.Send("You deleted the following rows:")  
  
                    While reader.Read()  
  
                       pipe.Send( reader.GetString(0) & ", " & reader.GetString(1) )  
  
                    End While   
  
                    reader.Close()  
  
                    ' Alternately, just send a tabular resultset back:  
                    ' pipe.ExecuteAndSend(command)  
  
                 Else  
                   pipe.Send("No rows affected.")  
                 End If  
  
              End Using   
        End Select  
    End Sub  
  
    Public Shared Function IsValidEMailAddress(emailAddress As String) As Boolean  
  
       return Regex.IsMatch(emailAddress, "^([\w-]+\.)*?[\w-]+@[\w-]+\.([\w-]+\.)*?[\w]+$")  
    End Function      
End Class  

Suponiendo que existen dos tablas con las siguientes definiciones:

CREATE TABLE Users  
(  
    UserName nvarchar(200) NOT NULL,  
    RealName nvarchar(200) NOT NULL  
);  
GO CREATE TABLE UserNameAudit  
(  
    UserName nvarchar(200) NOT NULL,  
    RealName nvarchar(200) NOT NULL  
)  

La instrucción Transact-SQL que crea el desencadenador en SQL Server es la siguiente y supone que el ensamblado SQLCLRTest ya está registrado en la base de datos de SQL Server actual.

CREATE TRIGGER EmailAudit  
ON Users  
FOR INSERT, UPDATE, DELETE  
AS  
EXTERNAL NAME SQLCLRTest.CLRTriggers.EmailAudit  

Validación y cancelación de transacciones no válidas

El uso de desencadenadores para validar y cancelar transacciones INSERT, UPDATE o DELETE no válidas o para evitar cambios en el esquema de la base de datos es común. Esto se puede lograr mediante la incorporación de lógica de validación en el desencadenador y, a continuación, revertir la transacción actual si la acción no cumple los criterios de validación.

Cuando se llama dentro de un desencadenador, el Transaction.Rollback método o sqlCommand con el texto de comando "TRANSACTION ROLLBACK" produce una excepción con un mensaje de error ambiguo y se debe encapsular en un bloque try/catch. El mensaje de error que ve es similar al siguiente:

Msg 6549, Level 16, State 1, Procedure trig_InsertValidator, Line 0  
A .NET Framework error occurred during execution of user defined routine or aggregate 'trig_InsertValidator':   
System.Data.SqlClient.SqlException: Transaction is not allowed to roll back inside a user defined routine, trigger or aggregate because the transaction is not started in that CLR level. Change application logic to enforce strict transaction nesting... User transaction, if any, will be rolled back.  

Se espera esta excepción y el bloque try/catch resulta necesario para que continúe la ejecución del código. Cuando el código del desencadenador finaliza la ejecución, se genera otra excepción.

Msg 3991, Level 16, State 1, Procedure trig_InsertValidator, Line 1   
The context transaction which was active before entering user defined routine, trigger or aggregate "trig_InsertValidator" has been ended inside of it, which is not allowed. Change application logic to enforce strict transaction nesting.  
The statement has been terminated.  

Esta excepción también se espera y es necesario un bloque try/catch alrededor de la instrucción Transact-SQL que realiza la acción que desencadena el desencadenador para que la ejecución pueda continuar. A pesar de las dos excepciones iniciadas, la transacción se revierte y los cambios no se confirman en la tabla. Una diferencia importante entre los desencadenadores CLR y los desencadenadores de Transact-SQL es que los desencadenadores de Transact-SQL pueden seguir funcionando más después de revertir la transacción.

Ejemplo

El siguiente desencadenador realiza una validación sencilla de instrucciones INSERT en una tabla. Si el valor entero insertado es igual a uno, la transacción se revierte y el valor no se inserta en la tabla. Todos los demás valores enteros se insertan en la tabla. Anote el bloque try/catch alrededor del Transaction.Rollback método . El script de Transact-SQL crea una tabla de prueba, un ensamblado y un procedimiento almacenado administrado. Tenga en cuenta que las dos instrucciones INSERT se encapsulan en un bloque try/catch para que se detecte la excepción cuando el desencadenador finaliza la ejecución.

C#

using System;  
using System.Data.SqlClient;  
using Microsoft.SqlServer.Server;  
using System.Transactions;  
  
public partial class Triggers  
{  
    // Enter existing table or view for the target and uncomment the attribute line  
    // [Microsoft.SqlServer.Server.SqlTrigger (Name="trig_InsertValidator", Target="Table1", Event="FOR INSERT")]  
    public static void trig_InsertValidator()  
    {  
        using (SqlConnection connection = new SqlConnection(@"context connection=true"))  
        {  
            SqlCommand command;  
            SqlDataReader reader;  
            int value;  
  
            // Open the connection.  
            connection.Open();  
  
            // Get the inserted value.  
            command = new SqlCommand(@"SELECT * FROM INSERTED", connection);  
            reader = command.ExecuteReader();  
            reader.Read();  
            value = (int)reader[0];  
            reader.Close();  
  
            // Rollback the transaction if a value of 1 was inserted.  
            if (1 == value)  
            {  
                try  
                {  
                    // Get the current transaction and roll it back.  
                    Transaction trans = Transaction.Current;  
                    trans.Rollback();                      
                }  
                catch (SqlException ex)  
                {  
                    // Catch the expected exception.                      
                }  
            }  
            else  
            {  
                // Perform other actions here.  
            }  
  
            // Close the connection.  
            connection.Close();              
        }  
    }  
}  

Visual Basic

Imports System  
Imports System.Data.SqlClient  
Imports System.Data.SqlTypes  
Imports Microsoft.SqlServer.Server  
Imports System.Transactions  
  
Partial Public Class Triggers  
' Enter existing table or view for the target and uncomment the attribute line  
' <Microsoft.SqlServer.Server.SqlTrigger(Name:="trig_InsertValidator", Target:="Table1", Event:="FOR INSERT")> _  
Public Shared Sub  trig_InsertValidator ()  
    Using connection As New SqlConnection("context connection=true")  
  
        Dim command As SqlCommand  
        Dim reader As SqlDataReader  
        Dim value As Integer  
  
        ' Open the connection.  
        connection.Open()  
  
        ' Get the inserted value.  
        command = New SqlCommand("SELECT * FROM INSERTED", connection)  
        reader = command.ExecuteReader()  
        reader.Read()  
        value = CType(reader(0), Integer)  
        reader.Close()  
  
        ' Rollback the transaction if a value of 1 was inserted.  
        If value = 1 Then  
  
            Try  
                ' Get the current transaction and roll it back.  
                Dim trans As Transaction  
                trans = Transaction.Current  
                trans.Rollback()  
  
            Catch ex As SqlException  
  
                ' Catch the exception.                      
            End Try  
        Else  
  
            ' Perform other actions here.  
        End If  
  
        ' Close the connection.  
        connection.Close()  
    End Using  
End Sub  
End Class  

Transact-SQL

-- Create the test table, assembly, and trigger.  
CREATE TABLE Table1(c1 int);  
go  
  
CREATE ASSEMBLY ValidationTriggers from 'E:\programming\ ValidationTriggers.dll';  
go  
  
CREATE TRIGGER trig_InsertValidator  
ON Table1  
FOR INSERT  
AS EXTERNAL NAME ValidationTriggers.Triggers.trig_InsertValidator;  
go  
  
-- Use a Try/Catch block to catch the expected exception  
BEGIN TRY  
   INSERT INTO Table1 VALUES(42)  
   INSERT INTO Table1 VALUES(1)  
END TRY  
BEGIN CATCH  
  SELECT ERROR_NUMBER() AS ErrorNum, ERROR_MESSAGE() AS ErrorMessage  
END CATCH;  
  
-- Clean up.  
DROP TRIGGER trig_InsertValidator;  
DROP ASSEMBLY ValidationTriggers;  
DROP TABLE Table1;  

Véase también

CREATE TRIGGER (Transact-SQL)
Desencadenadores DML
Desencadenadores DDL
PROBAR... CATCH (Transact-SQL)
Compilación de objetos de base de datos con la integración de Common Language Runtime (CLR)
EVENTDATA (Transact-SQL)