级联更新到相关对象

Cascade UPDATE to related objects

我已经将我的数据库和应用程序设置为软删除行。每个 table 都有一个 is_active 列,其中的值应该是 TRUENULL。我现在遇到的问题是我的数据不同步,因为与 DELETE 语句不同,将值设置为 NULL 不会级联到单独的 table 中的行"deleted" 另一行中的 table 是外键。

我已经采取措施通过从源 table 中查找非活动行并手动将其他 table 中的相关行也设置为非活动来更正数据。我认识到我可以在应用程序级别执行此操作(我在该项目中使用 Django/Python),但我觉得这应该是一个数据库进程。有没有办法利用类似 PostgreSQL 的 ON UPDATE 约束的方法,以便当一行 is_active 设置为 NULL 时,单独 table 中的所有行都将更新的行引用为外键也自动将 is_active 设置为 NULL

这是一个例子:

一个评估有很多提交。如果评估被标记为无效,则与其相关的所有提交也应标记为无效。

您可以使用 trigger:

CREATE OR REPLACE FUNCTION trg_upaft_upd_trip()
  RETURNS TRIGGER AS
$func$
BEGIN

UPDATE submission s
SET    is_active = NULL
WHERE  s.assessment_id = NEW.assessment_id
AND    NEW.is_active IS NULL;  -- recheck to be sure

RETURN NEW;                    -- call this BEFORE UPDATE

END
$func$  LANGUAGE plpgsql;

CREATE TRIGGER upaft_upd_trip
BEFORE UPDATE ON assessment
FOR EACH ROW
WHEN (OLD.is_active AND NEW.is_active IS NULL)
EXECUTE PROCEDURE trg_upaft_upd_trip();

相关:

  • How do I make a trigger to update a column in another table?

请注意,与具有 ON UPDATE CASCADE ON DELETE CASCADE 的 FK 约束相比,触发器具有更多可能的故障点。

我会考虑替代。

dba.SE上的相关回答:

一周后相关回答:

这是一个示意图问题,而不是程序问题。

您可能没有创建 "what constitutes a record" 的可靠定义。目前您有对象 A 可能被对象 B 引用,并且当 A 是 "deleted"(在您当前的情况下,其 is_active 列设置为 FALSE 或 NULL)B 没有反映.听起来这是一个单一的 table (你只提到行,而不是单独的 类 或 tables ...)并且你有一个通过自我引用形成的层次模型。如果是这种情况,您可以从几个方面考虑问题:

递归沿袭

在这个模型中,你有一个 table,它在一个地方包含所有数据,无论是父数据、子数据等等,你检查 table 的递归引用以遍历树.

在缺乏明确支持的 ORM 中正确执行此操作而不意外编写例程是很棘手的:

  • 通过对每个节点
  • 进行至少一次查询,迭代地清除数据库中的垃圾
  • 一次拉取整个table并在应用程序代码中遍历它

但是,在 Postgres 中执行此操作并让 Django 通过模型在您构建的沿袭查询的非托管视图上访问它是直接的。 (I wrote a little about this once.) 在此模型下,您的查询将沿着树下降,直到它到达标记为不活动的当前分支的第一行并停止,从而有效地截断下面与该行关联的所有行(不需要用于传播 is_active 列!)。

比方说,如果这是同一结构(相当常见的 CMS 模式)中的博客条目 + 评论,那么任何作为其父项的行都是主要实体,而任何具有非自身父项的行都是主要实体一条评论。要删除整个博客 post 及其子博客,您只需将博客 post 的行标记为不活动;要删除评论中的线程,请将该线程开始的评论标记为不活动。

对于博客 + 评论类型的功能,这通常是最直接的做事方式——尽管大多数 CMS 系统都会出错(但通常只有在您稍后开始处理重要数据的情况下才会出现这种情况,如果您只是在互联网上设置一些地方供人们争论 Worse is Better)。

递归沿袭+外部"record"定义

在此模型中,您将节点树和主要实体分开。主要实体被标记为活动或不活动,并且该属性对于与其相关的所有元素都是通用的在该主要实体的上下文中(它们存在并且具有独立的含义其中)。这意味着两个 table,一个用于主要实体,一个用于节点树。

当您有比简单的线程讨论更有趣的事情时使用它。例如,一个组件模型,其中一棵事物树可能会单独聚合成其他更大的事物,您需要有一种方法将这些 "other larger things" 标记为活动或不独立于组件本身。

再往下兔子洞...

