【问题标题】:Translation of DB2 Trigger to Oracle triggers ORA-04091 Error将 DB2 触发器转换为 Oracle 触发器 ORA-04091 错误
【发布时间】:2015-08-03 20:50:43
【问题描述】:

所以我有两个表:JOBS 和 TASKS。

TAKSS 表有一个 TAKS_STATUS 列,用于存储状态的进展(例如“提交”、“已提交”、“已授权”、“已完成”)。

JOBS 表有一个 JOB_STATUS 列,它是 TASKS 表中 TASK_STATUS 列的汇总(即最小状态)。 JOBS 表还有一个 TASK_COUNT 值,其中包含与作业关联的任务数。

作业可以有一个或多个任务:每个表中的 JOB_ID 链接它们。

在 DB2 中,我有一系列简单的触发器来汇总这种状态;这是中间的一个:

create or replace trigger JOB_AUTHORED
  after update of TASK_STATUS on TASKS
  referencing NEW as N
  for each row
  when (TASK_STATUS = 'Authored')
    update JOBS
      set JOB_STATUS = 'Authored'
      where JOBS.JOB_ID = N.JOB_ID
        and TASK_COUNT=(
          select count(0) from TASKS
            where TASKS.JOB_ID = N.JOB_ID
              and TASKS.TASK_STATUS in ('Authored','Completed'))

这在 DB2 中可以正常工作,因为触发器与触发事件在同一个工作单元中运行,因此它可以看到工作单元未提交的更改,并且可以计算刚刚发生的 TASK_STATUS 更改而不会触发行锁。

这是 Oracle 中翻译后的触发器:

create or replace trigger JOB_AUTHORED
  after update of TASK_STATUS on TASKS
  for each row
  when (NEW.TASK_STATUS = 'Authored')
  BEGIN
    update JOBS
      set JOB_STATUS='Authored'
      where JOBS.JOB_ID = :NEW.JOB_ID and TASK_COUNT=(
        select count(0) from TASKS
          where TASKS.JOB_ID = :NEW.JOB_ID
            and TASKS.TASK_STATUS in ('Authored','Completed'));
  END;

在 Oracle 中,这会失败:

ORA-04091: table MYSCHEMA.TASKS is mutating, trigger/function may not see it#012ORA-06512: at "MYSCHEMA.JOB_AUTHORED", line 1#012ORA-04088: error during execution of trigger 'MYSCHEMA.JOB_AUTHORED'#012] [query: UPDATE TASKS SET TASK_STATUS=:1 where TASK_ID=:2

显然,Oracle 的触发器不在同一上下文中运行,看不到未提交的触发更新,因此永远无法计算包括触发行在内的特定状态的任务数。

我想我可以将 AFTER 触发器更改为 INSTEAD OF 触发器并更新触发器内的 TASK_STATUS(以及 JOB_STATUS)(因此作业更新可以看到任务更新),但我会遇到同样的错误?也许不是第一个任务更新,但是如果触发程序在提交之前更新了一堆任务怎么办:当第二个任务更新时会发生什么?

我还考虑过删除触发器并让程序扫描活动作业以了解其任务的状态,但这似乎不优雅。

在 Oracle 中这样的最佳实践是什么?

【问题讨论】:

  • 您能解释一下为什么将 JOBS.TASK_COUNT 与 TASKS 表中的当前行数进行比较吗?我可以想象由于某些原因(应用程序错误?有人直接在表中插入/删除了一些行?)JOBS 中的计数将与 TASKS 表中相关行的计数不匹配,并且表中的数据不是处于一致状态。在这种情况下,您的触发器将静默失败,并且不会给出预期的原因。您如何执行规则JOBS.TASK_COUNT must always match the number of rows in TASKS table
  • 我计算处于特定状态或更高状态的 TASKS 的数量,以查看它是否与 TASK_COUNT 匹配,从而知道所有 TASKS 都已到达特定点(在此示例中:已创建) .因此,再举个例子,假设创建了一个 Job,其中有 20 个处于 Submitting Status 的 TASKS,它们开始运行并经历各种状态。当第一个任务点击“已授权”时,此触发器会检查是否有 20 个任务处于“已授权”或“已完成”状态,以及是否有作业变为“已授权”状态。由于这是第一个“授权”任务,计数将为 1,因此不会更改作业。
  • 当第 20 个任务达到“已授权”状态时,触发器的选择计数变为真并将作业的状态设置为“已授权”。此时,有些任务可能处于“已授权”状态,有些可能处于稍后状态(例如“已完成”)。触发器正好触发了 20 次,沿途计数从 1 到 20。如果某些任务在之前或之后进入“错误”状态,那么 Job 将永远不会进入“已授权”(另一个触发器会导致 Job 汇总到“错误”状态)。

标签: oracle triggers ora-04091


【解决方案1】:

最佳做法是尽可能避免触发
请参阅这些链接以了解为什么不应该使用触发器的答案:
http://www.oracle.com/technetwork/testcontent/o58asktom-101055.html
http://rwijk.blogspot.com/2007/09/database-triggers-are-evil.html

使用过程(API)代替触发器 - 您可以创建一个包含几个过程的包,如 add_new_jobadd_new_taskchange_task_status 等,并将所有逻辑放在这些过程中(检查、更改任务的状态) ,改变工作的状态等)在一个地方。易于理解、易于维护、易于调试和易于跟踪错误。


如果你坚持使用触发器,那么你可以创建compound trigger,正如上面第一个链接中Tom Kyte 提到的workaround,例如:

create or replace TRIGGER JOB_AUTHORED
FOR UPDATE OF TASK_STATUS on TASKS
COMPOUND TRIGGER

  TYPE JOB_ID_TABLE_TYPE IS TABLE OF PLS_INTEGER INDEX BY PLS_INTEGER;  
  JOB_ID_TABLE JOB_ID_TABLE_TYPE;
  dummy CHAR;

    AFTER EACH ROW IS
     BEGIN
       -- SELECT null INTO dummy FROM JOBS WHERE job_id = :NEW.JOB_ID FOR UPDATE;
        JOB_ID_TABLE( :NEW.JOB_ID ) := :NEW.JOB_ID;
     END AFTER EACH ROW;

     AFTER STATEMENT IS
     BEGIN
       FORALL x IN INDICES OF JOB_ID_TABLE
         UPDATE jobs set JOB_STATUS='Authored'
         WHERE JOBS.JOB_ID = JOB_ID_TABLE( x )
           and TASK_COUNT=(
                  select count(0) from TASKS
                  where TASKS.JOB_ID = JOBS.JOB_ID
                    and TASKS.TASK_STATUS in ('Authored','Completed')
          );
     END AFTER STATEMENT;

END JOB_AUTHORED;

但是……
我不确定这个例子中是否有我们目前不知道的任何陷阱。

例如,在这种情况下有一个陷阱:
假设有 18 个任务,状态为 Authored

  • X 用户 A 运行 UPDATE TASK SET status 'Authored' WHERE task_id = 2。触发器被触发并看到 18+1 个已提交的任务 状态Authored
  • 在时间 X+10ms 用户 B 运行 UPDATE TASK1 SET status ' Authored' task_id = 4。触发器被触发并看到 18+1 状态为Authored的已提交任务
  • 在时间 X+20ms 用户 A 提交
  • 在时间 X+30ms 用户 B 提交
  • 最后我们有 21 个任务 状态为authored。但工作的状态尚未更改为 Authored(但如果任务数 = 20,则应更改为 Authored)。

为了避免这个陷阱,您可以在触发器的after each row 部分中使用SELECT null INTO dummy FROM JOBS WHERE job_id = :NEW.JOB_ID FOR UPDATE;,以便在JOBS 表中的相应记录处加锁,以便序列化访问(我在上面的示例中对此进行了注释)。
但我仍然不确定这是否是正确的解决方案 - 这可能会导致我目前无法想象和预测的场景中的一些死锁。

【讨论】:

  • 奇怪的是几天后我们同时回答了。 ;-)
  • 我想避免触发器,但我没有提到的是我们想避免更新所有正在修改 TASK_STATUS 的代理(这是在 JOBS 表之前创建的)更。所以,我想我会使用带有“SELECT ... FOR UPDATE”行锁的复合触发器解决方案,或者使用单独的扫描程序来扫描活动 JOBS 并检查其所有 TASKS 的最小 TASK_STATUS。
  • 顺便说一句,我一直在根据您在 Oracle 中提到的内容审查 DB2 触发器,但我相信它在 DB2 中有效:DB2 在触发器运行之前对 TASKS 行进行行锁定,因此它阻止在不同工作单元中运行的触发器中的选择完成,直到第一个工作单元提交。尽管这提供了一致的行为,但它确实会增加死锁的机会,具体取决于您在其他地方的 JOBS 和 TASKS 的糟糕程度。
  • 虽然FORALL 语句似乎有正确的语法,但我似乎无法让它在那里编译。如果我删除 FORALL 语句并将其替换为简单的更新(例如 UPDATE jobs set JOB_STATUS='Authored' WHERE 0=1),则触发器将编译。当我添加回FORALL x IN INDICES OF JOB_ID_TABLE(在虚拟UPDATE 前面)时,它失败了。由于集合的值与索引匹配,我什至尝试了VALUES OF 而不是INDICES OF,但仍然没有乐趣。我也尝试了JOBS.JOB_ID = JOB_ID_TABLE( JOBS.JOB_ID ) 而不是FORALL,但这也失败了。想法?
【解决方案2】:

简而言之 - 在 Oracle 中,您不能在触发器中 select from 构建触发器的表。否则你可能/你会得到 mutating table 错误。

您有多种选择:

1) 根本没有触发器 - 在我看来这是最好的,我会这样做(但可能主题更广泛,我们并不知道一切)。 创建视图,它取代了触发器的需要,例如:

create or replace view v_jobs_done as 
  select * from jobs where not exists 
    select 1 from tasks 
      where TASKS.JOB_ID = jobs.JOB_ID
        and TASKS.TASK_STATUS not in ('Authored','Completed')

2) 不要增加价值,而是使用减少价值 - 所以当 jobs.tasks_count 达到零时,您就知道一切都已完成。在这种情况下,您必须构建/重建您的其他触发器,

3) 接近你的提议 - 你可以使用 modern compound trigger - 我怀疑这里的性能,但它有效:

create or replace trigger Job_Authored
for update of task_status on tasks compound trigger

  type t_ids   is table of tasks.job_id%type;
  type t_cnts  is table of number;
  type t_job_counts is table of number index by varchar2(10);
  v_ids        t_ids;
  v_cnts       t_cnts;
  v_job_counts t_job_counts;

before statement is
  begin
    select job_id, count(1) 
      bulk collect into v_ids, v_cnts
      from tasks where tasks.task_status in ('Authored','Completed')
      group by job_id;

    for i in 1..v_ids.count() loop
      v_job_counts(v_ids(i)) := v_cnts(i);
    end loop;
  end before statement;

after each row is
  begin
    if :new.task_status = 'Authored' then 
      update jobs set job_status='Authored'
        where job_id = :new.job_id
          and task_count = v_job_counts(:new.job_id);
    end if;
  end after each row;
end Job_Authored;

【讨论】:

    【解决方案3】:

    最佳做法是尽可能避免触发
    请参阅此链接以了解为什么不应该使用触发器的答案:
    http://www.oracle.com/technetwork/testcontent/o58asktom-101055.html
    http://rwijk.blogspot.com/2007/09/database-triggers-are-evil.html

    使用过程(API)代替触发器 - 您可以创建一个包含几个过程的包,如 add_new_jobadd_new_taskchange_task_status 等,并将所有逻辑放在这些过程中(检查、更改任务的状态) ,改变工作的状态等)在一个地方。易于理解、易于维护、易于调试和易于跟踪错误。


    如果你坚持使用触发器,那么你可以创建compound trigger,正如上面第一个链接中Tom Kyte 提到的workaround,例如:

    create or replace TRIGGER JOB_AUTHORED
    FOR UPDATE OF TASK_STATUS on TASKS
    COMPOUND TRIGGER
    
      TYPE JOB_ID_TABLE_TYPE IS TABLE OF PLS_INTEGER INDEX BY PLS_INTEGER;  
      JOB_ID_TABLE JOB_ID_TABLE_TYPE;
      dummy CHAR;
    
        AFTER EACH ROW IS
         BEGIN
           -- SELECT null INTO dummy FROM JOBS WHERE job_id = :NEW.JOB_ID FOR UPDATE;
            JOB_ID_TABLE( :NEW.JOB_ID ) := :NEW.JOB_ID;
         END AFTER EACH ROW;
    
         AFTER STATEMENT IS
         BEGIN
           FORALL x IN INDICES OF JOB_ID_TABLE
             UPDATE jobs set JOB_STATUS='Authored'
             WHERE JOBS.JOB_ID = JOB_ID_TABLE( x )
               and TASK_COUNT=(
                      select count(0) from TASKS
                      where TASKS.JOB_ID = JOBS.JOB_ID
                        and TASKS.TASK_STATUS in ('Authored','Completed')
              );
         END AFTER STATEMENT;
    
    END JOB_AUTHORED;
    

    但是……
    我不确定这个例子中是否有我们目前不知道的任何陷阱。

    例如,在这种情况下有一个陷阱:
    假设有 18 个任务,状态为 Authored

    • X 用户 A 运行 UPDATE TASK SET status 'Authored' WHERE task_id = 2。触发器被触发并看到 18+1 个已提交的任务 状态Authored
    • 在时间 X+10ms 用户 B 运行 UPDATE TASK1 SET status ' Authored' task_id = 4。触发器被触发并看到 18+1 状态为Authored的已提交任务
    • 在时间 X+20ms 用户 A 提交
    • 在时间 X+30ms 用户 A 提交
    • 最后我们有 21 个任务 状态为authored。但工作的状态尚未更改为 Authored(但如果任务数 = 20,则应更改为 Authored)。

    为了避免这个陷阱,您可以在触发器的after each row 部分中使用SELECT null INTO dummy FROM JOBS WHERE job_id = :NEW.JOB_ID FOR UPDATE;,以便在JOBS 表中的相应记录处加锁,以便序列化访问(我在上面的示例中对此进行了注释)。
    但我仍然不确定这是否是正确的解决方案 - 它可能会导致我现在无法想象和预测的场景中的死锁。

    【讨论】:

      猜你喜欢
      • 2018-11-30
      • 1970-01-01
      • 2013-05-13
      • 2012-05-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-05-21
      • 2012-04-26
      相关资源
      最近更新 更多