Tải bản đầy đủ (.pdf) (71 trang)

Microsoft SQL Server 2000 Programming by Example phần 6 doc

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (375.26 KB, 71 trang )

Microsoft SQL Server 2000 Programming by Example

340

CREATE TRIGGER tr1_Customers
ON Customers
AFTER UPDATE
AS
Your code here
PRINT 'This is the tr1 trigger'
GO

CREATE TRIGGER tr2_Customers
ON Customers
AFTER UPDATE
AS
Your code here
PRINT 'This is the tr2 trigger'
GO

CREATE TRIGGER tr3_Customers
ON Customers
AFTER UPDATE
AS
Your code here
PRINT 'This is the tr3 trigger'
GO

Test the order of execution
By using a MOCK operation


UPDATE Customers
SET ContactName = ContactName
GO

Specify the tr3 trigger as first trigger to execute

EXEC sp_settriggerorder 'tr3_Customers', 'FIRST', 'UPDATE'

Specify the tr2 trigger as last trigger to
execute

EXEC sp_settriggerorder 'tr2_Customers', 'LAST', 'UPDATE'

Specify the tr1 trigger as any order to execute

EXEC sp_settriggerorder 'tr1_Customers', 'NONE', 'UPDATE'

GO
Test the order of execution
By using a MOCK operation

PRINT CHAR(10) + 'After reordering'+ CHAR(10)

UPDATE Customers
SET ContactName = ContactName
Go
Chapter 9. Implementing Complex Processing Logic: Programming Triggers
341



This is the tr1 trigger
This is the tr2 trigger
This is the tr3 trigger

After reordering

This is the tr3 trigger
This is the tr1 trigger
This is the tr2 trigger

Caution
Remember that INSTEAD OF triggers are always executed before the data is modified. Therefore,
they execute before any of the AFTER triggers.

Checking for Updates on Specific Columns
To check inside a trigger if a column has been updated, you can use the IF UPDATE() clause. This clause
evaluates to TRUE if the column has been updated.
To test for changes in multiple columns in a single statement, use the COLUMNS_UPDATED() function. This
function returns a bitmap with the update status of every column in the base table. In other words,
COLUMNS_UPDATED returns a sequence of bits, one bit for every column, and the bit is 1 if the column has
been updated or otherwise it is 0.
Listing 9.15 shows an example of these two functions.
Listing 9.15 Inside a Trigger You Can Check Which Columns Have Been Updated


CREATE TRIGGER tr_OrderDetails
ON [Order Details]
AFTER UPDATE
AS
Testing for changes to the PRIMARY KEY

IF UPDATE(OrderID)
BEGIN
PRINT 'Changes to the PRIMARY KEY are not allowed'
ROLLBACK TRAN
Microsoft SQL Server 2000 Programming by Example

342
END

Testing for changes on the 2nd, 3rd and 5th columns
IF ((COLUMNS_UPDATED() & (2 + 4 + 8)) > 0)
BEGIN
IF ((COLUMNS_UPDATED() & 2) = 2)
PRINT 'ProductID updated'
IF ((COLUMNS_UPDATED() & 4) = 4)
PRINT 'UnitPrice updated'
IF ((COLUMNS_UPDATED() & 8) = 8)
PRINT 'Quantity updated'
END
GO

PRINT CHAR(10) + 'Updating ProductID and UnitPrice'

UPDATE [Order Details]
SET ProductID = ProductID,
UnitPrice = UnitPrice

PRINT CHAR(10) + 'Updating Quantity only'

UPDATE [Order Details]

SET Quantity = Quantity

PRINT CHAR(10) + 'Updating OrderID'

UPDATE [Order Details]
SET OrderID = OrderID


Updating ProductID and UnitPrice

ProductID updated
UnitPrice updated

Updating Quantity only

Quantity updated


Updating OrderID


Changes to the PRIMARY KEY are not allowed
Multiple-Row Considerations
Keep in mind that a trigger can be fired by an action that modifies a single row or multiple rows in a single
statement.
If you define your trigger to work for single rows only, you should reject changes that affect multiple rows. In
this case, you can check whether the system function @@ROWCOUNT returns a value greater than 1.
Chapter 9. Implementing Complex Processing Logic: Programming Triggers
343
You can define your trigger to deal only with multiple-row operations. In this case, you could use aggregate

functions or use cursors. None of these strategies is efficient for single-row operations.
The ideal situation would be to create a trigger with conditional logic to deal with either single-row or multiple-
row operations depending on the value returned by @@ROWCOUNT. Listing 9.16 shows a new version of the
example of Listing 9.6, optimized for both kinds of transactions.
Listing 9.16 You Can Use @@ROWCOUNT to Detect Multiple-Row Operations



Create trigger for insert


CREATE TRIGGER isrOrderDetails
ON [Order Details]
AFTER INSERT
AS
IF @@ROWCOUNT = 1
BEGIN

Single-row operation

