第10章 可编程对象2-动态SQL、用户定义函数、存储过程、触发器、错误处理
10.6 动态SQL
SQL Server允许用字符串来动态构造T-SQL代码的一个批处理,接着再执行这个批处理,这种功能称为动态SQL(dynamic SQL)。
两种执行动态SQL的方法:
- 使用 EXEC
- 使用 sp_executesql 存储过程
1、 EXEC 命令
是 T-SQL 最早提供的一种用于执行动态SQL的方法。输入既支持普通字符,也支持Unicode字符。
DECLARE @sql AS VARCHAR(100); DECLARE @myname AS VARCHAR(20); SET @myname = 'alex' SET @sql = 'PRINT ''My name is '+ @myname + '.'';'; EXEC(@sql); GO
注意提防SQL注入。
2、sp_executesql 存储过程
它支持输入和输出参数,因而更安全和灵活,且性能也比EXEC好,但只支持Unicode字符串作为其输入的批处理代码(即字符串外面用 N‘ ’包裹)。
它包含两个参数和一个参数赋值部分:
- 第一个参数@stmt:要运行的批处理代码的Unicode字符串
- 第二个参数@params:也是一个Unicode字符串,包含@stmt中所有输入和输出参数的声明。
- 参数赋值部分:为输入和输出参数指定值,各参数之间用逗号隔开。
DECLARE @sql AS NVARCHAR(100); SET @sql = N' SELECT @myname,@age '; EXEC sp_executesql @stmt = @sql, @params = N'@myname AS VARCHAR(20), @age AS INT', @myname = 'alex', @age = 18 GO
为了使用输出参数,只需要简单地在参数声明和参数赋值部分同时指定 OUTPUT 关键字。
DECLARE @Counts TABLE ( schemaname sysname NOT NULL, tablename sysname NOT NULL, numrows INT NOT NULL, PRIMARY KEY(schemaname, tablename) ); DECLARE @sql AS NVARCHAR(350), @schemaname AS sysname, @tablename AS sysname, @numrows AS INT; DECLARE C CURSOR FAST_FORWARD FOR SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES; OPEN C FETCH NEXT FROM C INTO @schemaname, @tablename; WHILE @@fetch_status = 0 BEGIN SET @sql = N'SET @n = (SELECT COUNT(*) FROM ' + QUOTENAME(@schemaname) + N'.' + QUOTENAME(@tablename) + N');'; EXEC sp_executesql @stmt = @sql, @params = N'@n AS INT OUTPUT', --定义输出参数 @n = @numrows OUTPUT; --将输出赋值给变量 @numrows INSERT INTO @Counts(schemaname, tablename, numrows) VALUES(@schemaname, @tablename, @numrows); FETCH NEXT FROM C INTO @schemaname, @tablename; END CLOSE C; DEALLOCATE C; SELECT schemaname, tablename, numrows FROM @Counts; GO
10.7 例程1--用户定义函数
例程(routine)是为了计算结果或执行任务而对代码进行封装的一种编程对象。SQLserver支持三种例程:
- 用户定义函数
- 存储过程
- 触发器
用户定义函数(UDF,user-defined function)有两种:标量UDF和表值UDF。
(表值UDF分为:内联表值函数和多语句表值函数,详见:SQL SERVER 用户自定义函数(UDF)深入解析)
UDF的最大优点是可以在查询中集成。标量UDF可以出现在查询中返回单个值得表达式处,表值UDF只能在查询的FORM子句中出现。
UDF不允许有任何副作用,即不能对数据库中的任何架构或数据进行修改。
USE TSQLFundamentals2008;
IF OBJECT_ID('dbo.fn_age') IS NOT NULL DROP FUNCTION dbo.fn_age;
GO
CREATE FUNCTION dbo.fn_age
(
@birthdate AS DATETIME,
@eventdate AS DATETIME
)
RETURNS INT
AS
BEGIN
RETURN
DATEDIFF(year, @birthdate, @eventdate)
- CASE WHEN 100 * MONTH(@eventdate) + DAY(@eventdate)
< 100 * MONTH(@birthdate) + DAY(@birthdate)
THEN 1 ELSE 0
END
END
GO
-- Test function
SELECT
empid, firstname, lastname, birthdate,
dbo.fn_age(birthdate, CURRENT_TIMESTAMP) AS age
FROM HR.Employees;
一个函数体内可以包含多个RETURN子句,也可以包含流程控制代码、计算代码等等。但是函数必须由一个RETURN子句返回一个值。
10.7 例程2--存储过程
存储过程可以有输入和输出参数,可以返回查询的结果集,也允许调用具有副作用的代码(即不但可以对数据进行修改,也可以对数据库架构进行修改)。
和使用普通代码相比,使用存储过程还可以获得如下好处:
- 封装逻辑处理
- 更好的控制安全性
- 整合错误处理
- 提高执行性能
USE TSQLFundamentals2008;
IF OBJECT_ID('Sales.usp_GetCustomerOrders', 'P') IS NOT NULL
DROP PROC Sales.usp_GetCustomerOrders;
GO
CREATE PROC Sales.usp_GetCustomerOrders
@custid AS INT,
@fromdate AS DATETIME = '19000101',
@todate AS DATETIME = '99991231',
@numrows AS INT OUTPUT
AS
SET NOCOUNT ON;
SELECT orderid, custid, empid, orderdate
FROM Sales.Orders
WHERE custid = @custid
AND orderdate >= @fromdate
AND orderdate < @todate;
SET @numrows = @@rowcount;
GO
----调用----
DECLARE @rc AS INT;
EXEC Sales.usp_GetCustomerOrders
@custid = 1, -- Also try with 100
@fromdate = '20071001',
@todate = '20080101',
@numrows = @rc OUTPUT;
SELECT @rc AS numrows;
GO
10.7 例程3--触发器
触发器是一种特殊的存储过程,一种不能被显式执行,而必须依附于一个事件的过程。
SQL Server支持把触发器和两种类型的事件相关:
- 数据操作事件(如 INSERT)------ DML触发器
- 数据定义事件(如 CREATE TABLE)------ DDL触发器
1、DML触发器
SQL Server支持两种DML触发器:
- AFTER 触发器:与之关联的事件完成后才触发,只能定义在持久化的表上。
- INSTEAD OF 触发器:是为了代替与之关联的事件操作,可定义在持久化的表或视图上。
在触发器代码中,可以访问称为inserted和deleted的两个表:
- inserted表:包含当执行 INSERT 和 UPDATE 语句时受影响行的新数据的镜像。
- deleted表:包含当执行 DELETE 和 UPDATE 语句时受影响行的旧数据的镜像。
注意:不能在 'inserted' 表和 'deleted' 表中使用 text、ntext 或 image 列。
对于INSTEAD OF 触发器,inserted表和deleted表包含导致触发器触发的修改操作打算要影响的行。
(1)AFTER触发器
包括:INSERT、DELETE、UPDATE三种类型
CREATE TRIGGER trg_T1_insert_audit ON dbo.T1 AFTER INSERT AS SET NOCOUNT ON; INSERT INTO dbo.T1_Audit(keycol, datacol) SELECT keycol, datacol FROM inserted; GO
2、DDL触发器
SQL Server 支持在两个作用域内创建 DDL触发器:
- 数据库作用域的事件(如 CREATE TABLE)
- 服务器作用域内的事件 (如 CREATE DATABASE)
SQL Server 只支持AFTER类型的DDL触发器。
10.8 错误处理
1、TRY...CATCH 块
如果TRY块中的代码没有错误,会忽略CATCH块;如果TRY块中有错误,会执行CATCH块中的代码。
BEGIN TRY PRINT 10/2; PRINT 'No error'; END TRY BEGIN CATCH PRINT 'Error'; END CATCH GO ------以上代码结果为------- --5 --No error ----------------------------- BEGIN TRY PRINT 10/0; PRINT 'No error'; END TRY BEGIN CATCH PRINT 'Error'; END CATCH GO ------以上代码结果为------- --error -----------------------------
2、错误函数
通常,在CATCH块中进行的错误处理会涉及检查导致错误的原因,采取某种处理操作。SQL Server可以通过一组函数来反馈有关错误的信息。
- ERROR_NUMBER:返回一个整数,代表错误的错误号
- ERROR_MESSAGE:返回错误的消息文本。要得到错误号和错误消息的列表,可以查询 sys.messages 目录视图。
- ERROR_SEVERITY和 ERROR_STATE:返回错误的严重级别和状态号
USE tempdb; IF OBJECT_ID('dbo.Employees') IS NOT NULL DROP TABLE dbo.Employees; CREATE TABLE dbo.Employees ( empid INT NOT NULL, empname VARCHAR(25) NOT NULL, mgrid INT NULL, CONSTRAINT PK_Employees PRIMARY KEY(empid), CONSTRAINT CHK_Employees_empid CHECK(empid > 0), CONSTRAINT FK_Employees_Employees FOREIGN KEY(mgrid) REFERENCES dbo.Employees(empid) ); GO -- Detailed Example BEGIN TRY INSERT INTO dbo.Employees(empid, empname, mgrid) VALUES(1, 'Emp1', NULL); -- Also try with empid = 0, 'A', NULL END TRY BEGIN CATCH IF ERROR_NUMBER() = 2627 BEGIN PRINT 'Handling PK violation...'; END ELSE IF ERROR_NUMBER() = 547 BEGIN PRINT 'Handling CHECK/FK constraint violation...'; END ELSE IF ERROR_NUMBER() = 515 BEGIN PRINT 'Handling NULL violation...'; END ELSE IF ERROR_NUMBER() = 245 BEGIN PRINT 'Handling conversion error...'; END ELSE BEGIN PRINT 'Handling unknown error...'; END PRINT 'Error Number : ' + CAST(ERROR_NUMBER() AS VARCHAR(10)); PRINT 'Error Message : ' + ERROR_MESSAGE(); PRINT 'Error Severity: ' + CAST(ERROR_SEVERITY() AS VARCHAR(10)); PRINT 'Error State : ' + CAST(ERROR_STATE() AS VARCHAR(10)); PRINT 'Error Line : ' + CAST(ERROR_LINE() AS VARCHAR(10)); PRINT 'Error Proc : ' + COALESCE(ERROR_PROCEDURE(), 'Not within proc'); END CATCH GO
可创建一个存储过程,以封装可以重用的错误处理代码。
浙公网安备 33010602011771号