microsoft press home   All Products  |   Support  |   Search  |   microsoft.com Home  
 
  Home  |   Register Books  |   Newsletter  |   Site Index  |   Book Lists  |

 

Advanced Search
    How to Buy
    Custom Books
    Worldwide Sites
    New Releases

  Books about:
    Microsoft .NET
    Windows
    Office
    Servers
    Developer Tools

  Books for:
    Certification
    IT Professional
    Developer
    Personal Desktop
    Educators

    Licensing & Sales

    Support


Developer Feature Article
SQL Server 2000: .NET and the Death and Re-Birth of Triggers
SQL Server 2000: .NET and the Death and Re-Birth of Triggers

 By Scott Mauvais

It seems everywhere you turn these days, everyone is talking about Microsoft's .NET initiative. Microsoft is certainly talking about it. The trade rags are taking about it. Every developer journal has articles about it. Last time I counted, there were just over independent 100 web sites devoted to it. Everyone is saying how great it will be.

Surprisingly, no one seems to be focusing on the fact that first part of the .NET platform has already shipped. On September 26, SQL Server 2000 became the first .NET Enterprise Server.

In this month's article I will cover an aspect of SQL Server that always generates a great deal of, well, shall we say "discussion"�triggers. Love them or hate them, SQL Server 2000 will change they way you think about triggers and will probably alter the way you design your applications. Specifically, this month I will dive into cascading referential integrity (RI) and the new INSTEAD OF and AFTER trigger concepts in SQL Server 2000.

To round out your understanding of this, the first .NET Enterprise Server, you should also take a look at Ian Matthews' companion article in the IT Professional section on Microsoft Press Online, SQL Server 2000 New Features and Enhancements, that covers the features important to the IT professional such as SQL Server 2000's new XML features, support for greater multiprocessing, ability to run multiple instances on one server machine, and new administrative tools.