UPDATE TC
SET TotalSales = TotalSales
+ I.UnitPrice * Quantity * (1 - Discount)
FROM TotalCategoriesSales TC
JOIN Products P
ON P.CategoryID = TC.CategoryID
JOIN Inserted I
ON I.ProductID = P.productID

END

ELSE
BEGIN

Multi-row operation

UPDATE TC
SET TotalSales = TotalSales
+ (SELECT SUM(I.UnitPrice * Quantity * (1 - Discount))
FROM Inserted I
WHERE I.ProductID = P.productID)
FROM TotalCategoriesSales TC
JOIN Products P
ON P.CategoryID = TC.CategoryID

END
GO


Create trigger for delete


Microsoft SQL Server 2000 Programming by Example

344
CREATE TRIGGER delOrderDetails
ON [Order Details]
AFTER DELETE
AS
IF @@ROWCOUNT = 1
BEGIN


Single-row operation

UPDATE TC
SET TotalSales = TotalSales
- D.UnitPrice * Quantity * (1 - Discount)
FROM TotalCategoriesSales TC
JOIN Products P
ON P.CategoryID = TC.CategoryID
JOIN Deleted D
ON D.ProductID = P.productID

END
ELSE
BEGIN

Multi-row operation

UPDATE TC
SET TotalSales = TotalSales
- (SELECT SUM(D.UnitPrice * Quantity * (1 - Discount))
FROM Deleted D
WHERE D.ProductID = P.productID)
FROM TotalCategoriesSales TC
JOIN Products P
ON P.CategoryID = TC.CategoryID


END
GO


Create trigger for Update


CREATE TRIGGER udtOrderDetails
ON [Order Details]
AFTER UPDATE
AS
IF @@ROWCOUNT = 1
BEGIN

Single-row operation

UPDATE TC
SET TotalSales = TotalSales
+ I.UnitPrice * I.Quantity * (1 - I.Discount)
FROM TotalCategoriesSales TC
JOIN Products P
ON P.CategoryID = TC.CategoryID
JOIN Inserted I
ON I.ProductID = P.productID

UPDATE TC
SET TotalSales = TotalSales
- D.UnitPrice * D.Quantity * (1 - D.Discount)
Chapter 9. Implementing Complex Processing Logic: Programming Triggers
345
FROM TotalCategoriesSales TC
JOIN Products P
ON P.CategoryID = TC.CategoryID

JOIN Deleted D
ON D.ProductID = P.productID

END
ELSE
BEGIN

Multi-row operation

UPDATE TC
SET TotalSales = TotalSales
+ (SELECT SUM(I.UnitPrice * Quantity * (1 - Discount))
FROM Inserted I
WHERE I.ProductID = P.productID)
FROM TotalCategoriesSales TC
JOIN Products P
ON P.CategoryID = TC.CategoryID
UPDATE TC
SET TotalSales = TotalSales
- (SELECT SUM(D.UnitPrice * Quantity * (1 - Discount))
FROM Deleted D
WHERE D.ProductID = P.productID)
FROM TotalCategoriesSales TC
JOIN Products P
ON P.CategoryID = TC.CategoryID


END
GO


Tip
As shown previously in Listing 9.16, you can easily define a trigger for AFTER UPDATE as a
sequence of the actions defined in the AFTER INSERT and AFTER DELETE triggers.
Altering Trigger Definitions
To modify the definition of a trigger, you can use the ALTER TRIGGER statement. In this case, the trigger will
take the new definition directly. Listing 9.17 shows how to execute the ALTER TRIGGER statement to modify
the tr_Employees trigger.
The syntax is identical to the CREATE TRIGGER statement. Moreover, because triggers are independent
objects, no objects are depending on them. They can be dropped and re-created any time, if necessary.
Caution
You can change the name of a trigger using the sp_rename stored procedure, but this does not
change the name of the trigger stored in the definition of the trigger in syscomments.
To rename a trigger, it is recommended to drop the trigger and re-create it with a different name.

Microsoft SQL Server 2000 Programming by Example

346
Listing 9.17 You Can Use the ALTER TRIGGER Statement to Modify a Trigger.


USE Northwind
GO

Create a trigger to restrict
modifications to the employees table
to the dbo

CREATE TRIGGER tr_Employees
ON Employees
AFTER UPDATE, INSERT, DELETE

AS
IF CURRENT_USER <> 'dbo'
BEGIN
RAISERROR ('Only Database Owners can modify Employees, transaction rolled back',
10, 1)
ROLLBACK TRAN
END
GO

Modify the trigger to restrict
modifications to the employees table
to the members of the db_owner role

ALTER TRIGGER tr_Employees
ON Employees
AFTER UPDATE, INSERT, DELETE
AS
IF IS_MEMBER('db_owner') <> 1
BEGIN
RAISERROR ('Only Database Owners can modify Employees, transaction rolled back',
10 ,1)
ROLLBACK TRAN
END
GO
Disabling Triggers
To prevent triggers from running when data arrives through replication, you can add the NOT FOR
REPLICATION option to the CREATE TRIGGER or ALTER TRIGGER statements. In this case, the trigger will
fire on direct modifications to the base table, but not from subscription actions.
Temporarily, you can disable a trigger to speed up some processes. To do so, you can use the ALTER TABLE
statement with the DISABLE TRIGGER option, as in Listing 9.18.

