为啥需要以只读方式输入 SQL Server 存储过程的表值参数?

Posted

技术标签:

【中文标题】为啥需要以只读方式输入 SQL Server 存储过程的表值参数?【英文标题】:Why are table valued parameters to SQL Server stored procedures required to be input READONLY?为什么需要以只读方式输入 SQL Server 存储过程的表值参数? 【发布时间】:2014-10-24 03:17:03 【问题描述】:

谁能解释防止表值参数被指定为存储过程的输出参数背后的设计决策?

我数不清有多少次我开始构建数据模型,希望完全锁定我的表以供外部访问(你知道...实现细节),通过存储过程授予应用程序访问数据库的权限仅(您知道...数据接口)并与 TVP 来回通信只是让 SSMS 称我为顽皮,因为我大胆地认为我可以使用用户定义的表类型作为我的数据服务和之间的传输对象我的申请。

所以请有人给我一个很好的理由,为什么 TVP 被设计为只读输入参数。

【问题讨论】:

在实施之前讨论表值参数的设计会议期间,SO 上有人在场的可能性几乎为零。对于一些潜在的问题,我可以发布一个充满猜测的答案,但仅此而已。 @Damien_The_Unbeliever 但仍然值得一问,老实说,您的回答在这里没有任何意义 【参考方案1】:

在 Michael Rys 的 Optimizing Microsoft SQL Server 2008 Applications Using Table Valued Parameters, XML, and MERGE 演示文稿中,他说。 (32:52)

请注意,在 SQL Server 2008 中,表值参数是只读的。 但正如您所注意到的,我们实际上要求您编写 READONLY。以便 实际上然后意味着在将来的某个时候也许如果你说 拜托,请经常请我们也许能够真正做到 它们在某些时候也是可写的。但目前他们正在阅读 仅限。

这是您应该用来添加“请”的连接项。 Relax restriction that table parameters must be readonly when SPs call each other.

Srini Acharya 对连接项发表了评论。

允许读/写表值参数涉及很多 SQL 引擎方面的工作以及客户端协议。由于 时间/资源限制以及其他优先事项,我们不会 能够在 SQL Server 2008 版本中承担这项工作。然而, 我们已经调查了这个问题,并将其牢牢地放在我们的雷达上 地址作为 SQL Server 下一版本的一部分。

【讨论】:

所以..."在我们的雷达上作为下一个 SQL Server 版本的一部分解决"作为 MSSQL08 版本。 ...MSSQL08R2、MSSQL12,现在是 MSSQL14。我想知道翻转保持是什么。 @k.alanbates 是的,有三个版本,但没有发生任何事情。不确定您是否真的需要它。仅使用过程中的常规结果集怎么样? ...哦,我从存储过程中获取结果没有问题。我的观点是,当 OUTPUT TVP 感觉像是在数据库服务层之间传输结构化数据的正确方法时(阅读:存储过程),它总是感觉像是一种黑客攻击。 K.alanbates 您是否考虑过使用表值函数?有一些限制。动态 sql 是其中之一,但您有一个类型化的结果集作为返回值。当然,每个函数也仅限于一个结果集。不过,我确实同意 TVP 看起来像是一个半生不熟的功能,他们认为完成这项功能需要付出太多努力。 动态sql是撒旦的产物。【参考方案2】:

表值参数有以下限制(来源 MSDN):

SQL Server 不维护表值列的统计信息 参数。 表值参数必须作为输入 READONLY Transact-SQL 例程的参数。您无法执行 DML 对表值进行 UPDATE、DELETE 或 INSERT 等操作 例程主体中的参数。 您不能将表值参数用作 SELECT INTO 的目标 或 INSERT EXEC 语句。表值参数可以在 SELECT INTO 或 INSERT EXEC 字符串中的 FROM 子句或存储 程序。

有几个选项可以克服这个限制,一个是

CREATE TYPE RTableType AS TABLE(id INT, NAME VARCHAR )

go

CREATE PROCEDURE Rproc @Rtable RTABLETYPE READONLY,
                       @id     INT
