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

NRe*_*ngh 15 sql-server t-sql

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

实现这一点的一种方法是简单地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
Run Code Online (Sandbox Code Playgroud)

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

Sol*_*zky 12

这种方法有几个缺陷:

  1. 在大多数情况下,术语“预览”可能会产生误导,这取决于正在操作的数据的性质(并且随着操作的不同而变化)。什么是确保当前正在操作的数据在收集“预览”数据和用户 15 分钟后回来之间处于相同状态 - 在喝了一些咖啡,走出去抽烟,走路之后在街区附近,回来,在 eBay 上检查一些东西——并意识到他们没有点击“确定”按钮来实际执行操作,所以最后点击了按钮?

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

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

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

    这种“低置信度”究竟从何而来?虽然SELECT当多个表被连接并且结果集中存在重复的行时,更新的行数可能与显示的行数不同,但这应该不是问题。任何应受 影响的行都可以UPDATE自行选择。如果不匹配,则您的查询不正确。

    并且那些由于 JOINed 表匹配将更新的表中的多行而导致重复的情况不是生成“预览”的情况。如果出现这种情况,则需要向用户解释他们更新了报告中重复的报告子集,以便在某人只是查看受影响的行数。

  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不会影响到表变量中的数据(这就是为什么你说你正在使用摆在首位表变量),所以它会更有意义,简单地SELECT FROM @output_to_return;底,然后甚至不打扰创建临时桌子。

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

这归结为:

  • 进行“预览”对于面向用户的操作没有多大意义。我经常为维护操作执行此操作,以便我可以查看如果继续操作将删除/收集的垃圾。我添加了一个名为的可选参数@TestMode并执行一个IF语句,该语句要么执行SELECTwhen @TestMode = 1else 执行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;
    
    Run Code Online (Sandbox Code Playgroud)
  • 如果这是您提到的报告方案,那么运行初始报告就是“预览”。如果有人想要更改他们在报告中看到的某些内容(可能是状态),则不需要额外的预览,因为期望的是更改当前显示的数据。

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

  • 如果您确实需要为面向最终用户的操作进行“预览”,那么您需要首先捕获数据的状态(可能是UPDATE操作结果集中所有字段的散列或DELETE操作),然后,在执行操作之前,将捕获的状态信息与当前信息进行比较——在事务中HOLD对表进行锁定,以便在进行此比较后没有任何变化——如果有任何差异,则抛出一个错误并执行 aROLLBACK而不是继续执行UPDATEor DELETE

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

    这两种处理检测与UPDATE操作相关的不一致状态的方法各有利弊,因此您需要确定哪种方法对您的系统具有更多的“优点”而不是“缺点”。但无论哪种情况,您都可以避免在生成预览和执行操作之间出现延迟,以免导致超出最终用户预期的行为。

  • 如果您正在执行面向最终用户的“预览”模式,那么除了在选择时捕获记录的状态、传递和在修改时检查之外,还包括DATETIMEforSelectTime和 populate viaGETDATE()或类似的东西。将其传递给应用程序层,以便将其传递回存储过程(很可能作为单个输入参数),以便在存储过程中对其进行检查。然后可以确定如果操作不是“预览”模式,则该@SelectTime值需要在 的当前值之前不超过X 分钟GETDATE()。也许2分钟?5分钟?最有可能不超过10分钟。如果DATEDIFFin MINUTES 超过该阈值,则抛出错误。


Cod*_*ior 3

我的担忧如下。

  • 事务处理不遵循嵌套在 Begin Try / Begin Catch 块中的标准模式。如果这是一个模板,那么在具有更多步骤的存储过程中,您可以在预览模式下退出此事务,并且数据仍被修改。

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

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

我没有提供更好的方法,但我认为你所拥有的不是一个好的模式。