SQL CLR 触发器 - 获取源代码 table
SQL CLR Trigger - get source table
我正在使用 Microsoft SQL Server 2012 中的 SQL CLR 触发器创建数据库同步引擎。这些触发器不调用存储过程或函数(因此可以访问 INSERTED 和 DELETED伪tables 但无权访问@@procid).
差异here,供参考。
此 "sync engine" 使用映射 table 来确定此同步作业的 table 和字段映射。为了确定目标 table 和字段(来自我的映射 table),我需要从触发器本身获取源 table 名称。我在 Stack Overflow 和其他网站上遇到过很多答案,都说这是不可能的。但是,我发现了一个 website 提供了线索:
可能的解决方案:
using (SqlConnection lConnection = new SqlConnection(@"context connection=true")) {
SqlCommand cmd = new SqlCommand("SELECT object_name(resource_associated_entity_id) FROM sys.dm_tran_locks WHERE request_session_id = @@spid and resource_type = 'OBJECT'", lConnection);
cmd.CommandType = CommandType.Text;
var obj = cmd.ExecuteScalar();
}
这实际上是 return 正确的 table 名称。
问题:
我的问题是,这个潜在解决方案的可靠性如何? @@spid 实际上仅限于这个单一触发器执行吗?或者是否有可能其他同时触发将在此进程 ID 内重叠?它能承受数据库中相同 and/or 不同触发器的多次执行吗?
从这些站点来看,似乎进程 ID 实际上仅限于打开的连接,不重叠:here, here, and here。
这是获取我的来源的安全方法吗table?
为什么?
正如我注意到的类似问题,但都没有针对我的具体情况的有效答案(那个除外)。这些网站上的大多数评论都询问 "Why?",为了抢占先机,原因如下:
这个同步引擎在单个数据库上运行,可以将更改推送到目标 tables,使用用户定义的转换、自动源到目标类型转换和解析来转换数据,甚至可以使用CSharpCodeProvider 执行方法也存储在那些映射 tables 中用于转换数据。它已经建成,非常健壮,并且对我们正在做的事情有很好的性能指标。我现在正在尝试构建它以允许 1:n table 更改(包括扩展 tables 需要与 'master' table 相同的 ID)和我正在尝试 "genericise" 代码。以前每个触发器都有一个硬编码的 "target table" 定义,我使用我的映射 table 来确定源。现在我想获取源 table 并使用我的映射 table 来确定所有目标 table。这在中等负载环境中使用,并将更改推送到 "Change Order Book",一个单独的服务器进程选择它来完成 CRUD 操作。
编辑
如评论中所述,上面列出的查询非常"iffy"。它通常(例如 SQL 服务器重启后)return 系统对象,如 syscolpars 或 sysidxstats。但是,似乎在 dm_tran_locks table 中总是有一个关联的 resource_type of 'RID'(行 ID)具有相同的 object_name。到目前为止,我当前可靠运行的查询如下(如果此更改或在高负载测试下不起作用,将会更新):
select t1.ObjectName FROM (
SELECT object_name(resource_associated_entity_id) as ObjectName
FROM sys.dm_tran_locks WHERE resource_type = 'OBJECT' and request_session_id = @@spid
) t1 inner join (
SELECT OBJECT_NAME(partitions.OBJECT_ID) as ObjectName
FROM sys.dm_tran_locks
INNER JOIN sys.partitions ON partitions.hobt_id = dm_tran_locks.resource_associated_entity_id
WHERE resource_type = 'RID'
) t2 on t1.ObjectName = t2.ObjectName
如果总是这样,我将不得不在测试期间找出答案。
How reliable is this potential solution?
虽然我没有时间设置测试用例来证明它不起作用,但我发现了这种方法(甚至考虑了 Edit 部分中的查询)"iffy"(即不保证到总是是可靠的)。
主要关注点是:
- 级联(无论是否递归)触发执行
- 用户(即显式/隐式)交易
- 子流程(即
EXEC
和 sp_executesql
)
这些场景允许同时锁定多个对象。
Is the @@SPID actually limited to this single trigger execution? Or is it possible that other simultaneous triggers will overlap within this process id?
和(来自对问题的评论):
I think I can join my query up with the sys.partitions
and get a dm_trans_lock
that has a type of 'RID' with an object name that will match up to the one in my original query.
这就是为什么它不应该完全可靠的原因:会话 ID(即 @@SPID
)对于该连接上的所有请求都是不变的)。因此,所有子流程(即 EXEC
调用、sp_executesql
、触发器等)都将在同一个 @@SPID
/ session_id
上。因此,在子流程和用户事务之间,您可以很容易地锁定多个资源,所有资源都在同一个会话 ID 上。
我说 "resources" 而不是 "OBJECT" 甚至 "RID" 的原因是锁可以发生在:行、页、键、table、模式、存储过程、数据库本身等。不止一件事可以被认为是一个 "OBJECT",并且您可能拥有页锁而不是行锁。
Will it stand up to multiple executions of the same and/or different triggers within the database?
只要这些执行发生在不同的会话中,那么它们就不是问题。
话虽如此,我可以看到简单的测试在哪里可以表明您当前的方法是可靠的。但是,添加更详细的测试也应该足够容易,其中包括一个显式事务,该事务首先在另一个 table 上执行一些 DML,或者在一个 table 上触发一个,在其中一个上执行一些 DML tables等
不幸的是,没有内置机制提供与 @@PROCID
为 T-SQL 触发器提供的相同功能。我想出了一个 应该 允许为 SQLCLR 触发器获取父 table 的方案(考虑到这些不同的问题),但是没有有机会测试一下。它需要使用 T-SQL 触发器,设置为 "first" 触发器,以设置可以被 SQLCLR 触发器发现的信息。
可以使用 CONTEXT_INFO
、如果您还没有将它用于其他用途(并且如果您还没有 "first" 触发设置)。在这种方法中,您仍然会创建一个 T-SQL 触发器,然后在 SQLCLR 触发器的上下文连接上使用 sp_settriggerorder. In this Trigger you SET CONTEXT_INFO to the table name that is the parent of @@PROCID
. You can then read CONTEXT_INFO() 将其设置为 "first" 触发器。如果有多个级别的触发器,那么 CONTEXT INFO 的值将被覆盖,因此读取该值必须是您在每个 SQLCLR 触发器中做的第一件事。
这是一个旧线程,但它是一个常见问题解答,我认为我有更好的解决方案。本质上,它使用插入或删除的 table 的模式通过对列名进行散列并将散列与 table 的散列与 CLR 触发器进行比较来查找基础 table在他们。
下面的代码片段 - 在某些时候我可能会将整个解决方案放在 Git 上(它会在触发器触发时向 Azure 服务总线发送消息)。
private const string colqry = "select top 1 * from inserted union all select top 1 * from deleted";
private const string hashqry = "WITH cols as ( "+
"select top 100000 c.object_id, column_id, c.[name] "+
"from sys.columns c "+
"JOIN sys.objects ot on (c.object_id= ot.parent_object_id and ot.type= 'TA') " +
"order by c.object_id, column_id ) "+
"SELECT s.[name] + '.' + o.[name] as 'TableName', CONVERT(NCHAR(32), HASHBYTES('MD5',STRING_AGG(CONVERT(NCHAR(32), HASHBYTES('MD5', cols.[name]), 2), '|')),2) as 'MD5Hash' " +
"FROM cols "+
"JOIN sys.objects o on (cols.object_id= o.object_id) "+
"JOIN sys.schemas s on (o.schema_id= s.schema_id) "+
"WHERE o.is_ms_shipped = 0 "+
"GROUP BY s.[name], o.[name]";
public static void trgSendSBMsg()
{
string table = "";
SqlCommand cmd;
SqlDataReader rdr;
SqlTriggerContext trigContxt = SqlContext.TriggerContext;
SqlPipe p = SqlContext.Pipe;
using (SqlConnection con = new SqlConnection("context connection=true"))
{
try
{
con.Open();
string tblhash = "";
using (cmd = new SqlCommand(colqry, con))
{
using (rdr = cmd.ExecuteReader(CommandBehavior.SingleResult))
{
if (rdr.Read())
{
MD5 hash = MD5.Create();
StringBuilder hashstr = new StringBuilder(250);
for (int i=0; i < rdr.FieldCount; i++)
{
if (i > 0) hashstr.Append("|");
hashstr.Append(GetMD5Hash(hash, rdr.GetName(i)));
}
tblhash = GetMD5Hash(hash, hashstr.ToString().ToUpper()).ToUpper();
}
rdr.Close();
}
}
using (cmd = new SqlCommand(hashqry, con))
{
using (rdr = cmd.ExecuteReader(CommandBehavior.SingleResult))
{
while (rdr.Read())
{
string hash = rdr.GetString(1).ToUpper();
if (hash == tblhash)
{
table = rdr.GetString(0);
break;
}
}
rdr.Close();
}
}
if (table.Length == 0)
{
p.Send("Error: Unable to find table that CLR trigger is on. Message not sent!");
return;
}
….
HTH
我正在使用 Microsoft SQL Server 2012 中的 SQL CLR 触发器创建数据库同步引擎。这些触发器不调用存储过程或函数(因此可以访问 INSERTED 和 DELETED伪tables 但无权访问@@procid).
差异here,供参考。
此 "sync engine" 使用映射 table 来确定此同步作业的 table 和字段映射。为了确定目标 table 和字段(来自我的映射 table),我需要从触发器本身获取源 table 名称。我在 Stack Overflow 和其他网站上遇到过很多答案,都说这是不可能的。但是,我发现了一个 website 提供了线索:
可能的解决方案:
using (SqlConnection lConnection = new SqlConnection(@"context connection=true")) {
SqlCommand cmd = new SqlCommand("SELECT object_name(resource_associated_entity_id) FROM sys.dm_tran_locks WHERE request_session_id = @@spid and resource_type = 'OBJECT'", lConnection);
cmd.CommandType = CommandType.Text;
var obj = cmd.ExecuteScalar();
}
这实际上是 return 正确的 table 名称。
问题:
我的问题是,这个潜在解决方案的可靠性如何? @@spid 实际上仅限于这个单一触发器执行吗?或者是否有可能其他同时触发将在此进程 ID 内重叠?它能承受数据库中相同 and/or 不同触发器的多次执行吗?
从这些站点来看,似乎进程 ID 实际上仅限于打开的连接,不重叠:here, here, and here。
这是获取我的来源的安全方法吗table?
为什么?
正如我注意到的类似问题,但都没有针对我的具体情况的有效答案(那个除外)。这些网站上的大多数评论都询问 "Why?",为了抢占先机,原因如下:
这个同步引擎在单个数据库上运行,可以将更改推送到目标 tables,使用用户定义的转换、自动源到目标类型转换和解析来转换数据,甚至可以使用CSharpCodeProvider 执行方法也存储在那些映射 tables 中用于转换数据。它已经建成,非常健壮,并且对我们正在做的事情有很好的性能指标。我现在正在尝试构建它以允许 1:n table 更改(包括扩展 tables 需要与 'master' table 相同的 ID)和我正在尝试 "genericise" 代码。以前每个触发器都有一个硬编码的 "target table" 定义,我使用我的映射 table 来确定源。现在我想获取源 table 并使用我的映射 table 来确定所有目标 table。这在中等负载环境中使用,并将更改推送到 "Change Order Book",一个单独的服务器进程选择它来完成 CRUD 操作。
编辑
如评论中所述,上面列出的查询非常"iffy"。它通常(例如 SQL 服务器重启后)return 系统对象,如 syscolpars 或 sysidxstats。但是,似乎在 dm_tran_locks table 中总是有一个关联的 resource_type of 'RID'(行 ID)具有相同的 object_name。到目前为止,我当前可靠运行的查询如下(如果此更改或在高负载测试下不起作用,将会更新):
select t1.ObjectName FROM (
SELECT object_name(resource_associated_entity_id) as ObjectName
FROM sys.dm_tran_locks WHERE resource_type = 'OBJECT' and request_session_id = @@spid
) t1 inner join (
SELECT OBJECT_NAME(partitions.OBJECT_ID) as ObjectName
FROM sys.dm_tran_locks
INNER JOIN sys.partitions ON partitions.hobt_id = dm_tran_locks.resource_associated_entity_id
WHERE resource_type = 'RID'
) t2 on t1.ObjectName = t2.ObjectName
如果总是这样,我将不得不在测试期间找出答案。
How reliable is this potential solution?
虽然我没有时间设置测试用例来证明它不起作用,但我发现了这种方法(甚至考虑了 Edit 部分中的查询)"iffy"(即不保证到总是是可靠的)。
主要关注点是:
- 级联(无论是否递归)触发执行
- 用户(即显式/隐式)交易
- 子流程(即
EXEC
和sp_executesql
)
这些场景允许同时锁定多个对象。
Is the @@SPID actually limited to this single trigger execution? Or is it possible that other simultaneous triggers will overlap within this process id?
和(来自对问题的评论):
I think I can join my query up with the
sys.partitions
and get adm_trans_lock
that has a type of 'RID' with an object name that will match up to the one in my original query.
这就是为什么它不应该完全可靠的原因:会话 ID(即 @@SPID
)对于该连接上的所有请求都是不变的)。因此,所有子流程(即 EXEC
调用、sp_executesql
、触发器等)都将在同一个 @@SPID
/ session_id
上。因此,在子流程和用户事务之间,您可以很容易地锁定多个资源,所有资源都在同一个会话 ID 上。
我说 "resources" 而不是 "OBJECT" 甚至 "RID" 的原因是锁可以发生在:行、页、键、table、模式、存储过程、数据库本身等。不止一件事可以被认为是一个 "OBJECT",并且您可能拥有页锁而不是行锁。
Will it stand up to multiple executions of the same and/or different triggers within the database?
只要这些执行发生在不同的会话中,那么它们就不是问题。
话虽如此,我可以看到简单的测试在哪里可以表明您当前的方法是可靠的。但是,添加更详细的测试也应该足够容易,其中包括一个显式事务,该事务首先在另一个 table 上执行一些 DML,或者在一个 table 上触发一个,在其中一个上执行一些 DML tables等
不幸的是,没有内置机制提供与 @@PROCID
为 T-SQL 触发器提供的相同功能。我想出了一个 应该 允许为 SQLCLR 触发器获取父 table 的方案(考虑到这些不同的问题),但是没有有机会测试一下。它需要使用 T-SQL 触发器,设置为 "first" 触发器,以设置可以被 SQLCLR 触发器发现的信息。
可以使用 CONTEXT_INFO
、如果您还没有将它用于其他用途(并且如果您还没有 "first" 触发设置)。在这种方法中,您仍然会创建一个 T-SQL 触发器,然后在 SQLCLR 触发器的上下文连接上使用 sp_settriggerorder. In this Trigger you SET CONTEXT_INFO to the table name that is the parent of @@PROCID
. You can then read CONTEXT_INFO() 将其设置为 "first" 触发器。如果有多个级别的触发器,那么 CONTEXT INFO 的值将被覆盖,因此读取该值必须是您在每个 SQLCLR 触发器中做的第一件事。
这是一个旧线程,但它是一个常见问题解答,我认为我有更好的解决方案。本质上,它使用插入或删除的 table 的模式通过对列名进行散列并将散列与 table 的散列与 CLR 触发器进行比较来查找基础 table在他们。
下面的代码片段 - 在某些时候我可能会将整个解决方案放在 Git 上(它会在触发器触发时向 Azure 服务总线发送消息)。
private const string colqry = "select top 1 * from inserted union all select top 1 * from deleted";
private const string hashqry = "WITH cols as ( "+
"select top 100000 c.object_id, column_id, c.[name] "+
"from sys.columns c "+
"JOIN sys.objects ot on (c.object_id= ot.parent_object_id and ot.type= 'TA') " +
"order by c.object_id, column_id ) "+
"SELECT s.[name] + '.' + o.[name] as 'TableName', CONVERT(NCHAR(32), HASHBYTES('MD5',STRING_AGG(CONVERT(NCHAR(32), HASHBYTES('MD5', cols.[name]), 2), '|')),2) as 'MD5Hash' " +
"FROM cols "+
"JOIN sys.objects o on (cols.object_id= o.object_id) "+
"JOIN sys.schemas s on (o.schema_id= s.schema_id) "+
"WHERE o.is_ms_shipped = 0 "+
"GROUP BY s.[name], o.[name]";
public static void trgSendSBMsg()
{
string table = "";
SqlCommand cmd;
SqlDataReader rdr;
SqlTriggerContext trigContxt = SqlContext.TriggerContext;
SqlPipe p = SqlContext.Pipe;
using (SqlConnection con = new SqlConnection("context connection=true"))
{
try
{
con.Open();
string tblhash = "";
using (cmd = new SqlCommand(colqry, con))
{
using (rdr = cmd.ExecuteReader(CommandBehavior.SingleResult))
{
if (rdr.Read())
{
MD5 hash = MD5.Create();
StringBuilder hashstr = new StringBuilder(250);
for (int i=0; i < rdr.FieldCount; i++)
{
if (i > 0) hashstr.Append("|");
hashstr.Append(GetMD5Hash(hash, rdr.GetName(i)));
}
tblhash = GetMD5Hash(hash, hashstr.ToString().ToUpper()).ToUpper();
}
rdr.Close();
}
}
using (cmd = new SqlCommand(hashqry, con))
{
using (rdr = cmd.ExecuteReader(CommandBehavior.SingleResult))
{
while (rdr.Read())
{
string hash = rdr.GetString(1).ToUpper();
if (hash == tblhash)
{
table = rdr.GetString(0);
break;
}
}
rdr.Close();
}
}
if (table.Length == 0)
{
p.Send("Error: Unable to find table that CLR trigger is on. Message not sent!");
return;
}
….
HTH