AS
  BEGIN
      SELECT *
      FROM   @Rtable
      WHERE  ID = @id
  END

go

DECLARE @Rtable RTABLETYPE
DECLARE @Otable RTABLETYPE

INSERT INTO @Rtable
VALUES      (1,'a'),
            (2,'b')

INSERT @Otable
EXEC Rproc
  @Rtable,
  2

SELECT *
FROM   @Otable 

通过这个你可以得到表格的值

【讨论】:

(重访帖子)感谢您的回复,但它根本没有解决手头的问题。您的过程恰巧返回一个结果集,该结果集恰巧匹配UDTT,因为您已经在临时脚本中编写了这样的结果集。最初的问题并不是要关注“如何从存储过程中获取表数据?”这是“微软为什么不使用 OUTPUT TVP 更新 SQL Server 以允许面向消息的数据库通信,而他们几乎在 7 年前就说过他们会这样做?这到底是什么问题?”【参考方案3】:

关于(强调):

所以请有人给我一个充分的理由为什么 TVP 被设计为只读输入参数。

我刚刚在 DBA.StackExchange 上发布了一个更详细的答案:

READONLY parameters and TVP restrictions

但它的摘要是这样的:

根据这篇博文 (TSQL Basics II - Parameter Passing Semantics),存储过程输出参数的设计目标是它们仅在存储过程成功完成时模仿“通过引用”行为!但是,当出现导致存储过程中止的错误时,对任何 OUTPUT 参数所做的任何更改不会在控制返回调用进程时反映在这些变量的当前值中。 p>

但是当 TVP 被引入时,他们将它们实现为真正的通过引用传递,因为继续“按值”模型——在该模型中,如果存储过程未成功完成,则会制作它的副本以确保更改丢失—— - 效率不高/不可扩展,尤其是在通过 TVP 传递大量数据的情况下。

因此,只有一个表变量实例是 TVP,并且在任何存储过程中对其所做的任何更改(如果它们不限于 READONLY)都将立即保留并保留,即使存储过程遇到错误。这违反了本摘要开头所述的设计目标。而且,由于表变量不受事务约束,因此无法以某种方式将对 TVP 所做的更改绑定到事务(即使是在幕后自动处理的事务)。

因此,将它们标记为READONLY 是(目前)保持存储过程参数设计目标的唯一方法,这样它们就不会反映在存储过程中所做的更改,除非:参数声明为@987654325 @并且存储过程成功完成。

【讨论】:

【参考方案4】:

提前警告。此代码将不起作用。 这就是问题

请注意,所有代码都是直接从内存中输入到帖子中的。我在示例中可能有类型错误或一些类似的错误。这只是为了演示这将促进的技术,它不适用于撰写本文时发布的任何 SQL Server 版本。所以它当前是否编译并不重要。


我知道这个问题现在已经过时了,但也许有人在这里看到我的帖子可能会受益于理解 为什么 TVP 不能被存储的过程直接操作和读取是一件大事作为调用客户端的输出参数。

“你怎么……”关于 OUTPUT TVP 的问题已经在 SQL Server 论坛上流传了五年多。几乎每一个都涉及到有人尝试一些假设的解决方法,但首先完全忽略了问题的重点。

通过创建表类型变量,插入其中然后从其中返回读取,您可以“获得与表类型匹配的结果集”,这完全是不合理的。 当您这样做时,结果集仍然不是消息。它是一个临时 ResultSet,其中包含“恰好匹配”UDTT 的任意列。需要的是以下能力:

create database [Test]
create schema [Request]
create schema [Response]
create schema [Resources]
create schema [Services]

create schema [Metadata]

create table [Resources].[Foo] ( [Value] [varchar](max) NOT NULL, [CreatedBy] [varchar](max) NOT NULL) ON [PRIMARY]

insert into [Resources].[Foo] values("Bar", "kalanbates");

create type [Request].[Message] AS TABLE([Value] [varchar](max) NOT NULL)
create type [Response].[Message] AS TABLE([Resource] [varchar](max) NOT NULL, [Creator] [varchar](max) NOT NULL, [LastAccessedOn] [datetime] NOT NULL)

