我正在尝试创建一个PL / PGSQL触发器函数来检查新行的日期范围,以确保表中没有其他日期范围重叠的行(对于相同的product_id) 。 我已经成功创建了该函数并将其设置为BEFORE INSERT触发器,但我正在试图弄清楚如何将其设置为BEFORE UPDATE触发器,因为触发器内的SELECT语句肯定会抛出异常,因为它适合重叠自身更新版本日期的标准。
这是我的功能:
CREATE OR REPLACE FUNCTION check_specials_dates()
RETURNS trigger AS
$$
DECLARE
BEGIN
IF EXISTS (SELECT * FROM rar.product_specials
WHERE product_id = NEW.product_id
AND (
(NEW.end_time between start_time and end_time) OR
(NEW.start_time between start_time and end_time) OR
(start_time between NEW.start_time and NEW.end_time))
THEN
RAISE EXCEPTION
'Cannot insert overlapping specials date for Product ID#%', NEW.product_id;
END IF;
RETURN NEW;
END
$$ LANGUAGE plpgsql;
我的想法是IF EXISTS SELECT语句将返回一个匹配项,因为它将在它尝试更新的行上匹配。
这是对的吗?如果是这样,我怎么能绕过它?
答案 0 :(得分:5)
您使用的是哪个版本的PostgreSQL?从9.0开始,您可以使用排除约束和cube / btree_gist扩展来实现所有这些。
为了使用触发器实现这一点,我通常会使用一个插入/更新后触发器,它使用相同的产品ID查看其他特殊内容,但插入/更新的行具有不同的主键。那就是:
IF EXISTS (SELECT 1 FROM rar.product_specials
WHERE product_specials.product_id = NEW.product_id
AND product_specials.product_special_id <> NEW.product_special_id
AND overlaps(NEW.start_time, NEW.end_time,
product_specials.start_time, product_specials.end_time))
如果您还没有为product_specials
生成主键,那么imho就可以添加一个。
(因为我一直需要提醒自己该怎么做,所以我想把它写在某处)
(请注意,如果您的开始/结束时间是离散的(例如日期或您可以将端点修复到足够大的粒度),那么您可以在由触发器填充的辅助表上使用唯一性约束:{{3 }})
PostgreSQL可以使用其广泛的运算符/索引方法基础结构来强制执行广义排除约束 - 如果任何其他行满足一组操作,则拒绝接受行。传统的唯一性约束本质上是一种特殊情况 - 如果行中的某些值/值集合等于到某个其他行的值/值,则会导致行被拒绝。 / p>
在您的情况下,如果与表中的其他行相比,product_id相等且范围(start_time,end_time)重叠,则希望拒绝行。
索引方法“gist”可用于构建索引以满足此类请求(具体地,重叠范围)。扩展名“cube”提供了一个通用数据类型,对此有一个gist-indexable,“btree_gist”为整数提供了一个gist索引方法,允许这两种类型组合在一个索引中。
所以在PostgreSQL 9.1中:
CREATE EXTENSION cube;
CREATE EXTENSION btree_gist;
(在9.0中,从contrib运行脚本)
以下是我测试的示例:
create table product_specials(product_special_id serial primary key,
product_id int not null,
start_time timestamp not null, end_time timestamp not null);
insert into product_specials(product_id, start_time, end_time)
values(1, '2011-10-31 15:00:00', '2011-11-01 09:00:00'),
(2, '2011-10-31 12:00:00', '2011-11-01 12:00:00'),
(1, '2011-11-01 15:00:00', '2011-11-02 09:00:00');
现在,这些范围不重叠,因此我们可以添加约束:
alter table product_specials add constraint overlapping_times exclude using gist (
product_id with = ,
cube(extract(epoch from start_time), extract(epoch from end_time)) with &&
);
cube(n1, n2)
创建一个从n1延伸到n2的一维“立方体”。 extract(epoch from t)
将时间戳t转换为数字。如果你有两个立方体,“&amp;&amp;”如果运算符重叠,则返回true。因此,这会为每行索引product_id和start_time / end_time“cube”,并且每次插入/更新行时,都会通过查找与新行的值匹配的现有行来测试约束:使用“=”测试product_id “运算符,以及带有”&amp;&amp;“的start_time / end_time”立方体“操作
如果您现在尝试插入冲突行,则会收到错误:
insert into product_specials(product_id, start_time, end_time)
values(2, '2011-10-31 00:00:00', '2011-10-31 13:00:00');
ERROR: conflicting key value violates exclusion constraint "overlapping_times"
DETAIL: Key (product_id, cube(date_part('epoch'::text, start_time), date_part('epoch'::text, end_time)))=(2, (1320019200),(1320066000)) conflicts with existing key (product_id, cube(date_part('epoch'::text, start_time), date_part('epoch'::text, end_time)))=(2, (1320062400),(1320148800)).
正如您所看到的,错误消息详细信息的易读性还有待改进! (@a_horse_with_no_name提到的文章PostgreSQL, triggers, and concurrency to enforce a temporal key中的“句点”类型可能会产生更好的类型)但是,功能完好无损。
使用约束排除解决了一些与我没有解决的锁定有关的问题。严格地说,在触发器中的“IF EXISTS ...”查询之前,您应该执行SELECT 1 FROM rar.product_specials WHERE product_specials.product_id = NEW.product_id FOR SHARE
以确保您正在测试的其他行都不会在被检查的约束和其事务提交之间发生变化。但是,当同时插入两个新特价时仍然存在竞争条件,其中没有什么可以锁定的 - 这是使用辅助表来排除离散值的动机,但是作为排除空间存在缩放问题变得更精细。
使用 PostgreSQL 9.2 ,将会有一个“范围”数据类型,这将消除使用立方体扩展或类似的需要。范围类型还允许正确指定边界是否在每端打开或关闭,而使用立方体边界始终在两端关闭(因此您需要做一些摆弄以避免日期范围重叠的错误)。像往常一样,Depesz在此功能上有一篇很好的帖子:http://thoughts.j-davis.com/2010/09/25/exclusion-constraints-are-generalized-sql-unique/
例如:
create table product_specials(product_special_id serial primary key,
product_id int not null,
applicable_dates tsrange not null);
insert into product_specials(product_id, applicable_dates)
values(1, tsrange('2011-10-31 15:00:00', '2011-11-01 09:00:00')),
(2, tsrange('2011-10-31 12:00:00', '2011-11-01 12:00:00')),
(1, tsrange('2011-11-01 15:00:00', '2011-11-02 09:00:00'));
alter table product_specials add exclude using gist (
product_id with =,
applicable_dates with &&
);
现在,如果您尝试插入冲突的行,您也会收到更易读的错误消息:
insert into product_specials(product_id, applicable_dates)
values(2, tsrange('2011-10-31 00:00:00', '2011-10-31 13:00:00'));
ERROR: conflicting key value violates exclusion constraint "product_specials_product_id_applicable_dates_excl"
DETAIL: Key (product_id, applicable_dates)=(2, ["2011-10-31 00:00:00","2011-10-31 13:00:00")) conflicts with existing key (product_id, applicable_dates)=(2, ["2011-10-31 12:00:00","2011-11-01 12:00:00")).
请注意,您不必更改表的架构以使用此新类型,因为您可以索引函数调用的结果。因此,使用范围类型来强制执行约束的细节不必放入应用程序或触发器中。那就是:
alter table product_specials add exclude using gist (
product_id with =,
tsrange(start_time, end_time) with &&
);