触发两个必须相互更新的表的替代方案

时间:2014-01-14 10:04:28

标签: sql oracle triggers

(对不起,很长的帖子,但我想所有的信息都是必要的)

我们有两个表 - 任务和子任务。每个任务由一个或多个子任务组成,每个对象都有一个开始日期,结束日期和持续时间。此外,子任务有一个排序。

表格

create table task (
  pk number not null primary key, 
  name varchar2(30) not null,
  start_date date,
  duration_in_days number,
  end_date date,
  needs_recomputation number default 0
);

create table subtask (
  pk number not null primary key, 
  task_fk references task(pk),
  name varchar2(30) not null,
  start_date date,
  duration_in_days number,
  end_date date,
  ordering number not null
);

业务规则

  • 第一个子任务与任务
  • 具有相同的开始日期
  • 对于每个后续子任务,其开始日期等于前任
  • 的结束日期
  • 最后一个子任务与任务
  • 具有相同的结束日期
  • 为每个子任务和任务:start_date + duration = end_date
  • 执行任务:duration = sum(duration of subtasks)
  • 任务的结束日期和持续时间不能直接更改(感谢上帝!)

这直接为更新/删除生成以下要求:

  • 当任务的开始日期改变时,其第一个子任务的开始日期设置为相同的值,并重新计算所有子任务的开始日期和结束日期
  • 当子任务的开始日期,结束日期或持续时间发生变化时,其他字段会相应更新,所有后续子任务都会相应更新,最后,任务会相应更新
  • 删除子任务时,所有后续子任务都会相应更新,最后,任务会相应更新

当前方法

  • 任务表有一个触发器,用于更新第一个子任务,并在更改开始日期时设置needs_recomputation标志
  • 子任务表有一个触发器,它保持开始日期/结束日期/持续时间一致,并为父任务设置needs_recomputation标志(由于变异表问题,我们无法直接更新后续任务)
  • 为避免触发级联,每个触发器设置一个包变量以指示不应触发其他触发器
  • dbms_scheduler作业定期检查任务表并重新计算其needs_recomputation标志已设置的任务的数据

