具有“预览模式”的数据库存储过程


15

在我使用的数据库应用程序中,一个相当普遍的模式是需要为具有“预览模式”的报表或实用程序创建存储过程。当此类过程确实更新时,此参数指示应返回操作结果,但该过程实际上不应执行对数据库的更新。

一种实现方法是简单地if为参数编写一条语句,并具有两个完整的代码块。其中一个确实更新并返回数据,而另一个仅返回数据。但这是不希望的,因为代码重复并且相对较低的可信度,即预览数据实际上是对更新所发生情况的准确反映。

下面的示例尝试利用事务保存点和变量(与临时表相反,不受事务影响)与临时表相比,仅将单个代码块用作实时更新模式。

注意:不能选择事务回滚,因为此过程调用本身可能嵌套在事务中。这已在SQL Server 2012上进行了测试。

CREATE TABLE dbo.user_table (a int);
GO

CREATE PROCEDURE [dbo].[PREVIEW_EXAMPLE] (
  @preview char(1) = 'Y'
) AS

CREATE TABLE #dataset_to_return (a int);

BEGIN TRANSACTION; -- preview mode required infrastructure
  DECLARE @output_to_return TABLE (a int);
  SAVE TRANSACTION savepoint;

  -- do stuff here
  INSERT INTO dbo.user_table (a)
    OUTPUT inserted.a INTO @output_to_return (a)
    VALUES (42);

  -- catch preview mode
  IF @preview = 'Y'
    ROLLBACK TRANSACTION savepoint;

  -- save output to temp table if used for return data
  INSERT INTO #dataset_to_return (a)
  SELECT a FROM @output_to_return;
COMMIT TRANSACTION;

SELECT a AS proc_return_data FROM #dataset_to_return;
RETURN 0;
GO

-- Examples
EXEC dbo.PREVIEW_EXAMPLE @preview = 'Y';
SELECT a AS user_table_after_preview_mode FROM user_table;

EXEC dbo.PREVIEW_EXAMPLE @preview = 'N';
SELECT a AS user_table_after_live_mode FROM user_table;

-- Cleanup
DROP TABLE dbo.user_table;
DROP PROCEDURE dbo.PREVIEW_EXAMPLE;
GO

我正在寻找有关此代码和设计模式的反馈,并且/或者是否希望以不同的格式存在针对同一问题的其他解决方案。

Answers:


12

这种方法有几个缺陷:

  1. 在大多数情况下,“预览”一词可能会引起误导,具体取决于要操作的数据的性质(以及操作之间的变化)。在采集“预览”数据的时间与用户在15分钟后返回时之间要确保正在操作的当前数据处于相同的状态-喝咖啡之后,走到外面抽烟,步行围绕该块,然后返回并在eBay上进行检查-并意识到他们没有单击“ OK”按钮来实际执行操作,因此最终单击了该按钮?

    生成预览后,您是否有进行操作的时间限制?或者是一种确定数据在修改时与初始SELECT时处于相同状态的方法?

  2. 这只是次要点,因为示例代码本可以草率完成,并且不能代表真实的用例,但是为什么要对INSERT操作进行“预览” ?当通过类似的方式插入多行时,这可能很有意义,并且插入的行INSERT...SELECT数可能是可变的,但这对于单例操作没有多大意义。

  3. 这是不理想的,因为...相对较低的可信度,即预览数据实际上是对更新所发生情况的准确反映。

    这种“低置信度”到底从何而来?尽管可以SELECT联接多个表,而联接多个表并且结果集中有重复的行时,可以更新的行数可以与显示的行数不同,但这在这里不应该成为问题。应该受到影响的任何行UPDATE都是可以自行选择的。如果不匹配,则说明您在错误地进行查询。

    并且那些由于JOINed表与将要更新的表中的多行匹配而导致重复的情况不是将生成“ Preview”的情况。并且,如果有这种情况,则需要向用户说明,他们已更新了该报告的子集,该子集在报告中重复出现,因此如果仅某人只是一个错误,就不会出现错误。查看受影响的行数。

  4. 为了完整起见(即使其他答案提到了这一点),您未使用该TRY...CATCH构造,因此在嵌套这些调用时很容易遇到问题(即使不使用保存点,甚至不使用事务)。请在DBA.SE上的以下问题中查看我对以下问题的回答,以获取用于处理跨嵌套存储过程调用的事务的模板:

    我们是否需要使用C#代码以及存储过程来处理事务

  5. 即使解决了上面提到的问题,仍然存在一个严重的缺陷:在执行操作的短时间内(即,在之前ROLLBACK),任何脏读查询(使用WITH (NOLOCK)或的查询SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED)都可以获取可以一会儿没来。当使用脏读查询任何人都应该已经意识到这一点,并已接受了这种可能性,如该业务大大增加是引入数据异常的机率非常调试困难的(意思是:你要多少时间花在试图找到没有明显直接原因的问题?)。

  6. 像这样的模式还会通过获取更多锁来增加阻塞并生成更多事务日志活动,从而降低系统性能。(我现在看到,@ MartinSmith在对“问题”的评论中也提到了这两个问题。)

    此外,如果要修改的表上有触发器,则可能是不必要的大量附加处理(CPU和物理/逻辑读取)。触发器还将进一步增加由于脏读取而导致数据异常的机会。

  7. 与上面直接指出的观点-增加的锁-事务的使用增加了陷入死锁的可能性,尤其是在涉及触发器的情况下。

  8. 不太严重的问题应该只与不太可能发生的INSERT操作场景有关:“预览”数据可能与关于由DEFAULT约束(Sequences/ NEWID()/ NEWSEQUENTIALID())和确定的列值所插入的数据不同IDENTITY

  9. 不需要将表变量的内容写入临时表的额外开销。这ROLLBACK不会影响Table Variable中的数据(这就是为什么您首先说要使用Table Variables的原因),因此仅SELECT FROM @output_to_return;在最后使用就更有意义了,然后甚至不必费心创建Temporary表。

  10. 万一这种保存点的细微差别是未知的(很难从示例代码中看出来,因为它仅显示一个存储过程):您需要使用唯一的保存点名称,以便ROLLBACK {save_point_name}操作按预期的方式进行。如果您重复使用这些名称,则ROLLBACK将回滚该名称的最新保存点,该保存点可能与ROLLBACK调用该名称的嵌套级别不同。请查看下面的答案中的第一个示例代码块,以查看此行为的实际效果:存储过程中的事务

