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

Ale*_*xei 5 sql sql-server ado.net stored-procedures temp-tables

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

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);
            }
        }
Run Code Online (Sandbox Code Playgroud)

2)测试了其中使用填充的过程DataTable使用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);
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

3)调用过程尝试获取这样的用户名:

DECLARE @username VARCHAR(128) = (select AttributeValue FROM #ConnectionContextInfo where AttributeName = 'Username')
Run Code Online (Sandbox Code Playgroud)

#ConnectionContextInfo在上下文中不再可用.

我已经针对数据库放置了一个SQL探查器,以检查发生了什么:

  • 使用某个SPID成功创建临时表
  • 使用相同的SPID调用过程

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

在T-SQL中做以下工作:

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

谢谢.

Vla*_*nov 6

如该结果显示答案,ExecuteNonQuery使用sp_executesql的时候CommandTypeCommandType.Text和命令有参数.

此问题中的C#代码未CommandType明确设置Text,默认情况下是这样,因此最有可能的代码的最终结果CREATE TABLE #ConnectionContextInfo是包含在内sp_executesql.您可以在SQL事件探查器中对其进行验证.

众所周知,sp_executesql它在自己的范围内运行(实际上它是一个嵌套的存储过程).搜索"sp_executesql临时表".下面是一个示例: 执行sp_executeSql for select ... into #table但不能选择Temp Table Data

因此,临时表#ConnectionContextInfo在嵌套范围内创建,sp_executesql并在sp_executesql返回后立即自动删除.运行的以下查询adapter.Fill未看到此临时表.


该怎么办?

确保该CREATE TABLE #ConnectionContextInfo语句未包含在内sp_executesql.

你的情况,你可以尝试分割包含两个单批CREATE TABLE #ConnectionContextInfoINSERT INTO #ConnectionContextInfo分成两个批次.第一个批处理/查询将仅包含CREATE TABLE不带任何参数的语句.第二个批处理/查询将包含INSERT INTO带参数的语句.

我不确定它会有所帮助,但值得一试.

如果这不起作用,您可以构建一个T-SQL批处理,创建临时表,将数据插入其中并调用存储过程.所有在一个SqlCommand,所有在一个批次.整个SQL将被包装sp_executesql,但这并不重要,因为创建临时表的范围将与调用存储过程的范围相同.从技术上讲它会起作用,但我不建议遵循这条道路.


这不是问题的答案,而是解决问题的建议.

说实话,整个方法看起来很奇怪.如果要将某些数据传递到存储过程,为什么不使用此存储过程的参数.这就是他们的目的 - 将数据传递到程序中.没有必要使用临时表.如果传递的数据很复杂,则可以使用表值参数(T-SQL,.NET).如果只是一个,那绝对是一种矫枉过正Username.

您的存储过程需要知道临时表,它需要知道它的名称和结构,所以我不明白具有显式表值参数的问题是什么.即使你的程序代码也不会有太大变化.你用@ConnectionContextInfo而不是#ConnectionContextInfo.

只有在使用没有表值参数的SQL Server 2005或更早版本时,才能对我所描述的内容使用临时表.它们是在SQL Server 2008中添加的.


Sol*_*zky 6

次要问题:我暂时假设问题中发布的代码不是正在运行的完整代码。不仅使用了我们没有看到声明的变量(例如AllowInsertConnectionContextInfo),而且方法中有一个明显的遗漏setConnectionContextInfocommand对象被创建但它的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();
    }
    
    Run Code Online (Sandbox Code Playgroud)

    请注意临时对象——本地和全局——不能在 T-SQL 用户定义函数或表值函数中访问。

  2. 更好的解决方案(最有可能)是使用CONTEXT_INFO,它本质上是会话内存。它是一个VARBINARY(128)值,但由于它不是一个对象,因此它的更改在任何子过程中都存在。这不仅消除了您当前面临的复杂情况,而且tempdb考虑到您在每次此进程运行时都在创建和删除临时表,并执行INSERT,并且所有 3 个操作都写入磁盘两次,因此它还减少了I/O : 首先在事务日志中,然后在数据文件中。您可以通过以下方式使用它:

    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();
    }
    
    Run Code Online (Sandbox Code Playgroud)

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

    DECLARE @Username NVARCHAR(128) = CONVERT(NVARCHAR(128), CONTEXT_INFO());
    
    Run Code Online (Sandbox Code Playgroud)

    这适用于单个值,但如果您需要多个值,或者如果您已经将其CONTEXT_INFO用于其他目的,那么您要么需要返回此处描述的其他方法之一,或者,如果使用 SQL Server 2016 (或更新版本),您可以使用SESSION_CONTEXT,它类似于CONTEXT_INFO一个 HashTable / Key-Value 对。

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

  3. 最后,另一种选择是创建一个全局临时表。如上所述,全局对象的好处是可以让子进程幸存下来,但它们也有使并发复杂化的缺点。一个很少使用但没有缺点的好处是给临时对象一个唯一的、基于会话的名称,而不是添加一个列来保存唯一的、基于会话的。使用对会话唯一的名称可以消除任何并发问题,同时允许您使用在连接关闭时将自动清除的对象(因此无需担心创建全局临时表然后遇到的进程在完成之前出现错误,而使用永久表需要清理,或者至少在开始时进行存在检查)。

    记住我们不能将任何值传递到存储过程的限制,我们需要使用数据层已经存在的值。要使用的值将是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();
    }
    
    Run Code Online (Sandbox Code Playgroud)

    然后您通过以下方式获取存储过程/触发器中的值:

    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;
    
    Run Code Online (Sandbox Code Playgroud)

    请注意临时对象——本地和全局——不能在 T-SQL 用户定义函数或表值函数中访问。

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

    您可以在中创建表tempdb(是的,您可以tempdb用作常规数据库,不需要只是以#或开头的临时对象##)。使用的好处tempdb是表不受其他任何事物的影响(毕竟只是临时值,不需要恢复,所以tempdb使用SIMPLE恢复模型是完美的),并且在恢复时会被清理干净实例重新启动(仅供参考:每次 SQL Server 启动时tempdb都会创建一个全新的副本model)。

    就像上面的选项 #3 一样,我们可以再次使用session_id/SPID 值,因为它对于此连接上的所有操作都是通用的(只要连接保持打开状态)。但是,与选项 #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();
    }
    
    Run Code Online (Sandbox Code Playgroud)

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

    SELECT [Username]
    FROM   [tempdb].[dbo].[Usernames]
    WHERE  [SessionID] = @@SPID;
    
    Run Code Online (Sandbox Code Playgroud)

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