Listing 9.18 You Can Disable a Trigger
Chapter 9. Implementing Complex Processing Logic: Programming Triggers
347


USE Northwind
GO

To disable a single trigger

ALTER TABLE Employees
DISABLE TRIGGER tr_Employees , isr_Employees, udt_Employees

To disable several triggers from the same table
ALTER TABLE Employees
DISABLE TRIGGER tr_Employees, isr_Employees, udt_Employees

To disable all the triggers from a table

ALTER TABLE Employees
DISABLE TRIGGER ALL
To reenable the trigger, use the ALTER TABLE statement with the ENABLE TRIGGER option. Listing 9.19
shows how to reenable the triggers that were disabled in Listing 9.18.
Listing 9.19 You Can Reenable a Trigger


USE Northwind
GO

To enable a single trigger


ALTER TABLE Employees
ENABLE TRIGGER tr_Employees , isr_Employees, udt_Employees

To enable several triggers from the same table

ALTER TABLE Employees
ENABLE TRIGGER tr_Employees, isr_Employees, udt_Employees

To enable all the triggers from a table

ALTER TABLE Employees
ENABLE TRIGGER ALL
Nesting Triggers
Microsoft SQL Server 2000 Programming by Example

348
A trigger can be defined to modify a table, which in turn can have a trigger defined to modify another table,
and so on. In this case, triggers force the execution of other triggers, and the execution stops when the last
action does not fire any more triggers.
Because triggers are a specialized form of stored procedures, you can nest trigger execution up to 32 levels.
Triggers, stored procedures, scalar user-defined functions, and multistatement table-valued functions share
this limit. If the execution of a sequence of nested triggers requires more than 32 levels, the execution is
aborted, the transaction is rolled back, and the execution of the batch is cancelled.
Nested triggers are enabled by default. You can change this option at server level by setting the "nested
triggers" option to 0, using the system stored procedure sp_configure.
You can read the system function @@NESTLEVEL to know how many levels of nesting you have during the
execution of a trigger, stored procedure, or user-defined function.
Note
In a nested trigger situation, all the triggers are running inside the same transaction. Therefore, any

errors inside any of the triggers will roll back the entire transaction.

Listing 9.20 shows an example where you define triggers to maintain sales totals at different levels.
1. You insert, update, or delete data in the Order Details table. This data modification forces the
execution of the AFTER UPDATE trigger.
2. The AFTER UPDATE trigger in the Order Details table updates the SaleTotal column in the
Orders table.
3. Because the SaleTotal column in the Orders table has been updated, the existing AFTER
UPDATE trigger in the Orders table runs automatically and updates the SaleTotal column in the
Employees table and the Customers table.
Listing 9.20 You Can Create Triggers That Can Be Nested in Sequence


USE Northwind
GO

Add the column SaleTotal to the
Orders table

ALTER TABLE Orders
ADD SaleTotal money NULL

Add the column SaleTotal to the
Employees table

ALTER TABLE Employees
ADD SaleTotal money NULL

Add the column SaleTotal to the
Customers table

Chapter 9. Implementing Complex Processing Logic: Programming Triggers
349

ALTER TABLE Customers
ADD SaleTotal money NULL
GO

Initialize the data

UPDATE Orders
SET SaleTotal =
(SELECT SUM([Order Details].UnitPrice * Quantity * (1 - Discount))
FROM [Order Details]
WHERE [Order Details].OrderID = Orders.OrderID)

UPDATE Employees
SET SaleTotal =
(SELECT SUM(Orders.SaleTotal)
FROM Orders
WHERE Orders.EmployeeID = Employees.EmployeeID)

UPDATE Customers
SET SaleTotal =
(SELECT SUM(Orders.SaleTotal)
FROM Orders
WHERE Orders.CustomerID = Customers.CustomerID)

GO

Create nested triggers


CREATE TRIGGER isrTotalOrderDetails
ON [Order details]
AFTER INSERT, DELETE, UPDATE
AS

IF @@rowcount = 1

Single-row operation

UPDATE Orders
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT UnitPrice * Quantity * (1 - Discount)
FROM Inserted
WHERE Inserted.OrderID = Orders.OrderID), 0)
- ISNULL(
(SELECT UnitPrice * Quantity * (1 - Discount)
FROM Deleted
WHERE Deleted.OrderID = Orders.OrderID), 0)
ELSE

Multi-row operation

UPDATE Orders
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT SUM(UnitPrice * Quantity * (1 - Discount))
FROM Inserted
WHERE Inserted.OrderID = Orders.OrderID), 0)

- ISNULL(
(SELECT SUM(UnitPrice * Quantity * (1 - Discount))
FROM Deleted
Microsoft SQL Server 2000 Programming by Example

350
WHERE Deleted.OrderID = Orders.OrderID), 0)

GO