这种(有点)有效,但它有几个缺点:

  • 如果有多人同时更改同一任务的数据,我们可能会收到不一致的数据(请参阅AskTom on problems with triggers
  • 在子任务表更新后,我们有一个短时间段,数据不一致(直到下次同步作业运行)。目前,我们在GUI中的每个更改操作之后手动运行作业,但这显然容易出错

所以我的问题是 - 对此有什么明智的替代方法吗?

封装

create or replace package pkg_task is

  g_update_in_progress boolean;
  procedure recomputeDates(p_TaskID in task.pk%TYPE);

  procedure recomputeAllDates;
end;

create or replace package body pkg_task is

  procedure recomputeDates(p_TaskID in task.pk%TYPE) is
  begin
    g_update_in_progress := true;
    -- update the subtasks
    merge into subtask tgt
    using (select pk,
                  start_date,
                  duration_in_days,
                  end_date,
                  sum(duration_in_days) over(partition by task_fk order by ordering) as cumulative_duration,
                  min(start_date) over(partition by task_fk) + sum(duration_in_days) over(partition by task_fk order by ordering rows between unbounded preceding and 1 preceding) as new_start_date,
                  min(start_date) over(partition by task_fk) + sum(duration_in_days) over(partition by task_fk order by ordering) as new_end_date
             from subtask s
            where s.task_fk = p_TaskID
            order by task_fk,
                     ordering) src
    on (src.pk = tgt.pk)
    when matched then
      update
         set tgt.start_date = nvl(src.new_start_date,
                                  src.start_date),
             tgt.end_date   = nvl(src.new_end_date,
                                  src.end_date);
    -- update the task                                  
    merge into task tgt
    using (select p_TaskID as pk,
                  min(s.start_date) as new_start_date,
                  max(s.end_date) as new_end_date,
                  sum(s.duration_in_days) as new_duration
             from subtask s
            where s.task_fk = p_TaskID) src
    on (tgt.pk = src.pk)
    when matched then
      update
         set tgt.start_date          = src.new_start_date,
             tgt.end_date            = src.new_end_date,
             tgt.duration_in_days    = src.new_duration,
             tgt.needs_recomputation = 0;
    g_update_in_progress := false;
  end;

  procedure recomputeAllDates is
  begin
    for cur in (select pk
                  from task t
                 where t.needs_recomputation = 1)
    loop
      recomputeDates(cur.pk);
    end loop;
  end;

begin
  g_update_in_progress := false;
end;

触发器

create or replace trigger trg_task
before update on task
for each row
  begin
    if (:new.start_date <> :old.start_date and not pkg_task.g_update_in_progress) then
      pkg_task.g_update_in_progress := true;
      -- set the start date for the first subtask
      update subtask s 
      set s.start_date = :new.start_date
      where s.task_fk = :new.pk
      and s.ordering = 1;
      :new.needs_recomputation := 1;
      pkg_task.g_update_in_progress := false;      
    end if;
  end;

create or replace trigger trg_subtask
  before update on subtask
  for each row
declare
  l_date_changed boolean := false;
begin
  if (not pkg_task.g_update_in_progress) then
    pkg_task.g_update_in_progress := true;

    if (:new.start_date <> :old.start_date) then
      :new.end_date  := :new.start_date + :new.duration_in_days;
      l_date_changed := true;
    end if;
    if (:new.end_date <> :old.end_date) then
      :new.duration_in_days := :new.end_date - :new.start_date;
      l_date_changed        := true;
    end if;
    if (:new.duration_in_days <> :old.duration_in_days) then
      :new.end_date  := :new.start_date + :new.duration_in_days;
      l_date_changed := true;
    end if;

    if l_date_changed then
      -- set the needs_recomputation flag for the parent task
      -- if this is the first subtask, set the parent's start date, as well
      update task t
         set t.start_date         =
             (case
               when :new.ordering = 1 then
                :new.start_date
               else
                t.start_date
             end),
             t.needs_recomputation = 1
       where t.pk = :new.task_fk;
    end if;
    pkg_task.g_update_in_progress := false;
  end if;
end;

作业

begin
  dbms_scheduler.create_job(
      job_name => 'JOB_SYNC_TASKS'
     ,job_type => 'PLSQL_BLOCK'
     ,job_action => 'begin pkg_task.recomputeAllDates; commit; end; '

     ,start_date      => to_timestamp_tz('2014-01-14 10:00:00 Europe/Berlin',
                                         'yyyy-mm-dd hh24:mi:ss tzr')
     ,repeat_interval => 'FREQ=HOURLY;BYMINUTE=0,5,10,15,20,25,30,35,40,45,50,55'
     ,enabled => TRUE
     ,comments => 'Task sync job, runs every 5 minutes');
end;

2 个答案:

答案 0 :(得分:7)

在这里使用触发器只是在寻找麻烦。

此外,选择使用调度程序可能不是最好的主意,因为预定作业只能看到已提交的数据。因此要么你提交了触发器,它会将事务逻辑抛出窗口,要么对表的更改会延迟到事务结束。

你应该:

  1. 使用程序。最简单的答案。当您有多个应用程序时,它们不应该直接执行DML / businees逻辑,它们应该始终使用过程来执行它们以便它们都运行相同的代码。禁止直接使用授权或视图的DML。您可能需要通过视图上的INSTEAD OF触发器强制使用过程(仅当您无法修改应用程序时才考虑这一点。)

  2. 可能比您的情况下的程序更好:使用不包含重复数据的模式。您不希望存储冗余数据:这使得应用程序开发比需要的更复杂。在性能,资源和能源方面,解决问题的最佳方法是当您意识到任务不必要时。

    从模型的描述中,您可以删除以下列:

    • task.duration_in_days
    • task.end_date
    • task.needs_recomputation
    • subtask.start_date
    • subtask.end_date


    task表仅包含开始日期,每个子任务仅存储其持续时间。如果需要聚合信息,请使用联接。您可以使用视图让应用程序透明地访问数据。

  3. 使用使用包变量的mutating trigger workaround来标识具有BEFOREAFTER语句触发器的已修改行。显然,这将涉及大量难以编码,测试和维护的代码,因此您应尽可能使用选项(1)和(2)。

答案 1 :(得分:1)

从我的观点来看一些更一般的建议(据我了解你的要求):

删除列“duration_in_days”,这是多余的。您可以通过视图或查询提供此功能。

使触发器尽可能简单,即仅用于:

  • 从序列生成PK值(如果需要)。
  • 如果您不想删除列“duration_in_days”: 将持续时间计算为end_date - start_date

不要直接对表格进行任何更新或插入,提供PL / SQL程序,您可以在其中处理所有业务规则。

PROCEDURE INSERT_Task(id in task.pk%type, name in task.pk%type, start_date in task.start_date%type) is
...   

PROCEDURE INSERT_SubTask(Task_id in task.pk%type, subtask_id in subtask.pk%type,
name in subtask.name%type, start_date in subtask.start_date%type) is
...

PROCEDURE DELETE_SubTask(subtask_id in subtask.pk%type) is
...

etc.

然后你不需要重新计算持续时间或时间,并且更容易保持一致。