15
Creating Triggers in SQL Server 2005 One of the excellent features provided by SQL Server 2005 (code named Yukon) is its integration with the .NET CLR which makes it possible for us to author triggers, stored procedures, user defined functions, and create other database objects using a managed language such as VB.NET, C#, and so on. This approach provides a number of benefits such as increased productivity, significant performance gains and the ability to leverage the features of .NET Code Access Security to prevent assemblies from performing certain operations and so on. In this article, we will take a look at this new CLR integration feature and learn how to create triggers in SQL Server using a managed language. Along the way, we will also learn how the features of .NET code access security can be leveraged to better control the assembly execution environment. Finally, we will discuss when to use T-SQL and when to use a .NET language when creating SQL Server triggers. .NET CLR and SQL Server Integration In previous versions of SQL Server, database programmers were limited to using Transact-SQL when creating server side objects such as triggers, stored procedures, and user defined functions and so on. But now with the integration of SQL Server with .NET CLR, it opens up a whole avenue of opportunities. Before we talk about the features of .NET CLR integration with SQL Server, let us understand the limitations of T-SQL when it comes to creating server side objects. Transact-SQL (T-SQL) is an extension of the Structured Query Language as defined by the International Standards Organization (ISO) and the American National Standards Institute (ANSI). Using T-SQL, database developers can create, modify and delete databases and tables, as well as insert, retrieve, modify and delete data stored in a database. T-SQL is specifically designed for direct data access and manipulation. While T-SQL can be very useful for data access and management, it is not a full-fledged programming language in the way that Visual Basic .NET and C# are. For example, T-SQL does not support arrays, strongly typed objects, collections, for each loops, bit shifting or classes and so on. While some of these constructs can be simulated in T-SQL, managed code based languages such as VB.NET or C# have first-class support for these constructs. Now that we have understood the limitations of T-SQL, let us talk about the advantages of .NET CLR integration with SQL Server. With CLR integration, things have changed dramatically. The CLR provides the execution environment for all the server side objects that are created using a .NET language. This means the database developers can now perform tasks that were impossible or difficult to achieve with T- SQL alone. Especially when working with large amounts of server code, developers can easily organize and maintain your code investments. By allowing the code to run under the control of .NET CLR, you can also

Creating Triggers in SQL Server 2005

Embed Size (px)

Citation preview

Page 1: Creating Triggers in SQL Server 2005

Creating Triggers in SQL Server 2005

One of the excellent features provided by SQL Server 2005 (code named Yukon) is its integration with the .NET CLR which makes it possible for us to author triggers, stored procedures, user defined functions, and create other database objects using a managed language such as VB.NET, C#, and so on. This approach provides a number of benefits such as increased productivity, significant performance gains and the ability to leverage the features of .NET Code Access Security to prevent assemblies from performing certain operations and so on. In this article, we will take a look at this new CLR integration feature and learn how to create triggers in SQL Server using a managed language. Along the way, we will also learn how the features of .NET code access security can be leveraged to better control the assembly execution environment. Finally, we will discuss when to use T-SQL and when to use a .NET language when creating SQL Server triggers.

.NET CLR and SQL Server Integration

In previous versions of SQL Server, database programmers were limited to using Transact-SQL when creating server side objects such as triggers, stored procedures, and user defined functions and so on. But now with the integration of SQL Server with .NET CLR, it opens up a whole avenue of opportunities. Before we talk about the features of .NET CLR integration with SQL Server, let us understand the limitations of T-SQL when it comes to creating server side objects.

Transact-SQL (T-SQL) is an extension of the Structured Query Language as defined by the International Standards Organization (ISO) and the American National Standards Institute (ANSI). Using T-SQL, database developers can create, modify and delete databases and tables, as well as insert, retrieve, modify and delete data stored in a database. T-SQL is specifically designed for direct data access and manipulation. While T-SQL can be very useful for data access and management, it is not a full-fledged programming language in the way that Visual Basic .NET and C# are. For example, T-SQL does not support arrays, strongly typed objects, collections, for each loops, bit shifting or classes and so on. While some of these constructs can be simulated in T-SQL, managed code based languages such as VB.NET or C# have first-class support for these constructs. Now that we have understood the limitations of T-SQL, let us talk about the advantages of .NET CLR integration with SQL Server.