CREATE TRIGGER isrTotalOrders
ON Orders
AFTER INSERT, DELETE, UPDATE
AS

IF @@rowcount = 1
BEGIN
Single-row operation

UPDATE Employees
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT SaleTotal
FROM Inserted
WHERE Inserted.EmployeeID = Employees.EmployeeID), 0)
- ISNULL(
(SELECT SaleTotal
FROM Deleted

WHERE Deleted.EmployeeID = Employees.EmployeeID), 0)

UPDATE Customers
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT SaleTotal
FROM Inserted
WHERE Inserted.CustomerID = Customers.CustomerID), 0)
- ISNULL(
(SELECT SaleTotal
FROM Deleted
WHERE Deleted.CustomerID = Customers.CustomerID), 0)

END
ELSE
BEGIN

Multi-row operation

UPDATE Employees
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT SUM(SaleTotal)
FROM Inserted
WHERE Inserted.EmployeeID = Employees.EmployeeID), 0)
- ISNULL(
(SELECT SUM(SaleTotal)
FROM Deleted
WHERE Deleted.EmployeeID = Employees.EmployeeID), 0)


UPDATE Customers
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT SUM(SaleTotal)
FROM Inserted
WHERE Inserted.CustomerID = Customers.CustomerID), 0)
- ISNULL(
Chapter 9. Implementing Complex Processing Logic: Programming Triggers
351
(SELECT SUM(SaleTotal)
FROM Deleted
WHERE Deleted.CustomerID = Customers.CustomerID), 0)

END
GO

Updating Order Details
and forcing the nested triggers
execution

update [order details]
set quantity = 100
where orderid = 10248
and productid = 11

Testing totals in Orders table

select CustomerID, EmployeeID, SaleTotal from orders
WHERE OrderID = 10248
SELECT SUM([Order Details].UnitPrice * Quantity * (1 - Discount))

FROM [Order Details]
WHERE OrderID = 10248

Testing totals in Employees

SELECT SaleTotal
FROM Employees
WHERE EmployeeID = 5

SELECT SUM(SaleTotal)
FROM Orders
WHERE EmployeeID = 5

Testing totals in Customers

SELECT SaleTotal
FROM Customers
WHERE CustomerID = 'VINET'

SELECT SUM(SaleTotal)
FROM Orders
WHERE CustomerID = 'VINET'
GO

Dropping triggers

DROP TRIGGER isrTotalOrderDetails

DROP TRIGGER isrTotalOrders


Analyzing the previous example, you can see that the data is updated only at the Order Details level, and
two nested triggers maintain the summary information in the tables Orders, Employees, and Customers.
You can solve the same problem without using nested triggers. Create three triggers in the Order Details
table: one trigger to update the SaleTotal column in the Orders table, a second trigger to update the
Employees table, and a third one to update the Customers table. You can see in Listing 9.21 how to
implement this solution (note, you must execute the code from Listing 9.20 before running the code from
Listing 9.21).
Tip
Microsoft SQL Server 2000 Programming by Example

352
Create one trigger per logical action, as in Listing 9.21, and avoid nested triggers. Your database
application will be more modular and the maintenance will be easier.

Listing 9.21 Every Table Can Have Multiple Triggers for Each Action


USE Northwind
GO

CREATE TRIGGER tr_OrderDetails_TotalOrders
ON [Order details]
AFTER INSERT, DELETE, UPDATE
AS

IF @@rowcount = 1

Single-row operation

UPDATE Orders

SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT UnitPrice * Quantity * (1 - Discount)
FROM Inserted
WHERE Inserted.OrderID = Orders.OrderID), 0)
- ISNULL(
(SELECT UnitPrice * Quantity * (1 - Discount)
FROM Deleted
WHERE Deleted.OrderID = Orders.OrderID), 0)

ELSE

Multi-row operation

UPDATE Orders
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT SUM(UnitPrice * Quantity * (1 - Discount))
FROM Inserted
WHERE Inserted.OrderID = Orders.OrderID), 0)
- ISNULL(
(SELECT SUM(UnitPrice * Quantity * (1 - Discount))
FROM Deleted
WHERE Deleted.OrderID = Orders.OrderID), 0)

GO


CREATE TRIGGER tr_OrderDetails_TotalEmployees
ON [Order details]

Chapter 9. Implementing Complex Processing Logic: Programming Triggers
353
AFTER INSERT, DELETE, UPDATE
AS

IF @@rowcount = 1

Single-row operation

UPDATE Employees
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT UnitPrice * Quantity * (1 - Discount)
FROM Inserted
JOIN Orders
ON Inserted.OrderID = Orders.OrderID
WHERE Orders.EmployeeID = Employees.EmployeeID), 0)
- ISNULL(
(SELECT UnitPrice * Quantity * (1 - Discount)
FROM Deleted
JOIN Orders
ON Deleted.OrderID = Orders.OrderID
WHERE Orders.EmployeeID = Employees.EmployeeID), 0)

ELSE

Multi-row operation

UPDATE Employees
SET SaleTotal = SaleTotal

