小编典典

使用临时表的SQL Server连接上下文不能在用SqlDataAdapter.Fill调用的存储过程中使用

sql

我想为任何存储过程(例如当前用户)提供一些信息。按照此处指示的临时表方法,我尝试了以下操作:

1)打开连接时创建临时表

        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)通过使用以下函数,使用用于填充DataTableusing的过程进行了测试SqlDataAdapter.Fill

    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事件探查器,以检查发生了什么:

  • 使用特定的SPID成功创建了临时表
  • 使用相同的SPID调用过程

为什么临时表在过程范围内不可用?

在T-SQL中,执行以下工作:

  • 创建一个临时表
  • 调用需要该特定临时表中的数据的过程
  • 仅显式或在当前作用域结束后删除临时表

谢谢。


阅读 224

收藏
2021-04-22

共1个答案

小编典典

小问题:我现在暂时假设问题中发布的代码不是正在运行的完整代码。不仅存在一些我们看不到被声明的变量(例如AllowInsertConnectionContextInfo),而且该setConnectionContextInfo方法中也存在明显的遗漏:command创建了对象,但其CommandText属性从未设置为commandBuilder.ToString(),因此它似乎是一个空的SQL批处理。我确信这实际上是正确地处理的,因为1)我相信提交一个空的批处理将生成一个异常,以及2)这个问题的确提到了临时表的创建出现在SQL
Profiler输出中。尽管如此,我还是指出这一点,因为这意味着可能存在与问题中未显示的与观察到的行为相关的其他代码,这使得给出准确答案更加困难。

正如@Vladimir的一个很好的回答中所述,由于查询在子流程(即sp_executesql)中运行,因此本地临时对象(表和存储过程)无法在该子流程完成后幸免于难。在父上下文中不可用。

全局临时对象和永久/非临时对象将在子流程完成后幸存下来,但是这两个选项的典型用法都带来了并发问题:在尝试创建表之前,您需要先测试是否存在,
你需要一种方法来区分不同的过程。因此,这些并不是真正的好选择,至少在它们的典型用法上不是这样(稍后会详细介绍)。

假设您不能将任何值传递到存储过程中(否则您可以username按照他的答案中@Vladimir的建议简单地传递),则有几种选择:

  1. 给定当前代码,最简单的解决方案是将本地临时表的创建与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用户定义函数或表值函数中访问临时对象(本地和全局)。

  1. 更好的解决方案(最有可能)是使用CONTEXT_INFO,它本质上是会话内存。它是一个VARBINARY(128)值,但是对它的更改可以在任何子过程中保留下来,因为它不是对象。这不仅消除了您当前面临的麻烦,而且还tempdb考虑到每次运行此过程都要创建和删除一个临时表并执行INSERT,并且还减少了I / O ,所有这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();
    

    }

然后,您可以通过以下方式在存储过程/用户定义函数/表值函数/触发器中获取值:

    DECLARE @Username NVARCHAR(128) = CONVERT(NVARCHAR(128), CONTEXT_INFO());

对于单个值来说,这很好用,但是如果您需要多个值,或者如果您已经将其CONTEXT_INFO用于其他用途,那么您需要返回到此处介绍的其他方法之一,或者,如果使用的是SQL
Server 2016(或更高版本),则可以使用SESSION_CONTEXT,它类似于CONTEXT_INFOHashTable /键值对。

这种方法的另一个好处是CONTEXT_INFO(至少我还没有尝试过SESSION_CONTEXT)在T-SQL用户定义函数和表值函数中可用。

  1. 最后,另一个选择是创建一个全局临时表。如上所述,全局对象具有幸存子流程的优势,但它们也具有使并发复杂化的缺点。在没有缺点的情况下很少获得好处的是,给临时对象一个唯一的基于会话的 名称 ,而不是添加一列以保存一个唯一的基于会话的 。使用会话唯一的名称可以消除所有并发问题,同时允许您使用一个对象,该对象在关闭连接后会自动清除(因此,无需担心会创建全局临时表然后运行的对象)在完成之前发生错误,而使用永久表则需要清除,或者至少在开始时存在检查)。

请记住,我们不能将任何值传递给存储过程的限制,我们需要使用数据层已经存在的值。要使用的值为session_id/
SPID。当然,该值在应用程序层中不存在,因此必须重新获取它,但是在该方向上没有任何限制。

    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用户定义函数或表值函数中访问临时对象(本地和全局)。

  1. 最后,可以使用真实/永久(即非临时)表,前提是您包括一列来保存特定于当前会话的值。此附加列将允许并发操作正常工作。

您可以在中创建表tempdb(是的,您可以将其tempdb用作常规数据库,而不必只是以#或开头的临时对象##)。使用的优势tempdb在于,该表不影响其他所有内容(毕竟,它只是临时值,不需要还原,因此tempdb使用SIMPLE恢复模型是完美的选择),并且当实例重新启动(FYI:tempdbmodelSQL
Server每次启动时作为副本创建)。

就像上面的选项3一样,我们可以再次使用session_id/
SPID值,因为它对于此Connection上的所有操作都是通用的(只要Connection保持打开状态)。但是,与选项#3不同,应用程序代码不需要SPID值:可以使用“默认约束”将其自动插入到每一行中。这稍微简化了操作。

这里的概念是首先检查永久表是否tempdb存在。如果是这样,请确保当前SPID的表中没有条目。如果不是,则创建表。由于它是一个永久表,即使在当前进程关闭其连接后,它也将继续存在。最后,插入@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();
}

然后,您可以通过以下方式在存储过程/用户定义函数/表值函数/触发器中获取值:

    SELECT [Username]
FROM   [tempdb].[dbo].[Usernames]
WHERE  [SessionID] = @@SPID;

这种方法的另一个好处是,可以在T-SQL用户定义函数和表值函数中访问永久表。

2021-04-22