第16章 触发器
学习导读
前面在介绍数据完整性时,提到SQL Server
2008提供了约束、默认值,以及触发器3类对象
,用于保证数据的完整性,那时笔者只介绍了约
束和默认值,本章主要介绍触发器(Trigger)。
其实,触发器就是一类特殊的存储过程。触发器
在创建后,一般是在数据库事件的驱动下自动执
行的,即当SQL Server中某一个事件发生时,
SQL Server将自动执行其相关联的触发器。
1
触发器概述
前面介绍的存储过程都是需要手工执行的,
即在SQL语句中,用EXECUTE语句执行相应
的存储过程;而触发器这类存储过程却由SQL
Server自动执行。SQL Server之所以能够自动
执行触发器,是因为触发器在创建时,与特定
的SQL Server事件相关联。
依据触发器使用的SQL Server事件的不同,
触发器可分为:登录触发器、DDL触发器、
DML触发器。
2
登录触发器
登录触发器关联的事件是事件组
SecurityAudit的事件AuditLogin,即LOGIN事
件。如果用户遗忘了事件或事件组的概念,可
以参考前面介绍SQL Server Profiler跟踪的内容
。登录触发器是一种AFTER触发器,不过,登
录触发器不存在于特定的架构之下。
3
登录触发器的执行时机和作用
登录触发器的执行时机是用户登录到SQL Server,
具体的是在登录过程的身份验证结束之后至建立会话
之前。显然,如果身份验证失败,将不激发登录触发
器,所以触发器不能检测到用户对于登录的尝试,以
及失败的次数。不过,用户可以通过SQL Server的日
志了解到这些信息。
既然登录触发器执行于这个时期,那么就可以使用
登录触发器控制用户会话的建立过程,如限制SQL
Server的用户名及其会话次数。
4
SQL登录触发器
所谓SQL登录触发器,是指用普通的SQL语
句编写的登录触发器,主要是区别使用CLR编
写的登录触发器。如果想要在登录触发器中使
用登录事件的信息,可以参看后面DDL中介绍
的EVENTDATA函数。
5
SQL登录触发器
1.创建SQL登录触发器
创建登录触发器的SQL语句为CREATE
TRIGGER。该语句支持创建SQL语句编写的登
录触发器,也支持创建基于程序集的登录触发
器:
CREATE TRIGGER trigger_name
ON ALL SERVER
[WITH [ENCRYPTION|EXECUTE AS][,…n]
FOR LOGON
AS
{sql_statement [][,…n]}
6
SQL登录触发器
组成元素的意义:
trigger_name是登录触发器的名称,不能以#或##
开头
ALL SERVER指示登录触发器的作用域是当前服
务器
WITH ENCRYPTION指示系统加密存储触发器的
文本
EXECUTE AS指示执行登录触发器的环境
sql_statement是登录触发器的核心
7
SQL登录触发器
创建一个登录触发器login_trigger,该登录触
发器的主要功能是将登录名为
PYW\Administrator的会话过程的信息记录到数
据库database_demo的表dbo.tb_logintrigger中
。
USE master
GO
CREATE TABLE database_demo.dbo.tb_logintrigger(id
INT,name NCHAR(256),logintime DATETIME)
GO
CREATE TRIGGER login_trigger
ON ALL SERVER WITH EXECUTE AS ‘PYW\Administrator’
FOR LOGON
8
SQL登录触发器
AS
BEGIN
INSERT INTO
database_demo.dbo.tb_logintrigger(id,name,logintime)
SELECT session_id,original_login_name,login_time
FROM sys.dm_EXEC_sessions
WHERE original_login_name=‘PYW\Administrator’
COMMIT
END
9
SQL登录触发器
也可以通过使用系统视图sys.server_evnets
和sys.server_triggers查看登录触发器。
SELECT
sysst.name,sysst.parent_class_desc,sysst.type_desc,sysse.ty
pe_desc,sysse.is_trigger_event,sysse.event_group_type_des
c
FROM sys.server_events AS sysse LEFT JOIN
sys.server_triggers AS sysst ON
sysse.object_id=sysst.object_id
WHERE sysse.type_desc=‘LOGON’
10
SQL登录触发器
2.使用登录触发器
在使用CREATE TRIGGER语句创建SQL登录触发器
login_trigger后,SQL Server将自动使用该登录触发
器。在下一次登录到SQL Server数据库引擎时,就可
以看到效果。我们重新打开一个Studio程序。
待登录成功后,在数据库database_demo的表
dbo.tb_logintrigger中将包含此次登录的信息:
USE database_demo
GO
SELECT id,name,logintime
FROM database_demo.dbo.tb_logintrigger
11
SQL登录触发器
3.更改登录触发器
更改触发器的语句为ALTER TRIGGER:
ALTER TRIGGER trigger_name
ON ALL SERVER
[WITH [ENCRYPTION|EXECUTE AS] [,…n]]
FOR LOGON
AS
{ sql_statement [] [,…n] }
12
SQL登录触发器
组成元素的意义:
trigger_name是登录触发器的名称。不能以#或##
开头
ALL SERVER指定登录触发器的作用域是当前服
务器
WITH ENCRYPTION指示对创建文本加密存储
EXECUTE AS是执行登录触发器的环境
sql_statement是登录触发器的核心
13
SQL登录触发器
4.删除登录触发器
DROP TRIGGER trigger_name[,…n]
ON ALL SERVER
14
登录触发器的常见错误
用户按照上面的SQL语句创建登录触发器,当然不
会产生问题,但是,如果用户自己创建登录触发器时
,需要慎重。因为创建登录触发器与创建普通的表、
视图、存储过程或函数相比,具有一定的风险。
用户编写的登录触发器很容易造成SQL Server服务
不可访问的情况发生,这是因为登录触发器会影响正
常的会话过程。如果编写的登录触发器有些地方不正
确,那么将直接导致SQLCMD和Studio等不能访问
SQL Server服务。
15
登录触发器的常见错误
但此时SQL Server服务是良好的,只是SQL Server
服务在处理与用户的会话时,执行了错误的登录触发
器。此时必须使用DAC来连接SQL Server服务来删除
、禁用或修改该登录触发器。
16
DDL 触发器
在SQL Server 2008中,CREATE、ALTER、
DROP等语句常被称为数据定义语句,或数据定义语
言(Data Definition Language,DDL)。DDL触发器
就是与DDL相关联的一种特殊的存储过程。其实,
DDL触发器是关联DLL语句对应的SQL Server事件。
这类操作被关联相应的DDL触发器后,再执行这类操
作时,SQL Server将会自动执行相关联的DDL触发器
。DDL触发器是一种AFTER触发器,不存在于特定的
架构之下,而存在于特定数据库中或整个SQL Server
实例中。
17
DDL触发器的执行时机和作用
DLL触发器的执行时机是其关联的操作结束
以后至下一个操作开始之前。所以,DDL触发
器可以帮助用户管理和控制一些与数据库对象
定义有关的操作,如管理和记录数据库对象结
构的更改。总地来说,DDL触发器主要用在以
下几方面。
处理对数据库对象结构的更改。
在数据库对象的结构发生更改后,执行处罚期内
定义的操作。
可以记录数据库对象结构的具体更改,以及相应
的SQL Server事件。
18
DLL触发器关联的事件组
DLL触发器关联的事件组依据其使用范围,
分为服务器范围内的事件组和数据库范围内的
事件组。可以通过SQL语句,查询当前SQL
Server的事件组。
SELECT syst.parent_type,syst.type_name,syst.type
FROM sys.trigger_event types AS syst
WHERE syst.type_name LIKE ‘DDL_%’
GROUP BY
syst.parent_type,syst.type,syst.type_name
ORDER BY syst.parent_type,syst.type
19
DLL触发器关联的事件
在SQL Server中,与DLL触发器关联的事件
都属于预定义的事件组。下面将给出这些事件
的详细信息,包括事件的名称、type,以及
parent type。具体的,用户可以使用如下SQL
语句查询到与DLL触发器关联的事件。
SELECT syst.parent_type,syst.type_name,syst.type
FROM sys.trigger_event_types AS syst
WHERE syst.type_name NOT LIKE ‘DDL_%’
GROUP BY
syst.parent_type,syst.type,syst.type_name
ORDER BY syst.parent_type,syst.type_name
20
EVENTDATA函数:返回事件的XML结构
在编写DDL触发器时,可能需要获得服务器或数据
库事件的信息。这时,就需要使用EVENTDATA函数
,其语法结构如下。
返回值=EVENTDATA()
其中返回值的类型为XML。对于不同事件,
EVENTDATA函数返回的XML的属性和内容也不相同
,相应事件返回的属性保存在events.xsd文件中,该
文件路径为C:\Program Files\Microsoft SQL
Server\100\Tools\Binn\schemAS\SQL
Server\2006\11\events\events.xsd。
21
EVENTDATA函数:返回事件的XML结构
可以在SQL Server 2008中通过打开文件查看
xsd文件。在SQL Server 中,EVENTDATA函
数返回事件的XML结构就是events.xsd文件中
定义的,确切地讲,是由文件中的
<xs:complexType></xs:complexType>单元定
义的。以CREATE TABLE事件为例,其在
events.xsd中对应的单元内容如下:
22
EVENTDATA函数:返回事件的XML结构
<xs:complexType name=“EVENT_INSTANCE_CREATE_TABLE”>
<xs:sequence>
<xs:element name=“EventType” type=“SSWNAMEType”/>
<xs:element name=“PostTime” type=“xs:string”/>
<xs:element name=“SPID” type=“xs:INT”/>
<xs:element name=“ServerName” type=“PathType”/>
<xs:element name=“LoginName” type=“SSWNAMEType”/>
<xs:element name=“UserName” type=“SSWNAMEType”/>
<xs:element name=“DatabaseName” type=“SSWNAMEType”/>
<xs:element name=“SchemaName” type=“SSWNAMEType”/>
<xs:element name=“ObjectName” type=“SSWNAMEType”/>
<xs:element name=“ObjectType” type=“SSWNAMEType”/>
<xs:element name=“TSQLCommand”
type=“EventTag_TSQLCommand”/>
</xs:sequence>
</xs:complexType>
23
EVENTDATA函数:返回事件的XML结构
EVENTDATA函数返回的XML内容如下:
<EVENT_INSTANCE>
<EventType>CREATE_TABLE</EventType>
<PostTime>2008-07-09T 10:03:26.560</PostTime>
<SPID>54</SPID>
<ServerName>PYW\MSSQL2008</ServerName>
<LoginName>PYW\MSSQL2008</LoginName>
<UserName>dbo</UserName>
<DatabaseName>database_demo</DatabaseName>
<SchemaName>dbo</SchemaName>
<ObjectName>tb_CRL_DDL_trigger_test</ObjectName>
<ObjectType>TABLE</ObjectType>
<TSQLCommand>
<SetOptions ANSI_NULLS=“ON” ANSI_NULL_DEFAULT=“ON”
ANSI_PADDING=“ON” QUOTED_IDENTIFIER=“ON”
ENCRYPTED=“FALSE”/>
24
EVENTDATA函数:返回事件的XML结构
<CommandText>CREATE TABLE [dbo].[tb_CRL_DDL_trigger_test](
[id][INT] NULL,
[name] [NCHAR](10) NULL,
[in_date] [datetime] NULL
) ON [PRIMARY]
</CommandText>
</TSQLCommand>
</EVENT_INSTANCE>
这里主要使用XML的value()方法,从该XML中获取相应的值。由于
value()函数是从XML结构中获取信息的值,所以必须确保名称的大小写正
确,即在XML中是区分大小写的,否则将提取不到任何信息。
假设创建一个DDL触发器是使用CREATE_TABLE事件,那么
EVENTDATA函数返回的XML变量@data_xml可以通过value方法提取返回
的信息。
XML_value(‘(/EVENT_INSTANCE/UserName)[1]’,’NVARCHAR(2000)’)
25
SQL DDL触发器
所谓SQL DDL触发器,是指用普通的SQL语句编写
的DDL触发器,主要是区别于使用CLR编写的DDL触
发器。
1.创建SQL DDL触发器
CREATE TRIGGER语句支持SQL语句编写的DDL触
发器,也支持CLR触发器。
CREATE TRIGGER trigger_name
ON ALL SERVER|DATABASE
[WITH [ENCRYPTION|EXECUTE AS][,…n]]
FOR {event_type|event_group} [,…n]
AS
{sql_statement [][,…n]}
26
SQL DDL触发器
组成元素的意义:
trigger_name是DDL触发器的名称,不能以#和##开头
ALL SERVER是将DDL触发器的作用域指定为当前服务器
范围内,而DATABASE是指示DDL触发器保存于数据库范围
内
WITH ENCRYPTION指示将触发器文本加密存储
EXECUTE AS指定DDL触发器的执行环境
event_type是SQL Server事件的名称
event_group是SQL Server中预定义事件组的名称。该事
件组中包含一些特定的SQL Server事件
sql_statement是DDL触发器的核心
27
SQL DDL触发器
创建一个DDL触发器ddl_trigger。它将使用
EVENTDATA函数获得该触发器关联的事件信息,并
将其存储到数据库database_demo的表
dbo.ddl_trigger中。
USE database_demo
GO
CREATE TABLE tb_ddl_trigger
(id INT identity(1,1),name NCHAR(2000),inTime datetime)
GO
CREATE TRIGGER ddl_trigger
ON DATABASE
FOR CREATE_TABLE
28
SQL DDL触发器
AS
BEGIN
DECLARE @data_xml XML
SET @data_xml=EVENTDATA()
INSERT INTO
database_demo.dbo.tb_ddl_trigger(name,inTime)
VALUES(@data_xml.value(‘(/EVENT_INSTANCE/UserName)
[1]’,’NVARCHAR(2000)’),GETDATE())
END
由于创建触发器时使用了ON DATABASE,所以创建的DDL触
发器ddl_trigger被保存到了当前数据库中。如果使用ON ALL
SERVER那么创建的DDL触发器将存储在服务器对象中。
29
SQL DDL触发器
2.使用DDL触发器
在创建DDL触发器后,SQL Server将自动使用
该触发器,再执行DDL操作,系统将根据事件
查询到相应的DDL触发器,并予以执行。
USE database_demo
GO
CREATE TABLE dbo.Table_1(
id INT NOT NULL,hao NCHAR(10) NOT NULL)
GO
SELECT * FROM tb_ddl_trigger
30
SQL DDL触发器
3.更改DDL触发器
更改触发器也是用ALTER TRIGGER语句。
4.删除DDL触发器
删除触发器也是用DROP TRIGGER语句
31
SQL DDL触发器
5.查看DDL触发器的信息
SELECT syst.name AS ‘名称’,syst.parent_class_desc AS ‘作
用域’,syst.type_desc AS ‘种类’,syste.type_desc AS ‘事件
’,syssm.definition AS ‘定义’
FROM sys.triggers AS syst LEFT JOIN sys.trigger_events AS
syste ON syst.object_id=syste.object_id LEFT JOIN
sys.sql_modules AS syssm ON
syst.object_id=syssm.object_id
32
DML触发器
在SQL Server 2008中,INSERT、UPDATE
、DELETE等语句常被称为数据操纵语句,或
数据操纵语言(Data Manipulation Language
,DML)。DML触发器是与DML相关联的一种
特殊的存储过程。当DML操作被关联上相应的
DML触发器后,再执行这类操作时,SQL
Server将会自动执行相关联的DML触发器。显
然,DML触发器属于特定的数据库,是对数据
库中表或视图操作的限制和拓展。
33
DML触发器类型
DML触发器有两种分类方法:一类是依据
DML触发器关联的DML语句,将DML触发器分
为INSERT触发器、UPDATE触发器和DELETE
触发器;另一类是依据DML执行过程,将DML
触发器分为AFTER触发器和INSTEAD OF触发
器。对于一个DML操作来说,其执行过程如下
。
(1)SQL Server将创建inserted表(INSERT
和UPDATE语句)或deleted表(DELETE语句
)
(2)SQL Server执行触发操作。
34
DML触发器类型
(3)SQL Server执行INSTEAD OF触发器。该触发器直接位
于触发操作之后,用于完成一些与触发操作类似的功能,因此
称为“替代”触发器。其实,INSTEAD OF触发器的出现是为了
完善和拓展触发器的部分功能。不过,最好不要在不熟悉的情
况下使用INSTEAD OF触发器。
(4)SQL Server处理约束。这些约束就是前面数据完整性里
介绍的内容。如果INSERT、UPDATE或DELETE等语句不能
通过约束的限制,那么SQL Server将撤销前面执行INSTEAD
OF触发器的操作,并且就此退出此次DML操作,而不再执行下
面的AFTER触发器。
(5)SQL Server执行AFTER触发器。
35
DML触发器的作用
与DDL触发器相比,DML触发器的用途更为
广泛。这是因为SQL Server中DML语句要比
DDL多。所以,对于用户来说,掌握DML触发
器很重要。
DML触发器可以检查并避免错误的INSERT、
UPDATE、DELETE等DML操作,而且可以编写比
CHECK约束更为复杂的检验和限制。除此之外,
DML触发器还具有CHECK约束没有的特性,如DML
触发器可以引用其他表中的列。
36
DML触发器的作用
DML触发器可以比较数据修改前后的表,并根据
两者的差异采取相应的操作,主要是使用inseted表
或deleted表
一个表中的多个同类DML触发器允许定义多个不
同的操作来响应同一个修改语句
INSTEAD OF触发器还可以更新视图中两个基表
的记录。这一点在普通视图中是不可能的。
37
DML触发器的创建语句
在SQL Server中,创建DML触发器的语句也
是CREATE TRIGGER。不过,语法结构与前
面创建DDL触发器有所不同。创建DML触发器
的CREATE TRIGGER语句的语法结构如下。
CREATE TRIGGER [ schema_name . ]trigger_name
ON TABLE | VIEW
[ WITH [ ENCRYPTION ] [ EXECUTE AS clause ] [ ,...n ] ]
FOR | AFTER | INSTEAD OF
[ INSERT ] [ , ] [ UPDATE ] [ , ] [ DELETE ]
AS
sql_statement [ ] [ ,...n ]
38
DML触发器的创建语句
组成元素的意义:
schema_name.trigger_name是DML触发器的名
称
TABLE|VIEW是DML触发器操作的对象名称
WITH ENCRYPTION是对创建文本加密
EXECUTE AS是指定执行环境
FOR|AFTER|INSTEAD OF指定DML触发器的类
型,FOR和AFTER的含义相同。不能对视图视图定
义AFTER触发器。对于表或视图,每个DML语句最
多可以定义一个INSTEAD OF触发器
[INSERT][,][UPDATE][,][DELETE]是指定DML触
发器的类型
39
DML触发器的创建语句
sql_statement是DML触发器的核心,包含用于触
发DML触发器时相应的操作。有一些语句不允许或
不推荐在DML触发器中使用:
ALTER DATABASE
LOAD DATABASE
LOAD LOG
ALTER INDEX
DBCC DBREINDEX
CREATE DATABASE
RESTORE DATABASE
RESTORE LOG
CREATE INDEX
ALTER PARTITION FUNCTION
40
AFTER触发器
前面提到,AFTER触发器是DML操作触发器
的一类触发器。其实,如果对AFTER触发器进
行细分,那么AFTER触发器可以分为AFTERINSERT触发器、AFTER-UPDATE触发器以及
AFTER-DELETE触发器3类。
41
AFTER-INSERT触发器
所谓AFTER-INSERT触发器,是指该
INSERT触发器执行位于DML操作中AFTER触
发器的位置;而INSERT触发器是指当向表中
插入新记录数据时,SQL Server自动执行特殊
的存储过程。
42
AFTER-INSERT触发器
1.创建AFTER-INSERT触发器
在database_demo中创建一个表tb_1并在其上创建一
个AFTER-INSERT触发器。
USE database_demo
GO
CREATE TABLE tb_1(id INT,name NCHAR(10))
GO
CREATE TRIGGER dbo.insert_dml_trigger
ON tb_1
AFTER INSERT
AS
BEGIN
SELECT id,name,GETDATE() AS ‘时间’
FROM inserted
END
43
AFTER-INSERT触发器
2.查看AFTER-INSERT触发器
USE database_demo
GO
SELECT syst.name AS ‘DML触发器’,syso.name AS ‘
所在的表’
FROM sys.triggers AS syst INNER JOIN sys.objects
AS syso ON syst.parent_id=syso.object_id
当然也可以从Studio查看
44
AFTER-INSERT触发器
3.执行AFTER-INSERT触发器
向tb_1中插入一条记录时,会触发AFTERINSERT触发器。
USE database_demo
GO
INSERT INTO tb_1 VALUES(11,’LXP’)
45
AFTER-UPDATE触发器
所谓AFTER-UPDATE触发器,是指该
UPDATE触发器执行位于DML操作中AFTER触
发器的位置;而UPDATE触发器是指当更新表
中记录数据时,SQL Server自动执行特殊的存
储过程。
46
AFTER-UPDATE触发器
1.创建AFTER-UPDATE触发器
USE database_demo
GO
CREATE TABLE tb_2(id INT,name NCHAR(10))
GO
INSERT INTO tb_2 VALUES(11,’LXP’)
GO
CREATE TRIGGER dbo.update_dml_trigger
ON tb_2
AFTER UPDATE
AS
BEGIN
SELECT id,name,GETDATE() AS ‘时间’
FROM inserted
END
47
AFTER-UPDATE触发器
2.查看AFTER-UPDATE触发器
USE database_demo
GO
SELECT syst.name AS ‘DML触发器’,syso.name AS ‘
所在的表’
FROM sys.triggers AS syst INNER JOIN sys.objects
AS syso
ON syst.parent_id=syso.object_id
48
AFTER-UPDATE触发器
3.执行AFTER-UPDATE触发器
USE database_demo
GO
UPDATE tb_2 SET name=‘LIN’
WHERE id=11
49
AFTER-DELETE触发器
所谓AFTER-DELETE触发器,是指该
DELETE触发器执行位于DML操作中AFTER触
发器的位置;而DELETE触发器是指当更新表
中记录数据时,SQL Server自动执行特殊的存
储过程。
50
AFTER-DELETE触发器
1.创建AFTER-DELETE触发器
USE database_demo
GO
CREATE TABLE tb_3(id INT,name NCHAR(10))
GO
CREATE TRIGGER dbo.delete_dml_trigger
ON tb_3
AFTER DELETE
AS
BEGIN
SELECT id,name,GETDATE() AS ‘时间’
FROM deleted
END
51
AFTER-DELETE触发器
2.查看AFTER-DELETE触发器
USE database_demo
GO
SELECT syst.name AS ‘DML触发器
’,syso.name AS ‘所在的表’
FROM sys.triggers AS syst INNER JOIN
sys.objects AS syso
ON syst.parent_id=syso.object_id
52
AFTER-DELETE触发器
3.执行AFTER-DELETE触发器
USE database_demo
GO
DELETE FROM tb_3 WHERE id=11
53
INSTEAD OF触发器
前面已经介绍了创建AFTER触发器,不过,
AFTER触发器仅支持表。本节介绍的INSTEAD
OF触发器也包括AFTER-INSERT触发器、
AFTER-UPDATE触发器和AFTER-DELETE触
发器,但它除支持表外,还支持视图。由于前
面已经详细介绍了AFTER触发器的创建、查看
以及使用方法,因此这里不再赘述。本节将通
过示例来介绍为视图创建INSTEAD OF触发器
。
54
INSTEAD OF触发器
创建两个基表tb_4和tb_5,基于这两个表创
建视图v_1,然后为v_1创建一个INSTEAD OF
类型的INSERT 触发器。
USE database_demo
GO
CREATE TABLE tb_4(id INT,name NCHAR(10))
GO
CREATE TABLE tb_5(id INT,salary INT)
GO
CREATE VIEW v_1(id,name,salary)
AS
55
INSTEAD OF触发器
SELECT tb_4.id,tb_4.name,tb_5.salary
FROM tb_4 INNER JOIN tb_5 ON tb_4.id=tb_5.id
GO
CREATE TRIGGER
dbo.insert_INSTEADOF_dml_trigger
ON v_1
INSTEAD OF INSERT
AS
BEGIN
INSERT INTO tb_4
SELECT id,name FROM inserted
56
INSTEAD OF触发器
INSERT INTO tb_5
SELECT id,salary FROM inserted
SELECT * FROM tb_4
SELECT * FROM tb_5
END
GO
57
INSTEAD OF触发器
向v_1的两个基表同时插入记录,触发器会返
回新插入的记录信息。
USE database_demo
GO
INSERT INTO v_1 VALUES(1,’LIN’,1200)
58
小结
本章主要介绍触发器的使用。触发器是数据
约束很重要的一个技术手段,在更新某个数
据时还要修改谁、修改什么,主要用触发器
来完成。由于在系统优先级方面,触发器高
于存储过程,所以在对重要数据更新时,一
般都使用触发器,但滥用触发器也会降低系
统速度。触发器是双刃剑,在使用时,一定
要谨慎。
59
© Copyright 2026 ExpyDoc