+ ISNULL(
(SELECT SUM(UnitPrice * Quantity * (1 - Discount))
FROM Inserted
JOIN Orders
ON Inserted.OrderID = Orders.OrderID
WHERE Orders.EmployeeID = Employees.EmployeeID), 0)
- ISNULL(
(SELECT SUM(UnitPrice * Quantity * (1 - Discount))
FROM Deleted
JOIN Orders
ON Deleted.OrderID = Orders.OrderID
WHERE Orders.EmployeeID = Employees.EmployeeID), 0)

GO

CREATE TRIGGER tr_OrderDetails_TotalCustomers
ON [Order details]
AFTER INSERT, DELETE, UPDATE
AS


IF @@rowcount = 1

Single-row operation
UPDATE Customers
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT UnitPrice * Quantity * (1 - Discount)
FROM Inserted
JOIN Orders

ON Inserted.OrderID = Orders.OrderID
WHERE Orders.CustomerID = Customers.CustomerID), 0)
- ISNULL(
Microsoft SQL Server 2000 Programming by Example

354
(SELECT UnitPrice * Quantity * (1 - Discount)
FROM Deleted
JOIN Orders
ON Deleted.OrderID = Orders.OrderID
WHERE Orders.CustomerID = Customers.CustomerID), 0)

ELSE

Multi-row operation

UPDATE Customers
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT SUM(UnitPrice * Quantity * (1 - Discount))
FROM Inserted
JOIN Orders
ON Inserted.OrderID = Orders.OrderID
WHERE Orders.CustomerID = Customers.CustomerID), 0)
- ISNULL(
(SELECT SUM(UnitPrice * Quantity * (1 - Discount))
FROM Deleted
JOIN Orders
ON Deleted.OrderID = Orders.OrderID
WHERE Orders.CustomerID = Customers.CustomerID), 0)

GO

DROP TRIGGER tr_OrderDetails_TotalOrders

DROP TRIGGER tr_OrderDetails_TotalCustomers

DROP TRIGGER tr_OrderDetails_TotalEmployees


GO
Note
The examples in Listings 9.20 and 9.21 create a single trigger for the three actions: INSERT,
DELETE, and UPDATE. Creating individual triggers per action is more efficient, as in Listing 9.16,
from the execution point of view. I use this strategy here only to simplify the examples.
Recursive Triggers
If a trigger defined in the Products table modifies data in the Employees table, and the Employees table
has a trigger that in turn modifies the Products table, the trigger defined in the Products table will fire again.
This situation is called indirect recursion, because a single statement forces multiple executions of the same
trigger, through the execution of other triggers. This is a special case of nested triggers, and everything said
about it in the preceding section can be applied to this case.
In some scenarios, it is possible to have direct recursion, when a table has a trigger that modifies some data
in the table again. In this case, by default, SQL Server will not fire the trigger again, avoiding this direct
recursion.
To enable trigger recursion in a database you must set the 'recursive triggers' option to 'true' at
database level using the sp_dboption system stored procedure, or set the option RECURSIVE_TRIGGERS
ON in the ALTER DATABASE statement. Listing 9.22 shows both statements.
Listing 9.22 You Can Enable Recursive Triggers at Database Level Only
Chapter 9. Implementing Complex Processing Logic: Programming Triggers
355



Enable Recursive triggers in Northwind

EXEC sp_dboption 'Northwind', 'recursive triggers', 'true'

Disable Recursive triggers in Northwind

ALTER DATABASE Northwind
SET RECURSIVE_TRIGGERS OFF
Consider the typical hierarchical table where you save cost and budget breakdown of a project cost control
system. Every row in this table has a single ID as primary key, but it refers to another row as a parent row,
excluding the root row: the project itself. Any change on Cost or Budget in a row has to be escalated to the
highest level, and you introduce costs only in rows with no children.
This strategy is very flexible, adjusting changes easily on the distribution of activities in the project. Listing
9.23 shows the code to implement this example .
Listing 9.23 Use Triggers to Maintain Hierarchical Data


Create the base table

CREATE TABLE CostBudgetControl (
ID int NOT NULL
PRIMARY KEY,
Name nvarchar(100) NOT NULL,
ParentID int NULL
REFERENCES CostBudgetControl(ID),
Cost money NOT NULL DEFAULT 0,
Budget money NOT NULL DEFAULT 0,
HasChildren bit DEFAULT 0)