这个想法还有其他观点,但它们变得越来越重要,这可能不是 suitable。例如,考虑此模型的第三种基本形式,其中层次结构、节点主体和主要实体都被分成不同的 table。一个节点主体可能通过引用出现在多棵树中,并且多棵树可能在单个主要实体的上下文中被认为是活动的或非活动的,等等。

如果您的数据更复杂,请考虑朝这个方向发展。如果你最终真的需要如此分解的模型("normalized"),那么我要提醒你,任何 ORM 最终可能会比它的价值更麻烦——你将开始 运行 直奔ORM 本质上是泄漏抽象的问题(1 个对象永远 真的 等于 1 table...)。

在我看来,使用 NULL 表示布尔值没有任何意义。 "is_active" 的语义表明唯一合理的值是 True 和 False。此外,NULL 会干扰级联更新。

所以我没有使用 NULL。

首先,创建 "parent" table,同时包含主键和主键的唯一约束以及 "is_active".

create table parent (
  p_id integer primary key,
  other_columns char(1) default 'x',
  is_active boolean not null default true,
  unique (p_id, is_deleted)
);

insert into parent (p_id) values
(1), (2), (3);

使用 "is_active" 列创建子项 table。声明引用父 table 的唯一约束中的列的外键约束(上面 CREATE TABLE 语句的最后一行)和级联更新。

create table child (
  p_id integer not null,
  is_active boolean not null default true,
  foreign key (p_id, is_active) references parent (p_id, is_active) 
    on update cascade,
  some_other_key_col char(1) not null default '!',
  primary key (p_id, some_other_key_col)
);

insert into child (p_id, some_other_key_col) values
(1, 'a'), (1, 'b'), (2, 'a'), (2, 'c'), (2, 'd'), (3, '!');

现在您可以将 "parent" 设置为 false,这将级联到所有引用的 table。

update parent 
set is_active = false 
where p_id = 1;

select *
from child
order by p_id;
p_id  is_active  some_other_key_col
--
1     f          a
1     f          b
2     t          a
2     t          c
2     t          d
3     t          !

如果将软删除实现为有效时间状态 table,则软删除会简单得多,语义也会好得多。 FWIW,我认为术语 soft deleteundeleteundo 在这种情况下都是误导性的,我认为你应该避免使用它们。

PostgreSQL 的范围数据类型对这类工作特别有用。我使用的是日期范围,但时间戳范围的工作方式相同。

对于此示例,我仅将 "parent" 视为有效时间状态 table。这意味着使特定行无效(软删除 特定行)也会使通过外键引用它的所有行无效。他们是直接还是间接引用它并不重要。

我没有在 "child" 上实施软删除。我可以做到,但我认为这会使基本技术变得难以理解。

create extension btree_gist; -- Necessary for the kind of exclusion
                             -- constraint below.

create table parent (
  p_id integer not null,
  other_columns char(1) not null default 'x',
  valid_from_to daterange not null,
  primary key (p_id, valid_from_to),
  -- No overlapping date ranges for a given value of p_id.
  exclude using gist (p_id with =, valid_from_to with &&)
);

create table child (
  p_id integer not null,
  valid_from_to daterange not null,
  foreign key (p_id, valid_from_to) references parent on update cascade,

  other_key_columns char(1) not null default 'x',
  primary key (p_id, valid_from_to, other_key_columns),

  other_columns char(1) not null default 'x'
);

插入一些示例数据。在 PostgreSQL 中,daterange 数据类型有一个特殊值 'infinity'。在此上下文中,这意味着 "parent"."p_id" 的值为 1 的行从 '2015-01-01' 直到永远有效。

insert into parent values 
(1, 'x', daterange('2015-01-01', 'infinity'));

insert into child values
(1, daterange('2015-01-01', 'infinity'), 'a', 'x'),
(1, daterange('2015-01-01', 'infinity'), 'b', 'y');

此查询将向您显示连接的行。

select *
from parent p 
left join child c 
       on p.p_id = c.p_id 
      and p.valid_from_to = c.valid_from_to;

要使行无效,请更新日期范围。此行(下方)从“2015-01-01”到“2015-01-31”有效。即2015-01-31被软删除

update parent
set valid_from_to = daterange('2015-01-01', '2015-01-31')
where p_id = 1 and valid_from_to = daterange('2015-01-01', 'infinity');

为 p_id 1 插入一个新的有效行,并选取在 1 月 31 日失效的子行。

insert into parent values (1, 'r', daterange(current_date, 'infinity'));

update child set valid_from_to = daterange(current_date, 'infinity')
where p_id = 1 and valid_from_to = daterange('2015-01-01', '2015-01-31');

Richard T Snodgrass 的开创性著作 在 SQL 中开发面向时间的数据库应用程序可从 his university web page 免费获得。