在 Oracle 插入触发器期间更新以前的记录版本

Posted

技术标签:

【中文标题】在 Oracle 插入触发器期间更新以前的记录版本【英文标题】:Update previous record version during Oracle insert trigger 【发布时间】:2016-10-05 14:24:05 【问题描述】:

这是我在 Table_A 上将参数存储到系统的插入触发器。当我插入表时,我想更改最后一条记录的 end_date 以保持记录版本控制。

create or replace trigger parameter_version
  before insert
  on parameters
  for each row
declare
  v_is_exist number := 0;
  v_rowid rowid;
begin  
  select count(*) into v_is_exist from parameters where name = :new.name; -- check if parameter exist
  select rowid into v_rowid from parameters where name = :new.name and end_date is null; -- record rowid, which sholud be changed    
  if v_is_exist <> 0 then     
    set end_date = :new.start_date - 1
  end if;
end;

插入前表中的情况是:

| id | name | value | start_date |  end_date  |
-----------------------------------------------
| 1  |Par_A |   10  | 2016-09-01 | 2016-10-01 |
-----------------------------------------------
| 2  |Par_A |   20  | 2016-10-02 | 2016-10-03 |
-----------------------------------------------
| 3  |Par_A |   30  | 2016-10-05 |   <null>   |
-----------------------------------------------

id=3 的记录应将 end_date 设置为 :new_start_date - 1(关闭版本),并且在插入记录时,我有一个 start_date = sysdate 的下一个参数版本。

我遇到了 ORA-04091 错误“表名正在发生变化,触发器/函数可能看不到它”。

我知道这个案子很难,而且可能不可能,但也许有人知道解决方案? 或者可能存在另一种解决方案?

【问题讨论】:

这不是使用触发器的好选择。正如错误消息所暗示的那样,表的内容仍在不断变化,并且在更新所有相关行后触发器可能不会触发。让调用代码进行必要的更新要好得多。如果这是一个严格的要求,最好研究一下Compound Triggers 取决于您的 Oracle 数据库版本,请查看compound triggers(仅作为最后的手段,我认为您应该修改您的设计) 您需要使用语句级触发器。行级触发器无法修改正在更新的表。 【参考方案1】:

您可以使用带有 LEAD 分析函数的 After Statement 触发器来处理此问题:

DROP TABLE demo;

CREATE TABLE demo( id          NUMBER
                 , name        VARCHAR2( 30 )
                 , VALUE       NUMBER
                 , start_date  DATE
                 , end_date    DATE
                  );

INSERT INTO demo( id, name, VALUE, start_date, end_date )
     VALUES ( 1, 'Par_A', 10, TO_DATE( '2016-09-01', 'YYYY-MM-DD' ), TO_DATE( '2016-10-01', 'YYYY-MM-DD' ) );

INSERT INTO demo( id, name, VALUE, start_date, end_date )
     VALUES ( 2, 'Par_A', 20, TO_DATE( '2016-10-02', 'YYYY-MM-DD' ), TO_DATE( '2016-10-04', 'YYYY-MM-DD' ) );

INSERT INTO demo( id, name, VALUE, start_date )
     VALUES ( 3, 'Par_A', 30, TO_DATE( '2016-10-05', 'YYYY-MM-DD' ) );

INSERT INTO demo( id, name, VALUE, start_date )
     VALUES ( 4, 'Par_A', 40, TO_DATE( '2016-10-07', 'YYYY-MM-DD' ) );

INSERT INTO demo( id, name, VALUE, start_date )
     VALUES ( 5, 'Par_A', 50, TO_DATE( '2016-10-11', 'YYYY-MM-DD' ) );

COMMIT;

SELECT   id
       , name
       , start_date
       , end_date
       , LEAD( start_date ) OVER( PARTITION BY name ORDER BY start_date ) - 1 AS new_date
    FROM demo
   WHERE end_date IS NULL
ORDER BY id;

CREATE OR REPLACE TRIGGER demo_aius
    AFTER INSERT OR UPDATE
    ON demo
    REFERENCING NEW AS new OLD AS old
DECLARE
    CURSOR c_todo
    IS
        SELECT id, new_date
          FROM (SELECT id
                     , name
                     , start_date
                     , end_date
                     , LEAD( start_date ) OVER( PARTITION BY name ORDER BY start_date ) - 1 AS new_date
                  FROM demo
                 WHERE end_date IS NULL)
         WHERE new_date IS NOT NULL;
BEGIN
    FOR rec IN c_todo
    LOOP
        UPDATE demo
           SET end_date  = rec.new_date
         WHERE id = rec.id;
    END LOOP;
END demo_aius;
/

INSERT INTO demo( id, name, VALUE, start_date )
     VALUES ( 6, 'Par_A', 60, TO_DATE( '2016-10-15', 'YYYY-MM-DD' ) );

COMMIT;

SELECT   id
       , name
       , start_date
       , end_date
    FROM demo
ORDER BY id;

就像脚本显示的那样,这样的更新甚至可以处理多个丢失的结束日期,以防触发器被意外禁用。 “PARTITION BY name”部分确保它在复杂的插入语句之后也能正常工作。

顺便说一句,我同意触发器中的自治事务是最后的手段。我试图通过控制用户界面并将所有此类功能放入包中来避免触发。

【讨论】:

我已经尝试过这个解决方案,但我仍然有 ora-04091 错误。 PL/SQL Developer 标记定义光标 c_todo 的行(选择 id...) 你好 seeksoul,这个触发器只能没有 FOR EACH ROW。如果这不是问题,请将 all 触发器发布到您正在测试的表上【参考方案2】:

试试这样的:

create or replace trigger parameter_version
   before insert
   on parameters
   for each row
begin
   /*Don't care if there's 0 rows updated */
   update parameters
      set end_date = :new.start_date - 1
    where name = :new.name and end_date is null;

   :new.end_date := null;
end;

【讨论】:

你不能在 Oracle 中这样做——这会触发臭名昭著的“表正在变异”错误 好的,既然我有这个错误,我就不确定了。我只需在处理插入的过程/包中执行此操作。

以上是关于在 Oracle 插入触发器期间更新以前的记录版本的主要内容,如果未能解决你的问题,请参考以下文章

从oracle中的触发器插入多条记录

oracle 触发器插入和更新时,怎么操作

PL/SQL ORACLE:删除时触发更新

Oracle-触发器和程序包

为啥我的触发器在 SSIS 插入期间没有触发?

尝试创建我的第一个 oracle 触发器时出现问题