Oracle 触发器 - 变异表的问题

Posted

技术标签:

【中文标题】Oracle 触发器 - 变异表的问题【英文标题】:Oracle triggers - problem with mutating tables 【发布时间】:2011-01-09 10:47:39 【问题描述】:

我的桌子:

TableA (id number, state number)
TableB (id number, tableAId number, state number)
TableC (id number, tableBId number, state number)

所以 TableC 中的项是 TableB 的子项,TableB 中的项是 TableA 的子项。反之亦然 - TableA 中的项是 TableB 的父项,TableB 中的项是 TableC 的父项。

我想控制父项的状态...例如,我们有以下数据:

TableA (id, state): 
1, 40

TableB (id, tableAId, state): 
1, 1, 40
2, 1, 60

TableC (id, tableBId, state): 
1, 1, 40
2, 1, 50
3, 2, 60
4, 2, 70

父状态应该永远是他的孩子的最小状态。因此,如果我们现在像这样更新 TableC:

update TableC set state = 50 where Id = 1;

我的触发器应该自动更新 TableB(设置状态 = 50,其中 id = 1),然后也更新 TableA(设置状态 = 50,其中 id = 1)

我想使用触发器(AFTER UPDATE、INSERT、DELETE,在 TableA、TableB、TableC 上)来执行此操作,以便在每个操作之后执行此步骤:

    获取父ID 从当前父节点的所有子节点中找出最小的状态 如果所有子节点的最小状态大于父节点的状态,则更新父节点

如何避免“变异表错误”?在这个例子中使用自治事务是否节省?我看到了一些意见,变异表错误表明应用程序的逻辑存在缺陷 - 这是真的吗?如何更改我的逻辑以防止此错误?

谢谢


编辑: 感谢所有出色的答案!

最后,我使用了触发器(感谢 Vincent Malgrat,他指出了 Tom Kyte 的文章)。


编辑: 在 REAL END 中,我使用了存储过程并删除了触发器 :)

【问题讨论】:

【参考方案1】:

您已经注意到,使用触发器来满足您的业务需求是很困难的。原因是 Oracle可能为单个查询(并行 DML)同时更新/插入具有多个线程的表。这意味着您的会话无法查询它更新的表更新发生时

如果您真的想使用触发器执行此操作,则必须遵循 kind of logic shown in this article by Tom Kyte。如您所见,这并不简单。

还有另一种更简单、更优雅、更容易维护的方法:使用过程。撤销应用程序用户的更新/插入权限,并编写一组允许应用程序更新状态列的过程。

这些过程将锁定父行(以防止多个会话修改同一组行),并以高效、可读和易于维护的方式应用您的业务逻辑。

【讨论】:

你能看一下文章的网址吗?它似乎不再存在,如果您能在其他地方找到它并更新您的答案,那就太好了。 @RaduGheorghiu 不幸的是,该链接目前似乎已关闭。虽然您可以从AskTom site (resources page) 下载文章,但我在网上找不到合法副本。您需要有一个 Oracle 帐户才能下载演示文稿。演示文稿本身称为“MutatingTable.html”。 感谢您的努力 :) 有谁知道为什么 Oracle 没有选择 FOR READ ONLY 以便我们可以读取脏数据而不锁定该死的数据库。这不是 Oracle 选择更新的原因吗? @RichBianco select 不锁定任何东西。【参考方案2】:

恕我直言,您不应将触发器用于复杂的业务逻辑。将其移至存储过程(PL/SQL 包)或客户端代码。具有大量触发器的应用程序变得无法维护,因为您很快就会失去“动作序列”的任何感觉。

使用自治事务是绝对不安全的,仅将自治事务用于日志记录、跟踪、调试和审计。

阅读:http://www.oracle.com/technetwork/issue-archive/2008/08-sep/o58asktom-101055.html

在这里你可以阅读当你想使用触发器而不使用自治事务时如何解决问题:http://www.procaseconsulting.com/learning/papers/200004-mutating-table.pdf

【讨论】:

您的建议似乎不错,但 Oracle 触发器的功能并非如此。触发器的本质意味着基于事件执行操作 - Oracle 是...【参考方案3】:

您能否重构解决方案以包含视图来执行计算?

CREATE VIEW a_view AS
SELECT a.Id, min(b.State) State FROM tableA,tableB
WHERE a.Id=b.tableAId
GROUP BY a.Id;

我同意存储过程(如其他帖子中所建议的)也是一个不错的选择 - 但请注意,视图将自动保持最新,而我相信您必须安排运行存储过程保持数据“同步”:这可能很好 - 这取决于您的要求。

我想另一种选择是创建一些函数来进行计算,但我个人会选择视图方法(所有条件都相同)。

【讨论】:

是的,看起来状态是一个计算值。创建视图或存储过程来检索正确的值。 这个例子简化了更新父状态的逻辑,实际上它更复杂(它并不总是更新,有时在法律上父状态不是子状态的最小状态) .所以我认为我不能在这个例子中使用视图。好主意:) 那么我认为存储过程方法是可行的方法:按照文森特马尔格拉特建议的程度,您可以有效地构建最终用户(或应用程序)必须使用的 SP 界面修改数据。【参考方案4】:

我看到了一些意见,变异表错误表明存在逻辑缺陷 应用程序 - 这是真的吗?我怎样才能改变我的逻辑以防止这种情况 错误?

我不知道你在哪里看到的,但我知道之前已经多次发表过这种观点。

为什么我认为变异表通常表明数据模型存在缺陷?因为驱动 ORA-4091 代码的那种“要求”通常与糟糕的设计有关,尤其是规范化不足。

你的场景就是一个典型的例子。您得到 ORA-04091 是因为您在插入或更新它时从 TableC 中进行选择。但是你为什么选择TableC呢?因为您“需要”更新其父列TableB。但那一栏是多余的信息。在完全规范化的数据模型中,该列将不存在。

非规范化通常被吹捧为提高查询性能的一种机制。不幸的是,非规范化的支持者掩盖了它的成本,当我们插入、更新和删除时,这是以过于复杂的货币支付的。

那么,你怎么能改变你的逻辑呢?简单的答案是删除列,不要费心按父 ID 存储最小状态。相反,只要您需要该信息,请执行MIN() 查询。如果您经常需要它并且执行查询会很昂贵,那么您可以构建存储数据的物化视图(请务必使用ENABLE QUERY REWRITE

【讨论】:

这个例子简化了更新父状态的逻辑,实际上它更复杂(它并不总是更新,有时在法律上父状态不是子状态的最小状态) .所以我认为我不能在这个例子中使用视图。状态字段也必须在所有表中。【参考方案5】:

您可以同时使用触发器和 完整性约束来定义和 执行任何类型的完整性规则。 然而,甲骨文公司强烈 建议您使用触发器来 仅在 以下情况:

在以下情况下强制执行参照完整性 子表和父表已打开 分布式的不同节点 数据库 执行复杂的业务 无法使用完整性定义的规则 约束条件 参照完整性规则不能 使用以下完整性强制执行 约束:

不为空,唯一的 主键 外键 检查 删除级联 删除设置为空

source: Oracle9i Database Concepts

【讨论】:

【参考方案6】:

作为您的逻辑为什么会失败的示例,请假设 PARENT A 有记录 1 和 CHILD 记录 1A 和 1B。 1A 的状态是 10,而 1B 是 15,所以你希望你的父母是 10。

现在有人将 1A 的状态更新为 20,同时有人删除了 1B。因为 1B 的删除是未提交的,所以更新 1A 的事务仍然会看到 1B,并希望将父状态设置为 15,而删除 1B 的事务将看到旧的未提交值 1A,并且希望父状态为10.

如果您对此进行反规范化,您必须非常小心锁定,以便在插入/更新/删除任何子记录之前,父记录被锁定,执行您的更改,选择所有子记录,更新父,然后提交释放锁。虽然可以使用触发器来完成,但最好使用存储过程。

【讨论】:

我能否在使用 Vincent Malgrat 建议的触发器时防止这种情况发生(Tom Kytes 文章)? 只要将父级锁定在 BEFORE ROW TRIGGERS 中就可以了。为了清楚起见,我更愿意将其全部包含在一个程序中。【参考方案7】:

这样做是一个很大的诱惑,如果您按照其他人引用的 Tom Kyte 文章中的建议进行操作,这是可能的。然而,仅仅因为某事可以完成并不意味着它应该完成。我强烈建议您将这样的东西实现为存储过程/函数/包。尽管有明显的诱惑,但不应使用触发器执行此类复杂逻辑,因为它大大提高了系统的复杂性,而没有相应地增加效用。我不得不偶尔编写这样的代码,这并不令人愉快。

祝你好运。

【讨论】:

【参考方案8】:

不要使用自主交易,否则你会得到非常有趣的结果。

为避免变异表问题,您可以执行以下操作:

在 AFTER INSERT OR UPDATE OR DELETE FOR EACH ROW 触发器中,找出父 ID 并将其保存在 PL/SQL 集合中(在 PACKAGE 中)。 然后,在 AFTER INSERT OR UPDATE OR DELETE TRIGGER(语句级,没有“for each row”部分)中,从 PL/SQL 集合中读取父 ID,并相应地更新父表。

【讨论】:

以上是关于Oracle 触发器 - 变异表的问题的主要内容,如果未能解决你的问题,请参考以下文章

Oracle触发器-变异表触发器不能访问本表

AFTER UPDATE 触发器上的变异表错误

仅在 old = new 上进行选择时获取变异表 (ORA-04091)

PL/SQL 触发器得到一个变异表错误

什么是 Oracle 中的变异表? [复制]

在行触发器中取消插入值