Both of these articles focus exclusively on using the new features of SQL Server 2000. If you are looking for a comprehensive look at the inner working of SQL Server 2000, you need to get a copy of Ron Soukup's and Kalen Delaney's Inside Microsoft SQL Server 2000 from Microsoft Press. They just finished revising their best selling, Inside Microsoft SQL Server 7.0 to provide essential insight into the architectural changes in SQL Server 2000. I just finished reading an early version of it and I must say, it is truly outstanding. Even if you have the 7.0 edition (or Ron's 6.5 version) you should purchase a copy of the new release as soon as you can because it tells you why SQL Server 2000 is the way it is and shows you how to use it to its full potential.

Now that you know where you can find additional information on SQL Server 2000, let's get back to triggers. Before we dive into the detail, I need to come clean. In the spirit of full disclosure, you should know I hate triggers; I always have. They are tedious to write, difficult to debug, and when it comes right down to it, most developers only use them for cascading referential integrity. I have always been a bit annoyed that SQL Server didn't take care of this grunt work for me. Well, now it does!

Trigger-Be-Gone: Cascading Deletes and Updates
With the release of SQL Server 2000, Microsoft has finally provided developers with what is arguably the most requested feature and fixed SQL Server's most glaring deficiency: cascading referential integrity. In earlier versions of SQL Server, if you wanted to delete or update child records when you deleted or updated a master table, you had to choose between the lesser of two evils: Either you had to remember to manually update and delete child records before executing the same statements against the master tables, or you had to turn off declarative referential integrity and write the logic in triggers. This was bad enough with simple parent-child relationships, but got downright nasty when you added multiple levels in your hierarchy.

To ease things a bit, you could certainly wrap the T/SQL required for the manual approach into a stored procedure. This had the benefit of making sure the cascading actions were applied consistently (you no longer had to worry about a developer forgetting to include the necessary logic and corrupting the database), but it was often hard to ensure that your stored procedure was generic enough to address all the scenarios your application might require.

The obvious solution would seem to be triggers. They allow you to automatically take some action every time the base table changes. The problem with using triggers, however, is that SQL Server enforces table constraints before firing triggers. As a result, if you wrote a trigger like the one in Listing 1 on a table that was the master (referenced) table of a foreign key relationship hoping that it would delete all the child records, the trigger would not work. Instead, SQL Server would return an error indicating that the "DELETE statement conflicted with COLUMN REFERENCE constraint," so the trigger would never fire.

Listing 1: Problem Trigger

CREATE TRIGGER 
tdCustomers
     ON Northwind.dbo.Customers
     FOR DELETE AS
BEGIN
     DELETE Orders
          FROM Customers, DELETED
          WHERE Customers.CustomerID = DELETED.CustomerID
END

The way around this, of course, was to drop the foreign key constraints on your tables and manage all the referential integrity in triggers. Simple triggers like the one above quickly grew into multi-page maintenance nightmares. What a drag!

This is all fixed in SQL Server 2000. Now, with just two clicks of the mouse from the table properties dialog, as shown in Figure 1, you can have SQL Server take care of synchronizing your master and child tables.

Figure 1: Adding cascading updates and deletes to the Customers table.
Figure 1: Adding cascading updates and deletes to the Customers table.

Once you have enabled cascading updates and deletes, when you delete a customer, SQL Server will automatically remove all the orders associated with that customer. No fuss, no muss.

Beside the obvious benefit of reducing the tedious code we developers need to write and enabling us to focus our time and energy on more interesting tasks, cascading updates and deletes has the added benefit of increasing the performance of our applications. Remember, all that T/SQL we built into the triggers had to be parsed, optimized, and executed at run time. Now, these cascading actions are built into the table definitions and occur inside the SQL Server engine.

For a very interesting discussion of the interaction among constraints, cascading actions, and the SQL Server 2000 system tables, see Chapter 6 of Inside Microsoft SQL Server 2000 (0-7356-0998-5).

When Microsoft first announced that SQL Server 2000 would finally include cascading referential integrity, I commented that this would be the end of triggers; I called it the trigger-be-gone feature. It turns out I was wrong. While it will be the end of triggers as we currently use them, we will now start to use triggers in a completely different manner, because the SQL Server 2000 development team added two new types of triggers to our arsenal of development tools.

Trigger Enhancements
In SQL Server 7.0 Microsoft added the ability to define multiple triggers per table. This was a boon to those developers working on team projects, because now multiple developers could work on their own triggers without having to worry about overwriting other people's work. Microsoft has extended this model with SQL Server 2000 by adding INSTEAD OF and AFTER triggers.

Unlike triggers in SQL Server 7.0 and the AFTER triggers in SQL Server 2000, an object can only have a single INSTEAD OF trigger. Another important difference is that an INSTEAD OF trigger fires instead of the triggering event. In other words, only the trigger executes; the original statement does not. INSTEAD OF triggers have two main uses:

  • Inserting data into views that would otherwise not be updatable
  • Pre-processing the results of statements and taking specific actions depending on the results

Let's look at examples of these two cases, starting off with adding new rows to a view.

Using INSTEAD OF Triggers to Update a Non-Updateable View
For this example, I created a very simple database for tracking temperatures. It has two tables: a header table listing locations and a detail table tracking the temperature readings at those locations. The text in Listing 2 contains a simplified version of SQL statements; if you want, you can download the complete version from my Web site. The two tables are defined as follows:

Listing 2: Creating the Sample Tables

CREATE 
TABLE tblTemperature (
     TemperatureID  INT IDENTITY(1, 1)  NOT NULL,
     LocationID     INT                 NOT NULL,
     DateTaken      DATETIME            NOT NULL,
     Celsius        INT                 NOT NULL

CREATE TABLE tblLocation (
     LocationID     INT IDENTITY(1, 1)  NOT NULL,
     LocationName   VARCHAR(20)         NOT NULL)

Finally, I created a view in Listing 3 to display the temperature readings in both Celsius and Fahrenheit.

Listing 3: View to Display Temperature Readings

CREATE VIEW qryFahrenheit
AS
     SELECT TemperatureID, LocationName, Celsius,
            Fahrenheit  = (32 + (Celsius * 9/5)),
            DateTaken
          FROM tblLocation    L,
               tblTemperature T
          WHERE L.LocationID = T.LocationID

Now, while you can issue INSERT and UPDATE statements against views, doing so is subject to a complex set of rules and one of them is that you the view cannot contain any derived columns in the select list. Therefore, if I want to add rows to my view, I cannot do so directly because the view contains the calculated Fahrenheit column. In earlier versions of SQL Server, this view would have been nonupdateable. To add rows to it, I would have to write directly into the base tables rather than through the view. This complicates my application because I would have to know the layout of the base tables. Sort of defeats the purpose of a view, doesn't it?

With SQL Server 2000, you can hide this complexity from your developers by creating and INSTEAD OF trigger like Listing 4:

Listing 4: A Sample INSTEAD OF Trigger

CREATE TRIGGER tiiFahrenheit ON qryFahrenheit 

     INSTEAD OF INSERT
AS
BEGIN

INSERT INTO tblTemperature
          (LocationID, Celsius, DateTaken)
     SELECT LocationID, Celsius, DateTaken
          FROM tblLocation L,
               INSERTED    I
          WHERE L.LocationName = I.LocationName
END

The first thing you will notice is that we can now create triggers on views. Other than that, it is a normal trigger. We are simply taking the values out of the special INSERTED table and adding them to the base table, tblTemperature. I am able to hide the physical layout of my base tables from the developer. No matter how many tables are included in the view and no matter how complex the relationships, all the developer needs to do is insert a row into the single view. From there, I can distribute the data to any table I like.

To add a row to the view, you would use an INSERT statement like this:

INSERT INTO qryFahrenheit 
     (TemperatureID, LocationName, Celsius, DateTaken)
     VALUES
     (0,  'Pacifica', 23, CAST ('9/26/2000' AS DATETIME))
Note: For the time being, assume that "Pacifica" already exists in Location table. In the next section, we drop this assumption.

The potentially confusing thing about this statement is that you must provide values for every column in your view that does not allow null values. In other words, if the column in the base table has NOT NULL as an attribute, you must provide a value in your INSERT statement. This even applies to IDENTITY columns. Luckily, the query processor makes this easy for you so you don't have to deal with SET IDENTITY_INSERT statements. Rather, you can just pass a placeholder value�0 in my example�and SQL Server will take care of the rest.

Using INSTEAD OF Triggers to Correct Constraint Violations
Now that we have seen how you can use INSTEAD OF to update otherwise non-updateable views, let's see how you could use them to pre-process the results of your statements.

As I mentioned in the earlier section on cascading updates and deletes, one of the main drawbacks of traditional triggers is that you cannot use them to recover from constraint violations, because SQL Server's query processor enforces constraints before firing triggers. Therefore, if your statement violates a constraint, your trigger will never fire. INSTEAD OF triggers, on the other hand, fire before SQL Server checks the constraints. Thus, you can write your INSTEAD OF triggers to anticipate constraint violations and attempt to fix them.

Continuing with our simple temperature database, let's add primary keys as shown in Listing 5 to both tables and then establish a foreign key relationship between tblLocation and tblTemperature.

Listing 5: Adding Primary Keys and a Foreign Key Relationship

ALTER TABLE tblLocation 
     ADD CONSTRAINT pkLocation
     PRIMARY KEY (LocationID)

ALTER TABLE tblTemperature
     ADD CONSTRAINT pkTemperature
     PRIMARY KEY (TemperatureID)

ALTER TABLE tblTemperature
     ADD CONSTRAINT fkTemperature_Location
          FOREIGN KEY (LocationID)
          REFERENCES tblLocation (LocationID)

Let's use the same INSERT statement again, but this time let's clear out both tables first:

DELETE FROM tblTemperature
DELETE FROM tblLocation

INSERT INTO qryFahrenheit
     (TemperatureID, LocationName, Celsius, DateTaken)
     VALUES
     (0,  'Pacifica', 23, CAST ('9/26/2000' AS DATETIME))

If you execute this batch as is, it will fail to insert any rows because your INSTEAD OF trigger is trying join the LocationName of "Pacifica" in the special INSERTED table to the empty table tblLocation. Obviously, it will not return any rows.

Initially, you may be tempted to give up on inserting data into the view and attempt to write directly into the base Temperature table. This, however, won't work either; if you try, you will receive an error due to the foreign key constraint. To solve this dilemma, you need to alter your INSTEAD OF trigger as shown in Listing 6 to insert any necessary rows into the Location table before attempting to add data to the Fahrenheit view.

Listing 6: The Altered INSTEAD OF Trigger

ALTER TRIGGER tiiFahrenheit ON qryFahrenheit 

     INSTEAD OF INSERT
AS
BEGIN

-- Insert any new locations

INSERT INTO tblLocation 
(LocationName)
     SELECT DISTINCT I.LocationName
          FROM INSERTED I
          WHERE I.LocationName
                NOT IN (SELECT LocationName FROM tblLocation)

-- Insert temperature readings

INSERT INTO tblTemperature 

          (LocationID, Celsius, DateTaken)
     SELECT LocationID, Celsius, DateTaken
          FROM tblLocation L,
                  INSERTED    I
          WHERE L.LocationName = I.LocationName
END

Now your trigger handles all the logic, allowing your developers to insert into the Fahrenheit view without having to worry about the layout of the underlying tables. The next step would be to create update and delete INSTEAD OF triggers. That is, as they say, an exercise that will be left to the reader.

AFTER Triggers
Before wrapping up, I want to take a quick look at AFTER triggers. After triggers are very easy to understand, because they are simply a replacement of the standard FOR triggers in earlier versions of SQL Server. The syntax used in FOR triggers is still maintained for compatibility.

Unlike INSTEAD OF triggers, you may only create AFTER triggers on tables; you cannot create an AFTER trigger on a view. You would use the following syntax to create a simple AFTER trigger on our tblLocation table to prevent deletions of the Pacifica row.

CREATE TRIGGER tdaLocation ON tblLocation
AFTER DELETE
AS
BEGIN
     SELECT * FROM DELETED WHERE LocationName = 'Pacifica'
     IF @@RowCount <> 0 ROLLBACK TRANSACTION
END

Controlling Trigger Firing
The final new feature we will look at is the ability to control the order in which triggers fire. In earlier versions of SQL Server, triggers fired in an indeterminate order. This meant that if you had several triggers on a table, you had no way of knowing, let alone controlling, in which order they would fire. Furthermore, the firing order might vary from one execution to another.

In SQL Server 2000 we are now able to specify which trigger will fire first and which will fire last. Any other triggers (the �middle� ones) will still fire in an indeterminate order. To specify the order, you use the following syntax:

sp_settriggerorder @triggername = 'tdaLocation', 

    @order       = 'first',
    @stmttype    = 'DELETE'
Or,

sp_settriggerorder @triggername = 'tdaLocation',
    @order       = 'last',
                   @stmttype    = 'DELETE'

Here, @triggername specifies the sysname of the trigger, @order is either FIRST or LAST (cannot have the same trigger be both the first and last trigger), and @stmttype is INSERT, UPDATE, or DELETE.

The firing order of triggers gets a little complex, so I have summarized some of the more critical information in Table 1 below. For simplicity's sake, I will separate FOR and AFTER triggers even though SQL Server processes them the same way. For a full explanation, see the SQL Server 2000 Books On Line.

Table 1: Trigger Firing Behavior

Table 1: Trigger Firing Behavior

To learn more about triggers in SQL Server 2000 and how you can design your applications to make the best use of them, see Chapter 9 in Ron and Kalen's Inside Microsoft SQL Server 2000 (0-7356-0998-5).

Conclusion
With all the futuristic talk surrounding .NET, you would think that it would not be available for many months. Well, the best part of Microsoft's .NET strategy is that several parts of it are available today. The release of SQL Server 2000 marks the beginning of the transition to the .NET platform from Windows DNA 2000. Over the next few months Microsoft will release the rest of the .NET Enterprise Server family, including Commerce Server 2000 and BizTalk Server 2000.You can also download the .NET SDK from the MSDN .NET Resource Center and get a head start developing with Microsoft's next generation toolset.

In this article, I have shown you how you can use the new cascading referential integrity and trigger features in SQL Server 2000 to increase the performance of your applications and give you greater flexibility as you design you applications. The new cascading updates and deletes will speed up your applications and release you from the drudgery of writing pages of T/SQL code. You can use the INSTEAD OF trigger to handle constraint violations proactively. Finally, sp_SetTriggerOrder enables you to control the order in which your AFTER triggers will fire.

Microsoft Press Solutions
Microsoft Press provides in-depth documentation for these and all the other issues related to developing and administering SQL Server applications. The best place to start is with Ron Soukup and Kalen Delaney's new book Inside Microsoft SQL Server 2000.

Even if you plan to purchase the SQL Server 2000 version the very day it is published, you should also get a copy Inside Microsoft SQL Server 7.0 if you plan on supporting any 7.0 applications. There are enough differences between the two products to make both texts essential for the serious SQL Server developer.

If you simply want to get a quick overview of the new features in SQL Server 2000, take a look at this article on the in the SQL Server section of Microsoft.com: What's New in SQL Server 2000.

If you are after a comprehensive look at the new features along with lots of sample code, check out Carl Nolan's excellent MSDN article SQL Server 2000: New Features Provide Unmatched Ease of Use and Scalability to Admins and Users.

Other titles from Microsoft Press you might want to check out include the following.

You'll find a complete list of all Microsoft Press SQL Server 2000 resources on Microsoft Press Online. For detailed information on all Microsoft Press's Windows 2000 titles for developers, visit the Windows 2000 for Developers page. For a complete list of all the developer titles, see the Developer Tools section.