级联UPDATE到相关对象

时间:2015-04-10 20:56:05

标签: database postgresql database-design triggers referential-integrity

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

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

以下是一个例子:

评估有很多提交。如果评估被标记为非活动状态,则与其相关的所有提交也应标记为无效。

3 个答案:

答案 0 :(得分:2)

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

所以我没有使用NULL。

首先,在主键和“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”列创建子表。声明引用父表的唯一约束(上面的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,它将级联到所有引用表。

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          !

如果将它们实现为有效时状态表,则软删除更简单,并且具有更好的语义。 FWIW,我认为术语软删除取消删除撤消在这种情况下都会产生误导,我认为你应该避免使用它们。< / p>

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

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

我没有在“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'。在此上下文中,它表示“父”,“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免费获得。

答案 1 :(得分:1)

您可以使用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();

相关:

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

@Mike added a solution with a multi-column FK constraint我会考虑替代。

关于dba.SE的相关答案:

一周后的相关答案:

答案 2 :(得分:0)

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

你可能已经躲过了创造一个可靠的定义&#34;什么构成了记录&#34;。目前你有对象A可能被对象B引用,而当A被&#34;删除&#34; (将is_active列设置为FALSE,或者在当前情况下为NULL)B未反映出来。听起来这是一个单独的表(你只提到行,而不是单独的类或表......)并且你有一个由自引用形成的层次模型。如果是这种情况,您可以通过以下几种方式来思考问题:

递归血统

在这个模型中,你有一个表包含一个地方的所有数据,无论是父项,子项等,你检查表是否有遍历树的递归引用。

在ORM中正确地执行此操作非常棘手,因为ORM缺乏对此的明确支持,而不会意外地编写以下任何一个例程:

  • 通过至少一个查询每个节点
  • 来迭代地从数据库中剔除垃圾
  • 立即拉动整个表并在应用程序代码中遍历它

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

如果这是一个博客条目+相同结构中的注释(一个相当常见的CMS模式),那么作为其自己父级的任何行都是主要实体,而任何父级本身不是评论的行。 。要删除整个博客帖子及其子项,您只需将博客帖子的行标记为非活动状态;将注释标记中的一个线程删除为非活动状态的注释。

对于博客+评论类型功能,这通常是最简单的做事方式 - 尽管大多数CMS系统都弄错了(但通常只有在以后开始做重要数据的情况下才有意义,如果你&#39 ;只是为人们在互联网上设立一些地方然后Worse is Better)。

递归血统+外部&#34;记录&#34;定义

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

当你有一些更有趣的事情而不仅仅是线程讨论时,使用它。例如,一个组件模型,其中事物树可以分别聚合到其他更大的事物中,并且您需要有一种方法来标记那些&#34;其他更大的事物&#34;作为活动的或不独立于组件本身。

进一步沿着兔子洞......

这个想法还有其他一些看法,但它们变得越来越不平凡,这可能不合适。例如,考虑对此模型的第三个基本考虑,其中层次结构,节点主体和主要实体都被分成不同的表。一个节点主体可能通过引用出现在多个树中,并且多个树可能在单个主实体的上下文中被视为活动或非活动等。

如果您的数据更复杂,请考虑使用此方向。如果你真的需要模型这个远程分解(&#34;标准化&#34;)那么我会提醒任何ORM可能会比它的价值更麻烦 - 你将开始一头扎进ORM基本上是漏洞抽象的问题(1个对象永远真的等于1个表......)。