【问题标题】:How to PRINT a message from SQL CLR function?如何从 SQL CLR 函数打印消息?
【发布时间】:2010-10-04 07:52:39
【问题描述】:

有没有类似的

PRINT 'hello world'

哪些可以从 CLR (C#) 代码中调用?

我正在尝试在我的函数中输出一些调试信息。我无法运行 VS 调试器,因为这是一个远程服务器。

谢谢!

【问题讨论】:

    标签: sql-server debugging trace sqlclr


    【解决方案1】:

    答案是你做不到

    PRINT 'Hello World'
    

    来自[SqlFunction()]。但是,您可以通过 [SqlProcedure()] 使用

    SqlContext.Pipe.Send("hello world")
    

    这与 T-SQL 一致,如果在函数中粘贴 PRINT,则会收到错误“在函数中使用副作用运算符 'PRINT' 无效”。但如果您从存储过程中执行此操作,则不会。

    我建议的解决方法:

    1. 从您的代码中使用 Debug.Print,并将调试器附加到 SQL Server(我知道这对您不起作用,正如您所解释的那样)。
    2. 将消息保存在全局变量中,例如List<string> messages,然后编写另一个返回messages 内容的表值函数。当然,对messages 的访问需要同步,因为可能有多个线程同时尝试访问它。
    3. 将您的代码移至[SqlProcedure()]
    4. 添加一个参数“debug”,当 =1 时,函数会将消息作为返回表的一部分返回(假设有一列包含文本..)

    【讨论】:

      【解决方案2】:

      你应该能够做到:

      SqlContext.Pipe.Send("hello world");
      

      如果您在 CLR UDF 中运行它,SqlContext.Pipe 将始终是您发现的 null。如果没有有效的SqlPipe,我不相信你能做你想做的事。

      如果这纯粹是出于调试目的,您始终可以在托管代码中打开一个文件并在其中写入您的输出。但是,这要求您的程序集具有EXTERNAL_ACCESS 权限,而这反过来又要求将数据库标记为可信任。不一定是我会做或推荐的事情。

      【讨论】:

      • RAISERROR() WITH NOWAIT 怎么样?
      • 您是建议作为替代方案还是询问是否可以在托管 SP/UDF 中这样做?
      • 嗯,很有趣,感谢您的建议,我尝试使用该类并且 SqlContext.Pipe 由于某种原因始终为空,同时 SqlContext.IsAvailable 标志为真。知道为什么会这样吗?
      • @Serguei 和 Sean:SqlContext.IsAvailable 指示此特定代码是否在 SQL Server 的 CLR 主机中运行。 SqlContext.Pipe 用于与 SQL Server 进行通信,这并不适用于所有场景,例如在函数中而不是在存储过程或触发器中。此外,将程序集设置为 EXTERNAL_ACCESS 确实 not 需要 TRUSTWORTHY ON,我同意不建议这样做。我在answer 中发布了一些详细信息。
      【解决方案3】:

      啊,我明白了... 澄清一下:如果您有 SqlFunction 则 SqlContext.Pipe 不可用,但是在 SqlProcedure 中它是可用的,您可以使用Send() 来写消息。

      除了异常消息之外,我还没有找到从 SqlFunction 输出信息的方法。

      【讨论】:

      • Serguei:我知道已经有几年了,但我在answer 中发布了一些选项。
      【解决方案4】:

      SQLCLR 函数 -- 标量用户定义函数 (UDF)、表值函数 (TVF)、用户定义聚合 (UDA) 和用户定义类型 (UDT) 中的方法 -- 使用上下文连接 (即 ConnectionString = "Context Connection = true;"),受大多数与 T-SQL 函数绑定的相同限制的约束,包括不能 PRINTRAISERROR('message', 10, 1)。但是,您确实有一些选择。

      在我们讨论这些选项之前,应该声明:

      • 您无需切换到使用存储过程。如果想要一个功能,那就坚持一个功能。

      • 添加“调试”参数并为此更改输出似乎有点极端,因为 UDF(T-SQL 和 SQLCLR)函数不允许重载。因此调试参数将始终在签名中。如果你想触发调试,只需创建一个名为#debug(或类似名称)的临时表,并使用"Context Connection = true;" 测试通过SELECT OBJECT_ID(N'tempdb..#debug'); 的ConnectionString(速度很快,可以在安全模式下完成 是同一会话的一部分,因此它可以看到临时表)。从if (SqlCommand.ExecuteScalar() == DBNull.Value) 获取结果。

      • 请不要使用全局(即静态)变量。这比必要的复杂得多,并且要求(通常)将 Assembly 设置为 UNSAFE,如果可能的话,应该避免这样做。

      所以,如果您至少可以将程序集设置为EXTERNAL_ACCESS,那么您有几个选择。这样做不需要需要将数据库设置为TRUSTWORTHY ON。这是一个非常普遍(也是不幸)的误解。您只需要签署程序集(无论如何这是一个很好的做法),然后从 DLL 创建一个非对称密钥(在 [master] 中),然后基于该非对称密钥创建一个登录,最后授予登录 EXTERNAL ACCESS ASSEMBLY。完成之后(一次),您可以执行以下任何操作:

      • 使用File.AppendAllText (String path, String contents) 将消息写入文件。当然,如果您无权访问文件系统,那么这将无济于事。如果网络上有可以访问的共享驱动器,那么只要 SQL Server 服务的服务帐户有权在该共享上创建和写入文件,那么这将起作用。如果存在服务帐户没有权限但您的域/Active Directory 帐户有权限的共享,那么您可以将 File.AppendAllText 调用打包:

        using (WindowsImpersonationContext _Impersonate = 
                              SqlContext.WindowsIdentity.Impersonate())
        {
           File.AppendAllText("path.txt", _DebugMessage);
            _Impersonate.Undo();
        }
        
      • 连接到 SQL Server 并将消息写入表。它可以是当前/本地 SQL Server 或任何其他 SQL Server。您可以在[tempdb] 中创建一个表,以便在下次重新启动 SQL Server 时自动清理它,否则会一直持续到那个时间,或者直到您删除它。进行常规/外部连接允许您执行 DML 语句。然后,您可以在运行该函数时从表中进行选择。

      • 将消息写入环境变量。自 Vista / Server 2008 以来,环境变量的大小并没有完全受到限制,尽管它们并不真正处理换行符。但在 .NET 代码中设置的任何变量也将继续存在,直到重新启动 SQL Server 服务。您可以通过读取当前值并将新消息连接到末尾来附加消息。比如:

        {
          string _Current = System.Environment.GetEnvironmentVariable(_VariableName,
                                          EnvironmentVariableTarget.Process);
        
          System.Environment.SetEnvironmentVariable(
              _VariableName,
              _Current + _DebugMessage,
              EnvironmentVariableTarget.Process);
        }
        

      需要注意的是,在这 3 种情况下,都假设测试是以单线程方式进行的。如果该函数将同时从多个会话运行,那么您需要一种分离消息的方法。在这种情况下,您可以获得当前的“transaction_id”(所有查询,即使没有BEGIN TRAN 也是一个事务!)对于任何特定的执行应该是一致的(在同一个函数中的多次使用以及如果该函数是跨多行的每一行调用)。如果使用文件或环境变量方法,您可以将此值用作消息的前缀,如果存储到表中,则可以用作单独的字段。您可以通过以下方式获取交易:

      int _TransactionID;
      
      using (SqlConnection _Connection = new SqlConnection("Context Connection = true;"))
      {
          using (SqlCommand _Command = _Connection.CreateCommand())
          {
              _Command.CommandText = @"
      SELECT transaction_id
      FROM sys.dm_exec_requests
      WHERE session_id = @@SPID;
      ";
      
              _Connection.Open();
              _TransactionID = (int)_Command.ExecuteScalar();
          }
      }
      

      有关 T-SQL 和 SQLCLR 函数的其他信息

      以下列表最初取自 Create User-defined Functions (Database Engine) 的 MSDN 页面,然后由我编辑,如上所述,以反映 T-SQL 函数和 SQLCLR 函数之间的差异:

      • 用户定义的函数不能用于执行修改数据库状态的操作。
      • 用户定义的函数不能包含以表为目标的 OUTPUT INTO 子句。
      • 用户定义的函数不能返回多个结果集。如果您需要返回多个结果集,请使用存储过程。
      • 错误处理受限于用户定义的函数。 UDF 不支持 TRY...CATCH、@@ERROR 或 RAISERROR。 [ 注意:这是就 T-SQL 而言的,无论是本机的还是从 SQLCLR 函数提交的。您可以在 .NET 代码中使用 try / catch / finally / throw。 ]​​i>
      • 用户定义的函数中不允许使用 SET 语句。
      • 不允许使用 FOR XML 子句
      • 用户自定义函数可以嵌套; ...嵌套级别在被调用函数开始执行时递增,在被调用函数完成执行时递减。用户定义的函数最多可以嵌套 32 层。
      • 以下 Service Broker 语句不能包含在 Transact-SQL 用户定义函数的定义中:
        • 开始对话
        • 结束对话
        • 获取对话组
        • 移动对话
        • 接收
        • 发送

      以下内容与 T-SQL 函数和 SQLCLR 函数有关:

      • 无法使用 PRINT
      • 不能调用 NEWID() [好吧,除非你在视图中SELECT NEWID()。但在 .NET 代码中,您可以使用 Guid.NewGuid()。 ]

      以下内容仅适用于 T-SQL 函数:

      • 用户定义函数不能调用存储过程,但可以调用扩展存储过程。
      • 用户定义的函数不能使用动态 SQL 或临时表。允许使用表变量。

      相比之下,SQLCLR 函数可以:

      • 执行存储过程,只要它们是只读的。
      • 利用动态 SQL(从 SQLCLR 提交的所有 SQL 本质上都是临时/动态的)。
      • 从临时表中选择。

      【讨论】:

        【解决方案5】:

        您可以尝试将这些信息通过“xp_logevent”存储过程。 您可以将调试信息设置为不同级别的“信息”、“警告”或“错误”。 我还尝试将这些调试/错误信息放入事件日志中,但这需要在安全性方面进行一些配置,我怀疑我不能在生产中使用它。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2013-09-25
          • 1970-01-01
          • 2019-11-06
          • 2016-12-04
          • 1970-01-01
          • 2021-11-19
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多