当您说“不使用触发器”时,您是指表上的任何触发器还是逐行触发器?
我问是因为您可以通过明智地使用该CONTEXT_INFO()
函数来获得所需的内容,但是您需要确保SET CONTEXT_INFO
在执行操作之前正确调用了该函数。
一个做到这一点的地方可能是服务器级别的登录触发器(即不是数据库/对象级别的触发器),如下所示:
USE master
GO
CREATE TRIGGER tr_audit_login
ON ALL SERVER
WITH EXECUTE AS 'sa'
AFTER LOGON
AS BEGIN
BEGIN TRY
DECLARE @eventdata XML = EVENTDATA();
IF @eventdata IS NOT NULL BEGIN
DECLARE @spid INT;
DECLARE @client_host VARCHAR(64);
SET @client_host = @eventdata.value('(/EVENT_INSTANCE/ClientHost)[1]', 'VARCHAR(64)');
SET @spid = @eventdata.value('(/EVENT_INSTANCE/SPID)[1]', 'INT');
-- pack the required data into the context data binary
-- (spid is just an example of packing multiple data items in a single field: you would probably use @@SPID at the point of use, instead)
DECLARE @context_data VARBINARY(128);
SET @context_data = CONVERT(VARBINARY(4), @spid)
+ CONVERT(VARBINARY(64), @client_host);
-- persist the spid and host into session-level memory
SET CONTEXT_INFO @context_data;
END
END TRY
BEGIN CATCH
/* do better error handling here...
* logon trigger can lock all users out of server, so i am just swallowing everything
*/
DECLARE @msg NVARCHAR(4000) = ERROR_MESSAGE();
RAISERROR('%s', 10, 1, @msg) WITH LOG;
END CATCH
END
然后可以将默认约束添加到表中以存储上下文(以提高插入速度):
ALTER TABLE cdc.schema_table_CT
ADD ContextInfo varbinary(128) NULL DEFAULT(CONTEXT_INFO())
一旦有了,就可以用一刀切的方式查询该ContextInfo
列:
SELECT *
,spid = CONVERT(INT, SUBSTRING(ContextInfo, 1, 4))
,client = CONVERT(VARCHAR(64), SUBSTRING(ContextInfo, 5, 64))
FROM cdc.schema_table_CT
从技术上讲,你能做到这一点SUBSTRING
和CONVERT
东西作为默认约束的一部分,并且只存储客户端IP存在,但它可能会更快存储整个上下文中出现(因为它是在每一个完成的INSERT
),并且只提取一个值SELECT
当您需要它们时。
我可能倾向于将所有我的调用SUBSTRING
和CONVERT
调用包装在一个单行内联表值函数中,CROSS APPLY
必要时将使用它。这样可以将拆包逻辑放在一个地方:
CREATE FUNCTION fn_context (
@context_info VARBINARY(128)
)
RETURNS TABLE
AS RETURN (
SELECT
spid = CONVERT(INT, SUBSTRING(@context_info, 1, 4))
,client = CONVERT(VARCHAR(64), SUBSTRING(@context_info, 5, 64))
)
GO
SELECT *
FROM cdc.schema_table_CT s
CROSS APPLY dbo.fn_context(s.ContextInfo) c
请注意,CONTEXT_INFO
只有128个字节VARBINARY
。如果您需要的数据超出了128字节的容纳空间,我将创建一个表来保存所有数据,在登录触发器中将该行作为“会话”的行插入表中,并设置CONTEXT_INFO
为该表的替代键值
您还应注意,由于这只是默认约束,因此对于具有适当特权的用户而言,覆盖静态表中的上下文数据是微不足道的。当然,“ audit”样式表中的所有其他列也是如此。
如果它可以是一个持久化的计算列,而不是默认列,那将是很好的选择,但是该CONTEXT_INFO()
函数是不确定的,因此是不可行的(您可能可以FUNCTION
在a周围使用一些技巧VIEW
,但是我不会)。
对于具有足够访问权限的用户来说,调用SET CONTEXT_INFO
自己并弄乱您的一天(例如,使用伪造的值或特制的存储进样)也很琐碎,因此请谨慎对待内容,在显示之前对其进行编码,并处理异常好。
至于主机名,我认为ClientHost
元素EVENTDATA()
为您提供IP地址(或<local machine>
指示符)。从技术上讲,您可以使用CLR将DNS反向查找回主机名,但对于每个主机来说,这样做往往太慢了INSERT
,因此我建议不要这样做。
如果必须具有主机名,则可能需要使用SQL代理作业定期使用本地DHCP服务器或DNS区域文件中的当前租约(作为带外进程)填充一个单独的表,并LEFT JOIN
使用将来的查询(或包装标量FUNCTION
以为默认约束提供时间点的值)。
同样,您应该注意,如果应用程序具有任何面向公众的组件,则IP地址和主机名将不可靠(例如,由于NAT)。即使它不是面向公众的,但对于大多数IP /主机名映射,还是有一些基于时间的组件可能需要考虑在内。
最后,在实施登录触发器之前,可能有必要打开服务器的专用管理员连接。如果登录触发器以任何方式中断,它可以阻止所有用户登录(包括sysadmin帐户):
USE master
GO
-- you may want to do this, so you have a back-out if the login trigger breaks login
EXEC sp_configure 'remote admin connections', 1
GO
RECONFIGURE
GO
如果确实被锁定,则可以使用DAC删除或禁用登录触发器:
C:\> sqlcmd -S localhost -d master -A
1> DISABLE TRIGGER tr_audit_login ON ALL SERVER
2> GO