【发布时间】:2014-02-02 08:13:40
【问题描述】:
(对不起,这篇文章很长,但我想所有的信息都是非常必要的)
我们有两个表 - 任务和子任务。每个任务由一个或多个子任务组成,每个对象都有开始日期、结束日期和持续时间。此外,子任务有顺序。
表格
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;
【问题讨论】:
-
逻辑中是否有某些东西使得从一个包过程中对两个表进行所有更新变得不切实际?对我来说,触发器似乎不是这种事情的明显首选工具,但不确定我是否遗漏了什么。 (大概……)
-
@AlexPoole 原则上,你是对的。但是这个数据被多个应用程序使用,如果我们设置这样一个程序,我们不能保证所有应用程序都真正使用这个程序:-(
-
好吧,您可以删除应用程序用户的插入/更新权限,这样他们就不能直接进行 CRUD 并且必须通过该过程。但我同意你的观点。
-
@AlexPoole 删除权限可能是一种可行的方法。感谢您的建议,我必须检查一下。