SQL 使用临时 table 的服务器连接上下文不能用于使用 SqlDataAdapter.Fill 调用的存储过程
SQL Server connection context using temporary table cannot be used in stored procedures called with SqlDataAdapter.Fill
我想要一些可用于任何存储过程的信息,例如当前用户。按照 here 指示的临时 table 方法,我尝试了以下方法:
1) 在连接打开时创建临时 table
private void setConnectionContextInfo(SqlConnection connection)
{
if (!AllowInsertConnectionContextInfo)
return;
var username = HttpContext.Current?.User?.Identity?.Name ?? "";
var commandBuilder = new StringBuilder($@"
CREATE TABLE #ConnectionContextInfo(
AttributeName VARCHAR(64) PRIMARY KEY,
AttributeValue VARCHAR(1024)
);
INSERT INTO #ConnectionContextInfo VALUES('Username', @Username);
");
using (var command = connection.CreateCommand())
{
command.Parameters.AddWithValue("Username", username);
command.ExecuteNonQuery();
}
}
/// <summary>
/// checks if current connection exists / is closed and creates / opens it if necessary
/// also takes care of the special authentication required by V3 by building a windows impersonation context
/// </summary>
public override void EnsureConnection()
{
try
{
lock (connectionLock)
{
if (Connection == null)
{
Connection = new SqlConnection(ConnectionString);
Connection.Open();
setConnectionContextInfo(Connection);
}
if (Connection.State == ConnectionState.Closed)
{
Connection.Open();
setConnectionContextInfo(Connection);
}
}
}
catch (Exception ex)
{
if (Connection != null && Connection.State != ConnectionState.Open)
Connection.Close();
throw new ApplicationException("Could not open SQL Server Connection.", ex);
}
}
2) 使用以下函数测试了用于使用 SqlDataAdapter.Fill
填充 DataTable
的过程:
public DataTable GetDataTable(String proc, Dictionary<String, object> parameters, CommandType commandType)
{
EnsureConnection();
using (var command = Connection.CreateCommand())
{
if (Transaction != null)
command.Transaction = Transaction;
SqlDataAdapter adapter = new SqlDataAdapter(proc, Connection);
adapter.SelectCommand.CommandTimeout = CommonConstants.DataAccess.DefaultCommandTimeout;
adapter.SelectCommand.CommandType = commandType;
if (Transaction != null)
adapter.SelectCommand.Transaction = Transaction;
ConstructCommandParameters(adapter.SelectCommand, parameters);
DataTable dt = new DataTable();
try
{
adapter.Fill(dt);
return dt;
}
catch (SqlException ex)
{
var err = String.Format("Error executing stored procedure '{0}' - {1}.", proc, ex.Message);
throw new TptDataAccessException(err, ex);
}
}
}
3) 被调用的过程会像这样尝试获取用户名:
DECLARE @username VARCHAR(128) = (select AttributeValue FROM #ConnectionContextInfo where AttributeName = 'Username')
但是 #ConnectionContextInfo
在上下文中不再可用。
我已经在数据库中放置了一个 SQL 分析器,以检查发生了什么:
- 临时table使用某个SPID创建成功
- 使用相同的 SPID 调用过程
为什么临时 table 在过程范围内不可用?
在 T-SQL 中进行以下工作:
- 创建临时 table
- 调用需要来自特定临时文件的数据的过程table
- 临时 table 仅显式删除或在当前范围结束后删除
谢谢。
如本 answer 所示,当 CommandType
为 CommandType.Text
并且命令具有参数时,ExecuteNonQuery
使用 sp_executesql
。
这个问题中的 C# 代码没有明确设置 CommandType
,它是 Text
by default,所以代码的最终结果很可能是 CREATE TABLE #ConnectionContextInfo
被包装到 sp_executesql
。您可以在 SQL Profiler 中验证它。
众所周知sp_executesql
是运行ning在自己的范围内(本质上是一个嵌套的存储过程)。搜索 "sp_executesql temp table"。这是一个示例:Execute sp_executeSql for select...into #table but Can't Select out Temp Table Data
因此,在 sp_executesql
的嵌套范围内创建了临时 table #ConnectionContextInfo
,并在 sp_executesql
returns 后立即自动删除。
adapter.Fill
运行 的以下查询没有看到此临时 table。
怎么办?
确保 CREATE TABLE #ConnectionContextInfo
语句未包含在 sp_executesql
.
中
在您的情况下,您可以尝试将包含 CREATE TABLE #ConnectionContextInfo
和 INSERT INTO #ConnectionContextInfo
的单个批次拆分为两个批次。第一个 batch/query 将只包含没有任何参数的 CREATE TABLE
语句。第二个 batch/query 将包含带有参数的 INSERT INTO
语句。
我不确定它是否有帮助,但值得一试。
如果这不起作用,您可以构建一个 T-SQL 批处理来创建一个临时 table,将数据插入其中并调用您的存储过程。全部在一个 SqlCommand 中,全部在一个批次中。整个 SQL 将被包裹在 sp_executesql
中,但这并不重要,因为创建 temp table 的范围与调用存储过程的范围相同。从技术上讲它会起作用,但我不建议遵循此路径。
这里不是问题的答案,而是解决问题的建议。
老实说,整个方法看起来很奇怪。如果您想将一些数据传递到存储过程中,为什么不使用此存储过程的参数。这就是它们的用途 - 将数据传递到过程中。没有真正需要为此使用 temp table 。如果您传递的数据很复杂,您可以使用 table 值参数 (T-SQL, .NET)。如果只是一个 Username
.
绝对是大材小用
您的存储过程需要知道临时 table,它需要知道它的名称和结构,所以我不明白显式 table 值有什么问题参数代替。即使您的程序代码也不会改变太多。您将使用 @ConnectionContextInfo
而不是 #ConnectionContextInfo
。
仅当您使用 SQL Server 2005 或更早版本(没有 table 值参数)时,对您描述的内容使用临时 tables 才对我有意义。它们是在 SQL Server 2008 中添加的。
"There are two types of temporary tables: local and global. They differ from each other in their names, their visibility, and their availability. Local temporary tables have a single number sign (#) as the first character of their names; they are visible only to the current connection for the user, and they are deleted when the user disconnects from the instance of SQL Server. Global temporary tables have two number signs (##) as the first characters of their names; they are visible to any user after they are created, and they are deleted when all users referencing the table disconnect from the instance of SQL Server." -- 来自 here
所以你的问题的答案是 ## 而不是 # 使本地临时 table 成为全局。
小问题:我暂时假设问题中发布的代码不是 运行 的完整代码。不仅使用了我们没有看到声明的变量(例如 AllowInsertConnectionContextInfo
),而且 setConnectionContextInfo
方法中还有一个明显的遗漏:command
对象被创建但从未被创建它的 CommandText
属性 设置为 commandBuilder.ToString()
,因此它看起来是一个空的 SQL 批次。我确定这实际上得到了正确处理,因为 1) 我相信提交一个空批次会产生异常,并且 2) 问题确实提到了临时 table 创建出现在 SQL 探查器中输出。尽管如此,我还是要指出这一点,因为这意味着可能存在与问题中未显示的观察到的行为相关的其他代码,这使得给出准确答案变得更加困难。
如@Vladimir 的罚款 所述,由于子流程中的查询 运行(即 sp_executesql
),本地临时对象 -- tables 和存储过程 -- 在该子进程完成后无法存活,因此在父上下文中不可用。
全局临时对象和 permanent/non-temporary 对象将在子进程完成后继续存在,但是这两个选项在它们的典型用法中都会引入并发问题:您需要先测试是否存在试图创建 table、 和 您需要一种方法来区分一个进程与另一个进程。所以这些并不是一个很好的选择,至少在它们的典型用法中不是(稍后会详细介绍)。
假设您不能将任何值传递到存储过程中(否则您可以按照@Vladimir 在他的回答中建议的那样简单地传递 username
),您有几个选择:
根据当前代码,最简单的解决方案是将本地临时 table 的创建与 INSERT
命令分开(@Vladimir 的回答中也提到) .如前所述,您遇到的问题是由 sp_executesql
中的查询 运行 引起的。使用 sp_executesql
的原因是为了处理参数 @Username
。因此,修复可以像将当前代码更改为以下一样简单:
string _Command = @"
CREATE TABLE #ConnectionContextInfo(
AttributeName VARCHAR(64) PRIMARY KEY,
AttributeValue VARCHAR(1024)
);";
using (var command = connection.CreateCommand())
{
command.CommandText = _Command;
command.ExecuteNonQuery();
}
_Command = @"
INSERT INTO #ConnectionContextInfo VALUES ('Username', @Username);
");
using (var command = connection.CreateCommand())
{
command.CommandText = _Command;
// do not use AddWithValue()!
SqlParameter _UserName = new SqlParameter("@Username", SqlDbType.NVarChar, 128);
_UserName.Value = username;
command.Parameters.Add(_UserName);
command.ExecuteNonQuery();
}
请注意,在 T-SQL 用户定义函数或 Table-Valued 函数中无法访问临时对象 -- 局部和全局。
更好的解决方案(最有可能)是使用 CONTEXT_INFO
,它本质上是会话内存。它是一个 VARBINARY(128)
值,但对它的更改在任何子进程中都有效,因为它不是对象。这不仅消除了您当前面临的复杂情况,而且还减少了 tempdb
I/O 考虑到每次此过程运行时您正在创建和删除临时 table,并执行 INSERT
,所有这 3 个操作都写入磁盘两次:首先在事务日志中,然后在数据文件中。您可以按以下方式使用它:
string _Command = @"
DECLARE @User VARBINARY(128) = CONVERT(VARBINARY(128), @Username);
SET CONTEXT_INFO @User;
";
using (var command = connection.CreateCommand())
{
command.CommandText = _Command;
// do not use AddWithValue()!
SqlParameter _UserName = new SqlParameter("@Username", SqlDbType.NVarChar, 128);
_UserName.Value = username;
command.Parameters.Add(_UserName);
command.ExecuteNonQuery();
}
然后您通过以下方式获取存储过程/用户定义函数/Table-Valued 函数/触发器中的值:
DECLARE @Username NVARCHAR(128) = CONVERT(NVARCHAR(128), CONTEXT_INFO());
这对单个值来说效果很好,但如果您需要多个值,或者如果您已经将 CONTEXT_INFO
用于其他目的,那么您要么需要返回到所描述的其他方法之一在这里,或者,如果使用 SQL Server 2016(或更新版本),您可以使用 SESSION_CONTEXT,它类似于 CONTEXT_INFO
,但是是一个 HashTable / Key-Value 对.
这种方法的另一个好处是 CONTEXT_INFO
(至少,我还没有尝试过 SESSION_CONTEXT
)在 T-SQL 用户定义函数和 Table-值函数。
最后,另一种选择是创建一个全局临时文件 table。上面说了,全局对象有子进程存活的好处,但是也有并发复杂化的缺点。一个很少被用来获得好处而没有缺点的方法是给临时对象一个唯一的、基于会话的 name,而不是添加一个列来保存一个唯一的、基于会话的 值。使用会话唯一的名称可以消除任何并发问题,同时允许您使用一个在连接关闭时自动清理的对象(因此无需担心创建全局临时文件的进程 table然后在完成之前遇到错误,而使用永久 table 将需要清理,或者至少在开始时进行存在性检查。
记住我们不能将任何值传递给存储过程的限制,我们需要使用数据层已经存在的值。要使用的值是 session_id
/ SPID。当然,这个值在app层是不存在的,所以要取回,但是没有限制往那个方向去。
int _SessionId;
using (var command = connection.CreateCommand())
{
command.CommandText = @"SET @SessionID = @@SPID;";
SqlParameter _paramSessionID = new SqlParameter("@SessionID", SqlDbType.Int);
_paramSessionID.Direction = ParameterDirection.Output;
command.Parameters.Add(_UserName);
command.ExecuteNonQuery();
_SessionId = (int)_paramSessionID.Value;
}
string _Command = String.Format(@"
CREATE TABLE ##ConnectionContextInfo_{0}(
AttributeName VARCHAR(64) PRIMARY KEY,
AttributeValue VARCHAR(1024)
);
INSERT INTO ##ConnectionContextInfo_{0} VALUES('Username', @Username);", _SessionId);
using (var command = connection.CreateCommand())
{
command.CommandText = _Command;
SqlParameter _UserName = new SqlParameter("@Username", SqlDbType.NVarChar, 128);
_UserName.Value = username;
command.Parameters.Add(_UserName);
command.ExecuteNonQuery();
}
然后您通过以下方式获取存储过程/触发器中的值:
DECLARE @Username NVARCHAR(128),
@UsernameQuery NVARCHAR(4000);
SET @UsernameQuery = CONCAT(N'SELECT @tmpUserName = [AttributeValue]
FROM ##ConnectionContextInfo_', @@SPID, N' WHERE [AttributeName] = ''Username'';');
EXEC sp_executesql
@UsernameQuery,
N'@tmpUserName NVARCHAR(128) OUTPUT',
@Username OUTPUT;
请注意,在 T-SQL 用户定义函数或 Table-Valued 函数中无法访问临时对象 -- 局部和全局。
最后,可以使用真实/永久(即非临时)Table,前提是您包含一个列来保存特定于当前会话的值。此附加列将允许并发操作正常工作。
您可以在 tempdb
中创建 table(是的,您可以将 tempdb
用作常规数据库,不需要只是以 [=39 开头的临时对象=] 或 ##
)。使用 tempdb
的优点是 table 不影响其他一切(毕竟它只是临时值,不需要恢复,所以 tempdb
使用 SIMPLE
恢复模型是完美的),并且在实例重新启动时它会被清理(仅供参考:每次 SQL 服务器时 tempdb
作为 model
的副本创建全新开始)。
就像上面的选项 #3 一样,我们可以再次使用 session_id
/ SPID 值,因为它对该连接上的所有操作都是通用的(只要连接保持打开状态)。但是,与选项 #3 不同的是,应用程序代码不需要 SPID 值:它可以使用默认约束自动插入到每一行中。这样就简化了一些操作。
这里的概念是先查看tempdb
中的永久table是否存在。如果是,则确保 table 中没有当前 SPID 的条目。如果没有,则创建 table。由于它是一个永久的table,它会继续存在,即使在当前进程关闭它的连接之后。最后,插入 @Username
参数,SPID 值将自动填充。
// assume _Connection is already open
using (SqlCommand _Command = _Connection.CreateCommand())
{
_Command.CommandText = @"
IF (OBJECT_ID(N'tempdb.dbo.Usernames') IS NOT NULL)
BEGIN
IF (EXISTS(SELECT *
FROM [tempdb].[dbo].[Usernames]
WHERE [SessionID] = @@SPID
))
BEGIN
DELETE FROM [tempdb].[dbo].[Usernames]
WHERE [SessionID] = @@SPID;
END;
END;
ELSE
BEGIN
CREATE TABLE [tempdb].[dbo].[Usernames]
(
[SessionID] INT NOT NULL
CONSTRAINT [PK_Usernames] PRIMARY KEY
CONSTRAINT [DF_Usernames_SessionID] DEFAULT (@@SPID),
[Username] NVARCHAR(128) NULL,
[InsertTime] DATETIME NOT NULL
CONSTRAINT [DF_Usernames_InsertTime] DEFAULT (GETDATE())
);
END;
INSERT INTO [tempdb].[dbo].[Usernames] ([Username]) VALUES (@UserName);
";
SqlParameter _UserName = new SqlParameter("@Username", SqlDbType.NVarChar, 128);
_UserName.Value = username;
command.Parameters.Add(_UserName);
_Command.ExecuteNonQuery();
}
然后您通过以下方式获取存储过程/用户定义函数/Table-Valued 函数/触发器中的值:
SELECT [Username]
FROM [tempdb].[dbo].[Usernames]
WHERE [SessionID] = @@SPID;
此方法的另一个好处是永久性 table 可在 T-SQL 用户定义函数和 Table 值函数中访问。
我想要一些可用于任何存储过程的信息,例如当前用户。按照 here 指示的临时 table 方法,我尝试了以下方法:
1) 在连接打开时创建临时 table
private void setConnectionContextInfo(SqlConnection connection)
{
if (!AllowInsertConnectionContextInfo)
return;
var username = HttpContext.Current?.User?.Identity?.Name ?? "";
var commandBuilder = new StringBuilder($@"
CREATE TABLE #ConnectionContextInfo(
AttributeName VARCHAR(64) PRIMARY KEY,
AttributeValue VARCHAR(1024)
);
INSERT INTO #ConnectionContextInfo VALUES('Username', @Username);
");
using (var command = connection.CreateCommand())
{
command.Parameters.AddWithValue("Username", username);
command.ExecuteNonQuery();
}
}
/// <summary>
/// checks if current connection exists / is closed and creates / opens it if necessary
/// also takes care of the special authentication required by V3 by building a windows impersonation context
/// </summary>
public override void EnsureConnection()
{
try
{
lock (connectionLock)
{
if (Connection == null)
{
Connection = new SqlConnection(ConnectionString);
Connection.Open();
setConnectionContextInfo(Connection);
}
if (Connection.State == ConnectionState.Closed)
{
Connection.Open();
setConnectionContextInfo(Connection);
}
}
}
catch (Exception ex)
{
if (Connection != null && Connection.State != ConnectionState.Open)
Connection.Close();
throw new ApplicationException("Could not open SQL Server Connection.", ex);
}
}
2) 使用以下函数测试了用于使用 SqlDataAdapter.Fill
填充 DataTable
的过程:
public DataTable GetDataTable(String proc, Dictionary<String, object> parameters, CommandType commandType)
{
EnsureConnection();
using (var command = Connection.CreateCommand())
{
if (Transaction != null)
command.Transaction = Transaction;
SqlDataAdapter adapter = new SqlDataAdapter(proc, Connection);
adapter.SelectCommand.CommandTimeout = CommonConstants.DataAccess.DefaultCommandTimeout;
adapter.SelectCommand.CommandType = commandType;
if (Transaction != null)
adapter.SelectCommand.Transaction = Transaction;
ConstructCommandParameters(adapter.SelectCommand, parameters);
DataTable dt = new DataTable();
try
{
adapter.Fill(dt);
return dt;
}
catch (SqlException ex)
{
var err = String.Format("Error executing stored procedure '{0}' - {1}.", proc, ex.Message);
throw new TptDataAccessException(err, ex);
}
}
}
3) 被调用的过程会像这样尝试获取用户名:
DECLARE @username VARCHAR(128) = (select AttributeValue FROM #ConnectionContextInfo where AttributeName = 'Username')
但是 #ConnectionContextInfo
在上下文中不再可用。
我已经在数据库中放置了一个 SQL 分析器,以检查发生了什么:
- 临时table使用某个SPID创建成功
- 使用相同的 SPID 调用过程
为什么临时 table 在过程范围内不可用?
在 T-SQL 中进行以下工作:
- 创建临时 table
- 调用需要来自特定临时文件的数据的过程table
- 临时 table 仅显式删除或在当前范围结束后删除
谢谢。
如本 answer 所示,当 CommandType
为 CommandType.Text
并且命令具有参数时,ExecuteNonQuery
使用 sp_executesql
。
这个问题中的 C# 代码没有明确设置 CommandType
,它是 Text
by default,所以代码的最终结果很可能是 CREATE TABLE #ConnectionContextInfo
被包装到 sp_executesql
。您可以在 SQL Profiler 中验证它。
众所周知sp_executesql
是运行ning在自己的范围内(本质上是一个嵌套的存储过程)。搜索 "sp_executesql temp table"。这是一个示例:Execute sp_executeSql for select...into #table but Can't Select out Temp Table Data
因此,在 sp_executesql
的嵌套范围内创建了临时 table #ConnectionContextInfo
,并在 sp_executesql
returns 后立即自动删除。
adapter.Fill
运行 的以下查询没有看到此临时 table。
怎么办?
确保 CREATE TABLE #ConnectionContextInfo
语句未包含在 sp_executesql
.
在您的情况下,您可以尝试将包含 CREATE TABLE #ConnectionContextInfo
和 INSERT INTO #ConnectionContextInfo
的单个批次拆分为两个批次。第一个 batch/query 将只包含没有任何参数的 CREATE TABLE
语句。第二个 batch/query 将包含带有参数的 INSERT INTO
语句。
我不确定它是否有帮助,但值得一试。
如果这不起作用,您可以构建一个 T-SQL 批处理来创建一个临时 table,将数据插入其中并调用您的存储过程。全部在一个 SqlCommand 中,全部在一个批次中。整个 SQL 将被包裹在 sp_executesql
中,但这并不重要,因为创建 temp table 的范围与调用存储过程的范围相同。从技术上讲它会起作用,但我不建议遵循此路径。
这里不是问题的答案,而是解决问题的建议。
老实说,整个方法看起来很奇怪。如果您想将一些数据传递到存储过程中,为什么不使用此存储过程的参数。这就是它们的用途 - 将数据传递到过程中。没有真正需要为此使用 temp table 。如果您传递的数据很复杂,您可以使用 table 值参数 (T-SQL, .NET)。如果只是一个 Username
.
您的存储过程需要知道临时 table,它需要知道它的名称和结构,所以我不明白显式 table 值有什么问题参数代替。即使您的程序代码也不会改变太多。您将使用 @ConnectionContextInfo
而不是 #ConnectionContextInfo
。
仅当您使用 SQL Server 2005 或更早版本(没有 table 值参数)时,对您描述的内容使用临时 tables 才对我有意义。它们是在 SQL Server 2008 中添加的。
"There are two types of temporary tables: local and global. They differ from each other in their names, their visibility, and their availability. Local temporary tables have a single number sign (#) as the first character of their names; they are visible only to the current connection for the user, and they are deleted when the user disconnects from the instance of SQL Server. Global temporary tables have two number signs (##) as the first characters of their names; they are visible to any user after they are created, and they are deleted when all users referencing the table disconnect from the instance of SQL Server." -- 来自 here
所以你的问题的答案是 ## 而不是 # 使本地临时 table 成为全局。
小问题:我暂时假设问题中发布的代码不是 运行 的完整代码。不仅使用了我们没有看到声明的变量(例如 AllowInsertConnectionContextInfo
),而且 setConnectionContextInfo
方法中还有一个明显的遗漏:command
对象被创建但从未被创建它的 CommandText
属性 设置为 commandBuilder.ToString()
,因此它看起来是一个空的 SQL 批次。我确定这实际上得到了正确处理,因为 1) 我相信提交一个空批次会产生异常,并且 2) 问题确实提到了临时 table 创建出现在 SQL 探查器中输出。尽管如此,我还是要指出这一点,因为这意味着可能存在与问题中未显示的观察到的行为相关的其他代码,这使得给出准确答案变得更加困难。
如@Vladimir 的罚款 sp_executesql
),本地临时对象 -- tables 和存储过程 -- 在该子进程完成后无法存活,因此在父上下文中不可用。
全局临时对象和 permanent/non-temporary 对象将在子进程完成后继续存在,但是这两个选项在它们的典型用法中都会引入并发问题:您需要先测试是否存在试图创建 table、 和 您需要一种方法来区分一个进程与另一个进程。所以这些并不是一个很好的选择,至少在它们的典型用法中不是(稍后会详细介绍)。
假设您不能将任何值传递到存储过程中(否则您可以按照@Vladimir 在他的回答中建议的那样简单地传递 username
),您有几个选择:
根据当前代码,最简单的解决方案是将本地临时 table 的创建与
INSERT
命令分开(@Vladimir 的回答中也提到) .如前所述,您遇到的问题是由sp_executesql
中的查询 运行 引起的。使用sp_executesql
的原因是为了处理参数@Username
。因此,修复可以像将当前代码更改为以下一样简单:string _Command = @" CREATE TABLE #ConnectionContextInfo( AttributeName VARCHAR(64) PRIMARY KEY, AttributeValue VARCHAR(1024) );"; using (var command = connection.CreateCommand()) { command.CommandText = _Command; command.ExecuteNonQuery(); } _Command = @" INSERT INTO #ConnectionContextInfo VALUES ('Username', @Username); "); using (var command = connection.CreateCommand()) { command.CommandText = _Command; // do not use AddWithValue()! SqlParameter _UserName = new SqlParameter("@Username", SqlDbType.NVarChar, 128); _UserName.Value = username; command.Parameters.Add(_UserName); command.ExecuteNonQuery(); }
请注意,在 T-SQL 用户定义函数或 Table-Valued 函数中无法访问临时对象 -- 局部和全局。
更好的解决方案(最有可能)是使用
CONTEXT_INFO
,它本质上是会话内存。它是一个VARBINARY(128)
值,但对它的更改在任何子进程中都有效,因为它不是对象。这不仅消除了您当前面临的复杂情况,而且还减少了tempdb
I/O 考虑到每次此过程运行时您正在创建和删除临时 table,并执行INSERT
,所有这 3 个操作都写入磁盘两次:首先在事务日志中,然后在数据文件中。您可以按以下方式使用它:string _Command = @" DECLARE @User VARBINARY(128) = CONVERT(VARBINARY(128), @Username); SET CONTEXT_INFO @User; "; using (var command = connection.CreateCommand()) { command.CommandText = _Command; // do not use AddWithValue()! SqlParameter _UserName = new SqlParameter("@Username", SqlDbType.NVarChar, 128); _UserName.Value = username; command.Parameters.Add(_UserName); command.ExecuteNonQuery(); }
然后您通过以下方式获取存储过程/用户定义函数/Table-Valued 函数/触发器中的值:
DECLARE @Username NVARCHAR(128) = CONVERT(NVARCHAR(128), CONTEXT_INFO());
这对单个值来说效果很好,但如果您需要多个值,或者如果您已经将
CONTEXT_INFO
用于其他目的,那么您要么需要返回到所描述的其他方法之一在这里,或者,如果使用 SQL Server 2016(或更新版本),您可以使用 SESSION_CONTEXT,它类似于CONTEXT_INFO
,但是是一个 HashTable / Key-Value 对.这种方法的另一个好处是
CONTEXT_INFO
(至少,我还没有尝试过SESSION_CONTEXT
)在 T-SQL 用户定义函数和 Table-值函数。最后,另一种选择是创建一个全局临时文件 table。上面说了,全局对象有子进程存活的好处,但是也有并发复杂化的缺点。一个很少被用来获得好处而没有缺点的方法是给临时对象一个唯一的、基于会话的 name,而不是添加一个列来保存一个唯一的、基于会话的 值。使用会话唯一的名称可以消除任何并发问题,同时允许您使用一个在连接关闭时自动清理的对象(因此无需担心创建全局临时文件的进程 table然后在完成之前遇到错误,而使用永久 table 将需要清理,或者至少在开始时进行存在性检查。
记住我们不能将任何值传递给存储过程的限制,我们需要使用数据层已经存在的值。要使用的值是
session_id
/ SPID。当然,这个值在app层是不存在的,所以要取回,但是没有限制往那个方向去。int _SessionId; using (var command = connection.CreateCommand()) { command.CommandText = @"SET @SessionID = @@SPID;"; SqlParameter _paramSessionID = new SqlParameter("@SessionID", SqlDbType.Int); _paramSessionID.Direction = ParameterDirection.Output; command.Parameters.Add(_UserName); command.ExecuteNonQuery(); _SessionId = (int)_paramSessionID.Value; } string _Command = String.Format(@" CREATE TABLE ##ConnectionContextInfo_{0}( AttributeName VARCHAR(64) PRIMARY KEY, AttributeValue VARCHAR(1024) ); INSERT INTO ##ConnectionContextInfo_{0} VALUES('Username', @Username);", _SessionId); using (var command = connection.CreateCommand()) { command.CommandText = _Command; SqlParameter _UserName = new SqlParameter("@Username", SqlDbType.NVarChar, 128); _UserName.Value = username; command.Parameters.Add(_UserName); command.ExecuteNonQuery(); }
然后您通过以下方式获取存储过程/触发器中的值:
DECLARE @Username NVARCHAR(128), @UsernameQuery NVARCHAR(4000); SET @UsernameQuery = CONCAT(N'SELECT @tmpUserName = [AttributeValue] FROM ##ConnectionContextInfo_', @@SPID, N' WHERE [AttributeName] = ''Username'';'); EXEC sp_executesql @UsernameQuery, N'@tmpUserName NVARCHAR(128) OUTPUT', @Username OUTPUT;
请注意,在 T-SQL 用户定义函数或 Table-Valued 函数中无法访问临时对象 -- 局部和全局。
最后,可以使用真实/永久(即非临时)Table,前提是您包含一个列来保存特定于当前会话的值。此附加列将允许并发操作正常工作。
您可以在
tempdb
中创建 table(是的,您可以将tempdb
用作常规数据库,不需要只是以 [=39 开头的临时对象=] 或##
)。使用tempdb
的优点是 table 不影响其他一切(毕竟它只是临时值,不需要恢复,所以tempdb
使用SIMPLE
恢复模型是完美的),并且在实例重新启动时它会被清理(仅供参考:每次 SQL 服务器时tempdb
作为model
的副本创建全新开始)。就像上面的选项 #3 一样,我们可以再次使用
session_id
/ SPID 值,因为它对该连接上的所有操作都是通用的(只要连接保持打开状态)。但是,与选项 #3 不同的是,应用程序代码不需要 SPID 值:它可以使用默认约束自动插入到每一行中。这样就简化了一些操作。这里的概念是先查看
tempdb
中的永久table是否存在。如果是,则确保 table 中没有当前 SPID 的条目。如果没有,则创建 table。由于它是一个永久的table,它会继续存在,即使在当前进程关闭它的连接之后。最后,插入@Username
参数,SPID 值将自动填充。// assume _Connection is already open using (SqlCommand _Command = _Connection.CreateCommand()) { _Command.CommandText = @" IF (OBJECT_ID(N'tempdb.dbo.Usernames') IS NOT NULL) BEGIN IF (EXISTS(SELECT * FROM [tempdb].[dbo].[Usernames] WHERE [SessionID] = @@SPID )) BEGIN DELETE FROM [tempdb].[dbo].[Usernames] WHERE [SessionID] = @@SPID; END; END; ELSE BEGIN CREATE TABLE [tempdb].[dbo].[Usernames] ( [SessionID] INT NOT NULL CONSTRAINT [PK_Usernames] PRIMARY KEY CONSTRAINT [DF_Usernames_SessionID] DEFAULT (@@SPID), [Username] NVARCHAR(128) NULL, [InsertTime] DATETIME NOT NULL CONSTRAINT [DF_Usernames_InsertTime] DEFAULT (GETDATE()) ); END; INSERT INTO [tempdb].[dbo].[Usernames] ([Username]) VALUES (@UserName); "; SqlParameter _UserName = new SqlParameter("@Username", SqlDbType.NVarChar, 128); _UserName.Value = username; command.Parameters.Add(_UserName); _Command.ExecuteNonQuery(); }
然后您通过以下方式获取存储过程/用户定义函数/Table-Valued 函数/触发器中的值:
SELECT [Username] FROM [tempdb].[dbo].[Usernames] WHERE [SessionID] = @@SPID;
此方法的另一个好处是永久性 table 可在 T-SQL 用户定义函数和 Table 值函数中访问。