Insert Cost Structure
Create a text file (Gas.txt)
with the following contents:
/*
1, Gas Pipeline Project, 0, 85601000.0000, 117500000.0000, 1
2, Engineering, 1, 800000.0000, 950000.0000, 1
3, Materials, 1, 23400000.0000, 28000000.0000, 1
4, Construction, 1, 61000000.0000, 88000000.0000, 1
5, Supervision, 1, 401000.0000, 550000.0000, 1
6, Line, 2, 300000.0000, 400000.0000, 0
7, Stations, 2, 500000.0000, 550000.0000, 0
8, Pipes, 3, 14500000.0000, 16000000.0000, 0
9, Machinery, 3, 8900000.0000, 12000000.0000, 0
10, Section A, 4, 31000000.0000, 47000000.0000, 1
Microsoft SQL Server 2000 Programming by Example

356
11, Section B, 4, 30000000.0000, 41000000.0000, 1
12, Welding, 5, 200000.0000, 250000.0000, 0
13, Civil, 5, 145000.0000, 200000.0000, 0
14, Buildings, 5, 56000.0000, 100000.0000, 0
15, Civil works, 10, 20000000.0000, 30000000.0000, 0
16, Civil works, 11, 18000000.0000, 25000000.0000, 0
17, Pipeline, 10, 11000000.0000, 17000000.0000, 0
18, Pipeline, 11, 12000000.0000, 16000000.0000, 0
*/

BULK INSERT Northwind.dbo.CostBudgetControl
FROM 'C:\Gas.txt'
WITH

(
FIELDTERMINATOR = ', ',
ROWTERMINATOR = '\n'
)
GO

UPDATE CostBudgetControl
SET ParentID = NULL
WHERE ID = 1
GO

Create the recursive trigger

CREATE TRIGGER udtCostBudget
ON CostBudgetControl
AFTER UPDATE
AS
IF @@rowcount>0
UPDATE CostBudgetControl
SET Cost = Cost
+ ISNULL((SELECT SUM(Cost)
FROM Inserted
WHERE Inserted.ParentID = CostBudgetControl.ID), 0)
- ISNULL((SELECT SUM(Cost)
FROM Deleted
WHERE Deleted.ParentID = CostBudgetControl.ID), 0),
Budget = Budget
+ ISNULL((SELECT SUM(Budget)
FROM Inserted
WHERE Inserted.ParentID = CostBudgetControl.ID), 0)

- ISNULL((SELECT SUM(Budget)
FROM Deleted
WHERE Deleted.ParentID = CostBudgetControl.ID), 0)
WHERE ID IN
(SELECT ParentID
FROM Inserted
UNION
SELECT ParentID
FROM Deleted)
GO

Enable Recursive triggers


ALTER DATABASE Northwind
SET RECURSIVE_TRIGGERS ON
GO

Chapter 9. Implementing Complex Processing Logic: Programming Triggers
357
Total Cost and Budget
Before the update

SELECT Cost, Budget
FROM CostBudgetControl
WHERE ID = 1

Update some cost
UPDATE CostBudgetControl
SET Cost = 12500000.0000

WHERE ID = 17

Total Cost and Budget
After the update listings;triggers;mainataining hierarchical
data>

SELECT Cost, Budget
FROM CostBudgetControl
WHERE ID = 1

GO

DROP TABLE CostBudgetControl
Cost Budget

85601000.0000 117500000.0000


Cost Budget

87101000.0000 117500000.0000

Security Implications of Using Triggers
Only certain users can create triggers:
• The owner of the table on which the trigger has to be defined
• Members of the db_owner and db_ddladmin database roles
• Members of the sysadmin server role, because permissions don't affect them
The user who creates the trigger needs specific permissions to execute the statements defined in the code of
the trigger.
Caution

If any of the objects referenced in the trigger don't belong to the same owner, you can have a
broken ownership chain situation. To avoid this situation, it is recommended that dbo must be the
owner of all the objects in a database.
Microsoft SQL Server 2000 Programming by Example

358
Enforcing Business Rules: Choosing Among INSTEAD of Triggers,
Constraints, and AFTER Triggers
This is the final chapter that discusses techniques to enforce data integrity, and as a summary, you can
propose which ways are recommended to enforce data integrity:
• To uniquely identify every row, define a PRIMARY KEY constraint. This is one of the first rules to apply
to designing a normalized database. Searching for values contained in a PRIMARY KEY is fast
because there is a UNIQUE INDEX supporting the PRIMARY KEY.
• To enforce uniqueness of required values in a column or group of columns, other than the PRIMARY
KEY, define a UNIQUE constraint. This constraint does not produce much overhead because there is
a UNIQUE INDEX supporting this constraint.
• To enforce uniqueness of optional values (columns that accept NULL), create a TRIGGER. You can
test this uniqueness before the data modification with an INSTEAD OF trigger, or after the data
modification with an AFTER trigger.
• To validate entries in a column, according to a specific pattern, range, or format, create a CHECK
constraint.
• To validate values in a row, where values in different columns must satisfy specific conditions, create
one or more CHECK constraints. If you create one CHECK constraint per condition, you can later
disable specific conditions only, if required.
• To validate values in a column, among a list of possible values, create a look-up table (LUT) with the
required values and create a FOREIGN KEY constraint to reference the look-up table. You could
create a CHECK constraint instead, but using a LUT is more flexible.
• To restrict values in a column to the values contained in a column in a second table, create a
FOREIGN KEY constraint in the first table .
• To make sure that every entry in a column is related to the primary key of another table, without