With CLR integration, things have changed dramatically. The CLR provides the execution environment for all the server side objects that are created using a .NET language. This means the database developers can now perform tasks that were impossible or difficult to achieve with T-SQL alone. Especially when working with large amounts of server code, developers can easily organize and maintain your code investments. By allowing the code to run under the control of .NET CLR, you can also leverage the code access security features of .NET. For example, before executing code, the CLR can check to see if the code is safe. This process is known as "verification." During verification, the CLR performs several checks to ensure that the code is safe to run. For example, the code is checked to ensure that no memory is read that has not be been written to. The CLR will also prevent buffer overflows. Now that we have had a complete overview of the .NET CLR integration, let us understand the steps to be followed for creating a trigger in VB.NET.

Creating a Trigger using Managed Code in SQL Server

As you may know, triggers are executed as the result of a user action against a table, such as an INSERT, UPDATE or DELETE statement. To create a trigger using a managed language such as C# or VB.NET, you need to go through the following steps.

Create a .NET class and implement the functionality of the extended trigger within that class.

Compile that class to produce a .NET assembly.

Page 2: Creating Triggers in SQL Server 2005

Register that assembly in SQL Server using the Create Assembly statement. Create trigger definitions. As part of this, you also associate the trigger with the actual

methods in the assembly. Once this is done, the triggers are configured and can be invoked automatically like any other triggers.

In the next section, we will take an in-depth look at the above steps and understand what it takes to create a trigger using C#.

Implementation of .NET class that will act as an extended Trigger

Before we go onto creating the .NET class that will implement the functionalities of the trigger, let us create a simple table named Users using the following DDL statement.

CREATE TABLE Users(UserName Varchar (100) )

Now that we have created the table, let us create the C# class that implements the functionalities required of the trigger. Towards this end, we will create a C# class named Users and modify its code to look like the following code.

using System.Data;using System.Data.Sql;using System.Data.SqlServer;public class Users{ public static void InsertTrigger() { SqlTriggerContext triggerContext = SqlContext.GetTriggerContext(); SqlPipe sqlPipe = SqlContext.GetPipe(); SqlCommand command = SqlContext.GetCommand(); if (triggerContext.TriggerAction == System.Data.Sql.TriggerAction.Insert) { command.CommandText = "SELECT * FROM INSERTED"; sqlPipe.Execute(command); } }}

Let us walk through the above code. To execute .NET code in SQL server, you need to reference the System.Data.Sql and System.Data.SqlServer namespaces. Then we declare a class named Users that is mainly used to implement the trigger functionalities for the Users table. Then we get reference to the current trigger context by invoking the GetTriggerContext method of the SqlContext class. The SqlTriggerContext object enables the code to access the virtual table that's created during the execution of the trigger. This virtual table stores the data that caused the trigger to fire. The SqlPipe object enables the extended trigger to communicate with the external caller. To get reference to the SqlPipe object, we invoke the GetPipe method of the SqlContext class. Once we have reference to the SqlPipe object, we can then return tabular results and messages to the client. In this example, the SqlTriggerContext object is used to first determine if the trigger action was an insert operation. If so, then the contents of the virtual trigger table are retrieved and sent to the caller.

Compilation of .NET class to create a .NET Assembly

Page 3: Creating Triggers in SQL Server 2005

Now that we have created the C# class, let us compile that class to produce a .NET assembly, which can then be registered with SQL Server. The following screenshot shows how to compile the C# class to produce the .NET assembly.

As shown in above screenshot, we need to reference the sqlaccess.dll since our code uses the classes contained in the System.Data.SqlServer namespaces.

Registering the assembly in SQL Server

When writing managed code, the deployment unit is called an assembly. An assembly is packaged as a DLL or executable (EXE) file. While an executable can run on its own, a DLL must be hosted in an existing application. Managed DLL assemblies can be loaded into and hosted by Microsoft SQL Server. To load an assembly into SQL Server, you need to use the Create Assembly statement.

CREATE ASSEMBLY UsersFROM 'C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Binn\Test\CLRProcedures\CS\Users.dll'

The FROM clause specifies the pathname of the assembly to load. This path can either be a UNC path or a physical file path that is local to the machine. The above statement will register the assembly with the SQL Server. Note that the assembly name should be unique within a database. Once you load the assembly, a copy of the assembly is loaded into SQL Server. After that, if you want to make changes to the assembly, you need to drop the assembly first and then reregister that with SQL Server again. To drop an assembly from SQL Server, you need to use the Drop Assembly statement. For example, to drop the assembly that we created earlier, we need to use the following command.