这归结为:

  • 进行“预览”对于面向用户的操作没有多大意义。我经常进行维护操作,这样我可以查看如果继续进行该操作将删除什么/收集垃圾。我添加了一个称为的可选参数,@TestMode并执行了一条IF语句,该语句要么执行一次SELECT@TestMode = 1否则执行DELETE。有时,我会将@TestMode参数添加到应用程序调用的存储过程中,以便我(和其他人)可以进行简单测试而不会影响数据状态,但是应用程序从不使用此参数。

  • 万一从“问题”的顶部看不清楚,以防万一:

    如果确实需要/希望使用“预览” /“测试”模式来查看要执行DML语句应受到的影响,则不要使用事务(即BEGIN TRAN...ROLLBACK模式)来完成此操作。这种模式充其量只能在单用户系统上真正起作用,在这种情况下甚至不是一个好主意。

  • 在语句的两个分支之间重复大量查询IF确实存在潜在的问题,即每次进行更改时都需要更新两个查询。但是,两个查询之间的差异通常很容易捕获代码审查,并且易于修复。另一方面,诸如状态差异和脏读之类的问题很难发现和解决。系统性能下降的问题无法解决。我们需要认识到并接受SQL不是面向对象的语言,并且像其他许多语言一样,封装/减少重复的代码也不是SQL的设计目标。

    如果查询足够长/复杂,则可以将其封装在一个内联表值函数中。然后,您可以SELECT * FROM dbo.MyTVF(params);为“预览”模式做一个简单的操作,并为“做到”模式连接到关键值。例如:

    UPDATE tab
    SET    tab.Col2 = tvf.ColB
           ...
    FROM   dbo.Table tab
    INNER JOIN dbo.MyTVF(params) tvf
            ON tvf.ColA = tab.Col1;
  • 如果这是您提到的报告方案,则运行初始报告为“预览”。如果某人想要更改他们在报表上看到的内容(可能是状态),则不需要额外的预览,因为期望更改的是当前显示的数据。

    如果操作可能是按某个百分比或业务规则更改出价,则可以在表示层(JavaScript?)中进行处理。

  • 如果您确实需要对面向最终用户的操作进行“预览”,则需要首先捕获数据的状态(可能是UPDATE操作结果集中的所有字段的哈希值,或者是DELETE操作),然后在执行该操作之前,将捕获的状态信息与当前信息进行比较- 在Transaction中HOLD对表进行锁定,这样在进行比较后就不会有任何变化-如果有任何区别,则抛出一个错误并执行ROLLBACK而不是继续操作UPDATEor DELETE

    为了检测UPDATE操作的差异,在相关字段上计算哈希的另一种方法是添加ROWVERSION类型的列。ROWVERSION每当该行发生更改时,数据类型的值就会自动更改。如果有这样的列,则将SELECT其与其他“预览”数据一起使用,然后将其与键值和值一起传递给“确定,继续进行更新”步骤改变。然后,您可以比较这些ROWVERSION传入从“预览”当前值(每个键)值,并且只用继续UPDATE,如果ALL匹配的值。这样做的好处是您不需要计算散列,该散列即使不大可能也有可能产生假负数,并且每次执行时需要花费一些时间SELECT。另一方面,该ROWVERSION值仅在更改后才自动增加,因此您无需担心。但是,该ROWVERSION类型为8个字节,在处理许多表和/或许多行时可以加起来。

    这两种方法的优缺点都在于检测与UPDATE操作相关的不一致状态,因此您需要确定系统中哪种方法的“ 优点”多于“优点”。但是,无论哪种情况,都可以避免在生成预览和执行操作之间造成延迟,以免引起最终用户期望之外的行为。

  • 如果您正在执行面向最终用户的“预览”模式,则除了在选择时捕获记录的状态,传递记录并在修改时检查之外,还包括DATETIMEfor SelectTime和via GETDATE()或类似内容。将其传递到应用程序层,以便可以将其传递回存储过程(通常可能作为单个输入参数),以便可以在存储过程中对其进行检查。然后,您可以确定该操作是否不是“预览”模式,则该@SelectTime值需要不超过当前值X分钟GETDATE()。也许2分钟?5分钟?最有可能不超过10分钟。如果DATEDIFFMINUTES中的超过该阈值,则会引发错误。