exceptions, define the FOREIGN KEY column as NOT NULL.
• To restrict the values in a column to complex conditions involving other rows in the same table, create
a TRIGGER to check these conditions. As an alternative, create a CHECK constraint with a user-
defined function to check this complex condition.
• To restrict the values in a column to complex conditions involving other tables in the same or different
database, create a TRIGGER to check these conditions.
• To declare a column as required, specify NOT NULL in the column definition.
• To specify a default value for columns where no value is supplied in INSERT operations, declare a
DEFAULT property for the column.
• To declare a column as autonumeric, declare an IDENTITY property in the column and specify the
seed value and the increment.
• To declare a default value, which depends on values in other rows or tables, declare a DEFAULT
property for the column using a user-defined function as a default expression.
• To cascade changes on primary keys to related fields in other tables, declare a FOREIGN KEY with
the ON UPDATE CASCADE clause. Do not create triggers to perform this operation.
• To delete in cascade related rows when the row in the primary table is deleted, declare a FOREIGN
KEY with the ON DELETE CASCADE clause. Do not create triggers to perform this operation.
• To cascade complex operations to other tables to maintain denormalized data, create individual
triggers to execute this operation.
• To validate INSERT, UPDATE, or DELETE operations applied through a view, define an INSTEAD OF
trigger on the view.
• Do not use RULE objects unless you want to define self-contained user-defined data types. It is
recommended to declare CHECK constraints instead.
• Do not use DEFAULT objects unless you want to define self-contained user-defined data types. It is
recommended to declare DEFAULT definitions instead .
What's Next?
This chapter covered the creation and use of triggers as a way to enforce complex data integrity.
Chapter 9. Implementing Complex Processing Logic: Programming Triggers
359
Chapter 10, "Enhancing Business Logic: User-Defined Functions (UDF)," covers user-defined

functions, which can be used as part of the trigger definition and as an alternative to triggers, providing extra
computing capabilities to CHECK constraints and DEFAULT definitions.
Chapter 12, "Row-Oriented Processing: Using Cursors," explains how to use cursors. This could be
useful in some triggers to deal with multiple-row actions.
Triggers always work inside a transaction, and Chapter 13, "Maintaining Data Consistency:
Transactions and Locks," covers specifically that: transaction and locks. There you can see the
implications of modifying data through triggers and how to increase concurrency, preventing undesired
blockings.

Chapter 10. Enhancing Business Logic: User-Defined Functions (UDF)
361
Chapter 10. Enhancing Business Logic: User-Defined
Functions (UDF)
Procedural languages are based mainly in the capability to create functions, encapsulate complex
programming functionality, and return a value as a result of the operation. Using SQL Server 2000, you can
define user-defined functions(UDF), which combine the functionality of stored procedures and views but
provide extended flexibility.
This chapter teaches you the following:
• What the built-in user-defined functions are and how to use them
• How to define user-defined functions that return a scalar value
• How to define user-defined functions that return a result set
• How to convert stored procedures and views into user-defined functions
• How to extend the functionality of constraints with user-defined functions
Benefits of User-Defined Functions
You learned in Chapter 8, "Implementing Business Logic: Programming Stored Procedures," how to
create stored procedures, which are similar to the way you create functions in other programming languages.
However, using stored procedures from Transact-SQL is not very flexible, because you can use them only
with the EXECUTE or INSERT EXECUTE statements. If you have a stored procedure that returns a single
value, you cannot use this procedure inside an expression. If your procedure returns a result set, you cannot
use this procedure in the FROM clause of any Transact-SQL statement.

In Chapter 3, "Working with Tables and Views," you learned about views and how to use them
anywhere as a replacement for tables. However, when you define a view, you are limited to a single SELECT
statement. Unlike stored procedures, you cannot define parameters in a view.
Some user-defined functions are similar to views but they can be defined with more than one statement and
they accept parameters. You can call user-defined functions in the same way you execute stored procedures,
and you can use scalar user-defined functions as part of any expression anywhere in a Transact-SQL
statement where an expression is valid. Furthermore, you can use a user-defined function that returns a table
in the FROM clause of any Transact-SQL Data Manipulation Language (DML) statement.
User-defined functions have many benefits in common with stored procedures, as covered in Chapter 8.
However, user-defined functions have more useful benefits. They enable you to
• Use the result set returned by a stored procedure in the FROM clause of a query
• Join the results of two stored procedures, without using temporary tables to store intermediate results
• Use the result of a stored procedure in the IN operator
• Use a stored procedure as a subquery in the WHERE clause
• Create a view that cannot be solved with a single SELECT statement
• Create a view with parameters similar to the way Microsoft Access creates queries with parameters
• Extend the list of built-in functions with any financial function
• Create new mathematical functions for any special scientific database applications that you might
require
This chapter will help you discover how user-defined functions can help you solve these common
programming problems.
Built-In User-Defined Functions
SQL Server 2000 implements some system functions as built-in user-defined functions. Many of them are not
documented; Query Analyzer, Enterprise Manager, Profiler, Replication, and other client applications and
system processes use some of these built-in user-defined functions internally. These functions can be used
almost as any other user-defined function, but SQL Server itself implements them.
Microsoft SQL Server 2000 Programming by Example