create PROCEDURE [Services].[GetResources]
(@request [Request].[Message] READONLY, @response [response].[Message] OUTPUT) 
AS
insert into @response
     select  [Resource].[Value] [Resource]
            ,[Resource].[CreatedBy] [Creator]
            ,GETDATE()  [LastAccessedOn]
     inner join @request as [Request] on [Resource].[Value] = [Request].[Value]
GO

并且让 ADO.NET 客户端能够说:

public IEnumerable<Resource> GetResources(IEnumerable<string> request)

using(SqlConnection connection = new SqlConnection("Server=blahdeblah;database=Test;notGoingToFillOutRestOfConnString")

   connection.Open();
   using(SqlCommand command = connection.CreateCommand())
   
      command.CommandText = "[Services].[GetResources]"
      command.CommandType = CommandType.StoredProcedure;

      SqlParameter _request;
      _request = command.Parameters.Add(new SqlParameter("@request","[request].[Message]");
      _request.Value = CreateRequest(request,_request.TypeName);
      _request.SqlDbType = SqlDbType.Structured;

      SqlParameter response = new SqlParameter("@response", "[response].[Message]")Direction = ParameterDirection.Output;
      command.Parameters.Add(response);

      command.ExecuteNonQuery();
      return Materializer.Create<List<ResourceEntity>>(response).AsEnumerable(); //or something to that effect.  

      //The point is, messages are sent to and received from the database.
      //The "result set" contained within response is not dynamic. It has a structure that can be *reliably* anticipated.
   



private static IEnumerable<SqlDataRecord> CreateRequest(IEnumerable<string> values, string typeName)

   //Optimally,
   //1)Call database stored procedure that executes a select against the information_schema to retrieve type metadata for "typeName", or something similar
   //2)Build out SqlDataRecord from returned MetaData

   //Suboptimally, hard code "[request].[Message]" metadata into a SqlMetaData collection
   //for example purposes.
   SqlMetaData[] metaData = new SqlMetaData[1];
   metaData[0] = new SqlMetaData("Value", SqlDbType.Varchar);
   SqlDataRecord record = new SqlDataRecord(metaData);
   foreach(string value in values)
   
      record.SetString(0,value);
      yield return record;
   


这里的重点是,使用这种结构,数据库将 [Response].[Message]、[Request].[Message] 和 [Services].[GetResource] 定义为其服务接口。调用客户端通过发送预先确定的消息类型与“GetResource”交互,并以预先确定的消息类型接收它们的响应。当然,它可以用 XML 输出参数来近似,您可以通过制定部落要求来推断预先确定的消息类型,即检索存储过程必须将其响应插入到本地 [Response].[Message] 表类型变量然后选择直接退出它以返回其结果。但是这些技术中没有一个比存储过程用其有效负载填充客户端提供的响应“信封”并将其发送回的结构几乎一样优雅。

【讨论】:

【参考方案5】:

仍然是 2020 年的 SQL 版本“Microsoft SQL Azure (RTM) - 12.0.2000.8”,我无法在存储过程中编辑表值参数。因此,我通过将数据移动到 Temp 表并对其进行编辑来解决问题。

ALTER PROCEDURE [dbo].[SP_APPLY_CHANGESET_MDTST09_MSG_LANG]
    @CHANGESET AS [dbo].[MDTSTYPE09_MSG_LANG] READONLY
AS
BEGIN

    SELECT * INTO #TCHANGESET FROM @CHANGESET 

    UPDATE #TCHANGESET SET DTST08_MSG_K = 0 WHERE ....
     ...............

【讨论】:

以上是关于为啥需要以只读方式输入 SQL Server 存储过程的表值参数?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的SQL server 在附加数据库后,数据库总是变成了只读?

应用程序意图 = 只读 SQL Server 连接错误

即席只读查询是不是存储在 SQL Server 事务日志中?

为啥我不能在 TypeScript 中以这种方式只读数组?

为啥 lambda 参数在 C++11 中以只读方式传递?

SQL Server数据库附加的数据库,是只读方式,怎样才能吧它弄成增删改查