4

最简单的方法通常是最好的方法,而对于SQL中的代码复制,尤其是不在同一模块中,我真的没有那么多问题。毕竟这两个查询在做不同的事情。因此,为什么不采用“ Route 1”或“ Keep It Simple”,而在存储的过程中只有两个部分,一个模拟您需要做的工作,另一个模拟要做的工作,例如:

CREATE TABLE dbo.user_table ( rowId INT IDENTITY PRIMARY KEY, a INT NOT NULL, someGuid UNIQUEIDENTIFIER DEFAULT NEWID() );
GO
CREATE PROCEDURE [dbo].[PREVIEW_EXAMPLE2]

    @preview CHAR(1) = 'Y'

AS

    SET NOCOUNT ON

    --!!TODO add error handling

    IF @preview = 'Y'

        -- Simulate INSERT; could be more complex
        SELECT 
            ISNULL( ( SELECT MAX(rowId) FROM dbo.user_table ), 0 ) + 1 AS rowId,
            42 AS a,
            NEWID() AS someGuid

    ELSE

        -- Actually do the INSERT, return inserted values
        INSERT INTO dbo.user_table ( a )
        OUTPUT inserted.rowId, inserted.a, inserted.someGuid
        VALUES ( 42 )

    RETURN

GO

这样做的优点是可以自我记录(即IF ... ELSE易于遵循),较低的复杂度(与使用表变量方法IMO的保存点相比),因此,不太可能出现错误(@Cody的最高点)。

关于您关于低信心的观点,我不确定我是否理解。从逻辑上讲,两个具有相同条件的查询应该执行相同的操作。UPDATE和之间的基数可能不匹配SELECT,但这将是您的联接和条件的特征。您能进一步解释吗?

顺便说一句,您应该设置NULL/ NOT NULL属性以及表和表变量,并考虑设置主键。

你原来的做法似乎有点过于复杂也可能会被更容易出现死锁,如INSERT/ UPDATE/ DELETE操作要求较高的锁定水平比平原SELECTs

我怀疑您的现实过程比较复杂,因此如果您认为上述方法对他们不起作用,请返回更多示例。


3

我的关注如下。

  • 事务处理未遵循嵌套在Begin Try / Begin Catch块中的标准模式。如果这是模板,则在具有更多步骤的存储过程中,您可以在预览模式下退出该事务,同时仍修改数据。

  • 遵循格式会增加开发人员的工作量。如果他们更改内部列,则还需要修改表变量定义,然后修改临时表定义,然后最后修改插入列。它不会流行。

  • 某些存储过程并非每次都返回相同格式的数据。将sp_WhoIsActive视为一个常见示例。

我没有提供更好的方法来执行此操作,但我认为您拥有的不是一个好的模式。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.