DROP ASSEMBLY Users

Loading an assembly into Microsoft SQL Server is the first step in exposing the functionality that the assembly provides. Now that we have loaded the assembly, the next step is to associate an extended trigger to a specific method of the class that is contained in the assembly.

Creating Trigger Definitions

In this step, we will create an extended trigger using the Create Trigger statement. SQL Server 2005 supports a new clause named External Name that allows you to reference a method in the registered assembly. By referencing this method, we hook the trigger to that method in the assembly.

Page 4: Creating Triggers in SQL Server 2005

CREATE TRIGGER InsertTriggerON UsersFOR INSERTASEXTERNAL NAME Users:[Users]::InsertTrigger

For the purposes of this example, we will use the InsertTrigger method in the Users class. In the above code, External Name clause uses the following syntax.

[Name of the Assembly]:[Name of the Class]::[Name of the Method]

Now that the trigger is created, let us test the trigger by using the following Insert statement that inserts a row into the Users table.

Insert Users Values('TestUser')

You will see the output as shown in the following screenshot.

So far, we have seen the steps involved in creating the trigger and executing that trigger from SQL Server Workbench. Now let us demonstrate how to execute the same Insert statement from a C# Windows forms application and get the resultset returned by the trigger and then display it in the screen.

Creating a Windows Forms Client Application that executes the SQL Statement

Page 5: Creating Triggers in SQL Server 2005

Now we will create a simple Windows Forms to execute the insert SQL statement that we used in the previous step. To this end, let us create a new Visual C# Windows Forms application. After the project is created, open up the design view of the Form1. To the form, add a command button and name it as btnInsertUser. Then modify the Click event of the command button to look like the code shown below.

private void btnInsertUser_Click(object sender, System.EventArgs e){ string connString = "server=localhost;uid=sa;pwd=thiru;database=Test;"; using (SqlConnection conn = new SqlConnection(connString)) { string sql = "Insert into Users Values ('TestUser')"; SqlDataAdapter adapter = new SqlDataAdapter(sql,conn); adapter.SelectCommand.CommandType = CommandType.Text; DataSet insertTriggerDataSet = new DataSet(); adapter.Fill(insertTriggerDataSet); grdUsers.DataSource = insertTriggerDataSet.Tables[0].DefaultView; }}

The above code is very simple and straightforward. We simply execute the Insert statement and that will invoke automatically invoke the trigger. To start with, we declare a variable named connString and assign the connection string to the database to that variable. Then we create a new SqlConnection object passing in the connection string as an argument to its constructor. Then we create a SqlDataAdapter object and supply the sql statement and the previously created SqlConnection object as its arguments. Finally, we execute the sql statement by invoking the Fill method of the SqlDataAdapter object. Once we get the results in the form of a DataSet object, we then bind the results of the DataSet to a DataGrid control. If you run the application and click on the command button, you will get an output as similar to the following.

Page 6: Creating Triggers in SQL Server 2005

In the above screen, when you click on the Insert Users hyperlink, it will not only execute the sql statement, but will also invoke the trigger and the results of the trigger execution are sent to the client application, which are then displayed in the data grid control.

Advanced Operations in an Extended Trigger

So far, we have seen the steps involved in creating an extended trigger and indirectly invoking that from a client application. In this section, we will enhance our previous example by adding the following two capabilities.

Validate the email address specified by the users. If the email address is invalid, we will then send out confirmation email to those users.

For this example, let us create a new class named UsersValidation and add the following lines of code to it.

using System.Data;using System.Data.Sql;using System.Data.SqlServer;using System.Web.Mail;using System.Text.RegularExpressions;