362
You cannot change the definition of these built-in user-defined functions. In some cases, you cannot see their

definition using the sp_help or sp_helptext system stored procedures, and you cannot script them.
However, their definition is stored in the syscomments system table as any other user-defined function.
Caution
Microsoft does not guarantee that undocumented built-in user-defined functions will remain
unchanged in the future; however, we can use some of them as examples of what kind of
operations a user-defined function can do.

In some cases, built-in user-defined functions return a single scalar value, and all of them are undocumented:
• fn_CharIsWhiteSpace(@nchar) returns 1 if the variable @nchar only contains a space, a tab
character, a newline character, or carriage return character; it returns 0 otherwise.
• fn_MSSharedVersion(@len_minorversion) returns the major and minor version number of
SQL Server. @len_minorversion specifies how many digits to show for the minor version.
• fn_MsGenSqeScStr(@pstrin) returns the string @pstring, converting single quotes into two
single quotes so that you are able to concatenate this string with other strings to execute a dynamic
statement.
• fn_IsReplMergeAgent() returns 1 if the present process is executed by the Replication Merge
Agent.
• fn_GetPersistedServerNameCaseVariation(@servername) returns the server name of the
server specified in @servername with exactly the same case it uses in the sysservers system
table, regardless of the case used to call this function.
• fn_ReplGetBinary8LoDWord(@binary8_value) takes the lower four bytes from the
@binary8_value binary variable and converts them into an integer value.
• fn_ReplPrepadBinary8(@varbinary8_value) converts the varbinary(8) value stored in
@varbinary8_value into a fixed-length binary(8) value with leading zeros.
• fn_ReplMakeStringLiteral(@string) converts the value stored in the @string value into a
UNICODE string, including quotes, such as N'Hello', to be used in dynamically constructed
statements.
• fn_ReplQuoteName(@string) returns the value stored in @string en closed in square brackets.
You can use this function in dynamic execution to select object names that contain spaces or
keywords, such as [Order Details].

• fn_GenerateParameterPattern(@parameter) returns a pattern string you can use with the
LIKE operator to test for strings containing any case variation of the value stored in @parameter,
such as converting 'Hello' into '%[hH][eE][lL][lL][oO]%'. This is useful in case-sensitive
servers, databases, or columns.
• fn_UpdateParameterWithArgument, fn_SkipParameterArgument, and
fn_RemoveParameterWithArgument are internal functions, and their study is not the purpose of
this book.
Listing 10.1 shows some examples of scalar built-in, user-defined functions and the partial result of some of
them.
Listing 10.1 Using Undocumented Built-In User-Defined Functions
Chapter 10. Enhancing Business Logic: User-Defined Functions (UDF)
363


USE Northwind
GO

PRINT CHAR(10)
+ 'fn_chariswhitespace(CHAR(9))'
+ CHAR(10)

select fn_chariswhitespace(CHAR(9))
GO

PRINT CHAR(10)
+ 'fn_mssharedversion(1)'
+ CHAR(10)

select master.dbo.fn_mssharedversion(1)
GO


PRINT CHAR(10)
+ 'fn_replgetbinary8lodword(0x0304030401020102)'
+ CHAR(10)
select fn_replgetbinary8lodword(0x0304030401020102)
GO

PRINT CHAR(10)
+ 'fn_replmakestringliteral(@a)'
+ CHAR(10)

declare @a varchar(100)

set @a = 'peter is right'

select fn_replmakestringliteral(@a)
GO

PRINT CHAR(10)
+ 'fn_replprepadbinary8(123456890123)'
+ CHAR(10)

select fn_replprepadbinary8(123456890123)
GO

PRINT CHAR(10)
+ 'fn_replquotename("hello")'
+ CHAR(10)

select fn_replquotename('hello')

Microsoft SQL Server 2000 Programming by Example

364


fn_chariswhitespace(CHAR(9))



1

fn_mssharedversion(1)



80

fn_replgetbinary8lodword(0x0304030401020102)



16908546

fn_replmakestringliteral(@a)

N'peter is right'


fn_replprepadbinary8(123456890123)




0x0C0000010BA59ABE


fn_replquotename("hello")



[hello]
In other cases, built-in, user-defined functions return a table. SQL Server documents some of them:
• fn_ListExtendedProperty produces a list of available extended properties for a given database
or database objects, such as database users, user-defined data types, tables, views, stored
procedures, user-defined functions, default objects, rule objects, columns of tables and views,
parameters of stored procedures and user-defined functions, indexes, constraints, and triggers.
• fn_HelpCollations returns a list of the available collations.
• fn_ServerSharedDrives returns a list of the drives shared by a clustered server.
• fn_VirtualServerNodes returns the list of server nodes, defining a virtual server in a clustering
server environment.
• fn_VirtualFileStats returns statistical I/O information about any file in a database, including
transaction log files.
Listing 10.2 shows some examples of how to use these table-valued, built-in, user-defined functions.
Note

×