为必须相互更新的两个表触发备选方案

Posted

技术标签:

【中文标题】为必须相互更新的两个表触发备选方案【英文标题】:Trigger alternatives for two tables that have to mutually update each other 【发布时间】: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 删除权限可能是一种可行的方法。感谢您的建议,我必须检查一下。 【参考方案1】:

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

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

您应该:

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

    在您的情况下可能甚至比过程更好:使用不包含重复数据的架构。您不想存储冗余数据:这会使应用程序开发变得比需要的更复杂。就性能、资源和精力而言,解决问题的最佳方法是当您意识到该任务是不必要的。

    根据您的模型描述,您可以删除以下列:

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

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

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

【讨论】:

五。明智的,尤其是在删除冗余数据方面。我还将研究使用集群来存储两个表,因为视图的使用将涉及在单个任务的数据集中进行大量查询,并且将记录放在一起会有所帮助。我会考虑存储任务开始日期或结束日期而不是持续时间,因为“显示所有明天开始的任务”形式的查询比“显示所有”形式的查询更有可能长任务”,并且您希望从索引或简单和早期的行过滤中获得一些好处,以将它们从数据中排除... ... 如果那种“给我看明天开始的所有任务”和“给我看明天结束的所有任务”的表现有问题(他们应该没问题,因为这可能永远不会将是一百万行表)然后物化视图可能会很方便。 @DavidAldridge 我同意,出于性能原因存储冗余数据是合理的。你是对的,搜索给定日期的所有子任务会导致 FTS,如果无法以某种方式对问题进行分区,则可能会出现问题。在这种情况下,使用物化视图可能是一个好主意:它是一种声明性解决方案,不涉及大量编码并自动处理数据同步。 @VincentMalgrat 感谢您的意见。我将尝试推动删除重复数据并仅使用包来更新数据。【参考方案2】:

从我的角度来看一些更一般的建议(据我了解您的要求):

删除列“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.

这样您就不需要重新计算持续时间或时间,而且保持一切一致会容易得多。

【讨论】:

+1 感谢您的回答 - 我接受了 Vincent Maigrat 的回答,因为它更广泛。

以上是关于为必须相互更新的两个表触发备选方案的主要内容,如果未能解决你的问题,请参考以下文章

更新触发器以将两个不同表中的数据添加到审计表中

更新/插入/删除表中的行后触发的触发器有问题

sql server 触发器 从一个表添加到另个表是否两个表对应的字段必须相同呢

unity同一个gameobject上可以同时存在碰撞体和触发器吗

更新表上的触发器[关闭]

列更新时的 SQL mariaDB 触发器