public class UsersValidation{ public static void InsertTrigger() { SqlTriggerContext triggerContext = SqlContext.GetTriggerContext(); SqlPipe sqlPipe = SqlContext.GetPipe(); SqlCommand command = SqlContext.GetCommand(); if (triggerContext.TriggerAction == System.Data.Sql.TriggerAction.Insert) { command.CommandText = "SELECT * FROM INSERTED"; SqlDataRecord record = command.ExecuteRow(); string userName = (string)record[0]; sqlPipe.Execute(command); if (CheckEMailAddress(userName)) { MailMessage mail = new MailMessage(); mail.From = "[email protected]"; mail.To = userName; mail.Body = "Your Registration has been successfully recieved"; mail.BodyFormat = MailFormat.Html; //Send the mail SmtpMail.Send(mail); } else throw new System.Exception ("Invalid user name"); } }

public static bool CheckEMailAddress(string email) { return Regex.IsMatch(email, @"([\w-]+\.)*?[\w-]+@[\w-]+\.([\w-]+\.)*?[\w]+$"); }

Page 7: Creating Triggers in SQL Server 2005

}

As you can see from the above code, it is very similar to our previous example. It simply get the supplied user name from the Inserted table by executing the ExecuteRow method of the SqlCommand object. It first stores the results of the statement execution in an SqlDataRecord object and then moves the results into a local variable named username. After that, the variable is supplied to a static function named CheckEMailAddress, which validates the supplied user name. If the user name is a valid email address, the code then sends an email to that user providing a confirmation of the registration. Otherwise it simply throws an exception to the caller by creating an exception object and throwing it back.

Now that we have created the class, let us compile it as shown in the following screenshot.

Once the C# class is compiled into a .NET assembly, let us register it in the SQL Server using the Create Assembly statement.

CREATE ASSEMBLY UsersValidationFROM 'C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Binn\Test\CLRProcedures\CS\UsersValidation.dll'WITH PERMISSION_SET = UNSAFE

Note that in the above code, we added a new clause named WITH PERMISSION_SET to the Create Assembly statement. The PERMISSION_SET clause allows you to specify the level of security in which your code will be executed.

When loading an assembly into SQL Server, you can specify any one of the following 3 values for PERMISSION_SET:

SAFE - It is the default permission set and with this mode, the assembly can only do computation and data access within the server via the in-process managed provider.

EXTERNAL_ACCESS - This permission set is typically useful in scenarios where the code needs to access resources outside the server such as files, network, registry and environment variables. Whenever the server accesses an external resource, it impersonates the security context of the user calling the managed code.

UNSAFE - It is used in situations where an assembly is not verifiably safe or requires additional access to restricted resources, such as the Win32 API.

In our example, since we want to send out email from within the trigger, we set the PERMISSION_SET to UNSAFE. Now that we have created the assembly, let us create the trigger definition using the Create Trigger statement.

Page 8: Creating Triggers in SQL Server 2005

CREATE TRIGGER InsertTriggerON UsersFOR INSERTASEXTERNAL NAME UsersValidation:[UsersValidation]::InsertTrigger

Now that the trigger is created, let us test the trigger by using the following Insert statement that inserts a row into the Users table.

Insert Users Values('[email protected]')

The above sql statement will not only insert the data into the Users table, but will also trigger an email to be sent to [email protected].

Creating DDL Triggers in Managed Code

Another new and excellent feature of SQL Server 2005 is that it allows us to hook triggers on to DDL constructs as well. This means you can write logic in a trigger that will be executed when someone performs DDL operations (such as CREATE TABLE, and ALTER TABLE) in your database. However one issue with this approach is that how do we know what happened when a DDL trigger fires - after all we won't get any entries in the INSERTED and DELETED tables. So somehow the database engine needs to deliver the trigger reason (or context) to the trigger. It does this by making the data accessible in the form of an XML document. Let us create a simple example trigger to illustrate this. For the purposes of this example, let us create a new C# class named TableTrigger and modify the class to look like the following.

using System.Data;using System.Data.Sql;using System.Data.SqlServer;using System.Xml;using System.IO;using System.Diagnostics;

public class TableTrigger{ public static void AddTable() { SqlTriggerContext ctx = SqlContext.GetTriggerContext(); XmlDocument doc = new XmlDocument(); if (ctx.TriggerAction == TriggerAction.CreateTable) { string s = new string(ctx.EventData.Value); StringReader r = new StringReader(s); XmlReader reader = new XmlTextReader(r); doc.Load(reader); reader.Dispose(); EventLog evt = new EventLog("Application", ".", "Create Table Audit"); evt.WriteEntry(string.Format("XML {0} created in ", doc.OuterXml)); } }}

Page 9: Creating Triggers in SQL Server 2005

In the above code, we utilize the SqlTriggerContext object to find out if the trigger is invoked by someone executing the CREATE TABLE statement. If that is the case, we then use the EventData property of the SqlTriggerContext to get reference to the actual XML that was created. Then we first load that information into a StringReader object and then onto a XmlReader object. Finally, we extract the content of the XmlDocument object and write that information onto the Application event log.

Now that the C# class is created, let us compile that class into a .NET assembly. Once the assembly is created, we can register the assembly into SQL Server using the following statement.

CREATE ASSEMBLY TableTriggerFROM 'C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Binn\Test\CLRProcedures\CS\TableTrigger.dll'WITH PERMISSION_SET =UNSAFE

Now create the trigger definition and associate the trigger with the AddTable method in the TableTrigger class using the following statement.

CREATE TRIGGER TableTriggerON DATABASE FOR CREATE_TABLEAS EXTERNAL NAME TableTrigger:TableTrigger::AddTable

Now that we have completed all the steps, let us test the trigger by executing the following CREATE TABLE statement.

CREATE TABLE Test(TestColumn Varchar(100) )

When you execute the above statement, you will find that the trigger caused an entry to be recorded in the application event log.

Page 10: Creating Triggers in SQL Server 2005

If you take a closer look at the above entry, you will find that the create table statement has resulted in the following the XML document.

<EVENT_INSTANCE> <PostTime>2004-07-14T23:11:18.517</PostTime> <SPID>55</SPID> <EventType>CREATE_TABLE</EventType> <ServerName>THIRU-SERVER1</ServerName> <LoginName>sa</LoginName> <UserName>sa</UserName> <DatabaseName>Test</DatabaseName> <SchemaName>dbo</SchemaName> <ObjectName>XmlLog</ObjectName> <ObjectType>TABLE</ObjectType> <TSQLCommand> <SetOptions ANSI_NULLS="ON" ANSI_NULL_DEFAULT="ON" ANSI_PADDING="ON" QUOTED_IDENTIFIER="ON" ENCRYPTED="FALSE" /> <CommandText>create table XmlLog (TestColumn nvarchar)</CommandText> </TSQLCommand></EVENT_INSTANCE>

Once you have the information in the form of an XmlDocument, you can then parse that information and execute code based on that information.

Transact-SQL versus Managed Code

Page 11: Creating Triggers in SQL Server 2005

With the introduction of the ability to write server side objects using a .NET compliant language, developers are presented with one more way to write triggers. This flexibility also poses some important challenges in deciding when to use T-SQL and when to use the managed language. This is an important decision that the developers need to make while defining their application architectures. There is no straight answer to this question but it depends on the particular situation. In some situations, you'll want to use T-SQL; in other situations, you will want to use managed code. T-SQL is best used in situations where the code will mostly perform data access with little or no procedural logic. Managed code is best suited for CPU intensive computations and server side objects where complex logic needs to be created. Another reason you might want to consider using managed code is the ability to leverage the rich features and object model supported by the .NET Framework's Base Class Library. The location in which the code gets executed is also an important factor to consider. By writing server side objects such as triggers using a .NET language you can move some of your middle tier code to the server side objects. This will also allow you to take full advantage of the processing power of the database server. On the other hand, you may wish to avoid placing processor intensive tasks on your database server. Most client machines today are very powerful, and you may wish to take advantage of this processing power by placing as much code as possible on the client. To summarize,

Choose T-SQL for data-access tasks that contain little or no procedural code. Choose managed code when the task is computationally expensive and will be

performed often; as such, it will benefit from compiled code. Locate the managed code on the server side if the data returned to the client can be

substantially reduced by pre-processing it on the server. Locate the managed code on the client when the data returned is small or the client can

perform processing in an asynchronous manner without compromising the performance of the application.

Always conduct performance testing and compare the various options available. Use performance-monitoring tools to avoid overloading the server CPU; upgrade where

possible to higher-end processors.

Conclusion

The closer integration of the .NET CLR and the SQL Server database engine provide developers with a whole new range of choices when writing data-access code and manipulating data. Proper use of managed code and choosing the best-performing physical location allows database developers to write applications that perform and scale better. Access to the BCL and enhancements to the IDE will make developers more productive, allowing them to concentrate on implementing business-specific solutions. With all of these enhancements, writing database objects will become easier, allowing more developers to become familiar with database interaction.

Page 12: Creating Triggers in SQL Server 2005

DDL Trigger:

CREATE TRIGGER ddl_trig_loginAWON ALL SERVERFOR DDL_LOGIN_EVENTSAS PRINT 'Added trigger event to DDLServerTriggerData' INSERT INTO [AdventureWorks].[dbo].[dbo.DDLServerTriggerData] (DDLServerEvent) VALUES ( EVENTDATA())