0

我有一种情况,我需要确保在任何时候只有一个具有相同 object_id 和 user_id 的活动记录。这是一个有代表性的表格:

CREATE TABLE actions (
    id SERIAL PRIMARY KEY,
    object_id integer,
    user_id integer,
    active boolean default true,
    created_at timestamptz default now()
);

一次只有一个活动记录,我的意思是你可以有一系列插入,如下所示:

insert into actions (object_id, user_id, active) values (1, 1, true);
insert into actions (object_id, user_id, active) values (1, 1, false);

但做一个后续

insert into actions (object_id, user_id, active) values (1, 1, true);

应该会失败,因为此时已经存在 1 个 object_id = 1 且 user_id = 1 的活动元组。

我正在使用 PostgreSQL 8.4。

我看到这篇文章看起来很有趣,但它是特定于 Oracle 的。

我也看到了这篇文章,但它需要更多地关注事务隔离级别。我认为它不会在读取提交模式下按原样工作。

我的问题是还有哪些其他选项可用于不确定这种约束?

编辑:删除了第一组中的第三个插入。我认为这使示例感到困惑。我还添加了 created_at 时间戳来帮助处理上下文。重申一下,可以有多个 (object_id, user_id, false) 元组,但只有一个 (object_id, user_id, true) 元组。

更新:我接受了克雷格的回答,但对于其他可能偶然发现类似问题的人来说,这是另一种可能的(虽然不是最佳的)解决方案。

CREATE TABLE action_consistency (
    object_id integer,
    user_id integer,
    count integer default 0,
    primary key (object_id, user_id),
    check (count >= 0 AND count <= 1)
);


CREATE OR REPLACE FUNCTION keep_action_consistency()
  RETURNS TRIGGER AS
$BODY$
    BEGIN

    IF NEW.active THEN
        UPDATE action_consistency
        SET count = count + 1
        WHERE object_id = NEW.object_id AND
        user_id   = NEW.user_id;

        INSERT INTO action_consistency (object_id, user_id, count)
        SELECT NEW.object_id, NEW.user_id, 1
        WHERE NOT EXISTS (SELECT 1 
                          FROM action_consistency
                          WHERE object_id = NEW.object_id AND
                                user_id   = NEW.user_id);
    ELSE
        -- assuming insert will be active for simplicity
        UPDATE action_consistency
        SET count = count - 1
        WHERE object_id = NEW.object_id AND
        user_id   = NEW.user_id;
    END IF;

    RETURN NEW;

    END;
$BODY$
LANGUAGE plpgsql;

CREATE TRIGGER ensure_action_consistency AFTER INSERT OR UPDATE ON actions
    FOR EACH ROW EXECUTE PROCEDURE keep_action_consistency();

它需要使用跟踪表。由于我希望是显而易见的原因,这根本不是可取的。这意味着您在操作中每个不同的(object_id,user_id)都有一个额外的行。

我接受@Craig Ringer 回答的另一个原因是,当给定的操作元组更改状态时,其他表中也存在对 actions.id 的外键引用。这就是为什么历史表在这种情况下不太理想的原因。感谢您的评论和回答。

4

3 回答 3

2

您可以使用UNIQUE 约束来确保该列包含唯一值...这里,设置object_iduser_id已被设置为唯一...。

CREATE TABLE actions (
    id SERIAL PRIMARY KEY,
    object_id integer,
    user_id integer,
    active boolean default true,
    UNIQUE (object_id , user_id )

);

签出SQLFIDDLE

类似地,如果要设置 和as object_id,只需在 列表中添加列名即可。user_idactiveUNIQUEUNIQUE

CREATE TABLE actions (
    id SERIAL PRIMARY KEY,
    object_id integer,
    user_id integer,
    active boolean default true,
    UNIQUE (object_id , user_id,active )

);

签出SQLFIDDLE

于 2012-09-10T22:38:33.363 回答
2

鉴于您希望一次只限制一个条目的规范,请active尝试:

CREATE TABLE actions (
    id SERIAL PRIMARY KEY,
    object_id integer,
    user_id integer,
    active boolean default true,
    created_at timestamptz default now()
);

CREATE UNIQUE INDEX actions_unique_active_y ON actions(object_id,user_id) WHERE (active = 't');

这是一个部分唯一索引,一个 PostgreSQL 特定的功能 - 请参阅部分索引。它限制集合,使得只有一个(object_id,user_id)元组可能存在 where activeis true

正如您在评论中进一步解释的那样,这严格回答了您的问题,但我认为Wildplasser 的回答描述了更正确的选择和最佳方法。

于 2012-09-11T08:20:00.663 回答
1

原来的:

CREATE TABLE actions (
    id SERIAL PRIMARY KEY,
    object_id integer,
    user_id integer,
    active boolean default true
);

我的版本:

CREATE TABLE actions (
    object_id integer NOT NULL REFERENCES objects (id),
    user_id integer NOT NULL REFERENCES users(id),
    PRIMARY KEY (user_id, object_id)
);

有什么区别:

  • 省略了代理键。它没用,它不强制任何约束,也没有人会引用它
  • 添加了一个(复合)主键,它恰好是逻辑
  • 将这两个字段更改为 NOT NULL,并将它们设为外键(用户或对象表中存在的行是什么意思?
  • 删除了布尔标志。不存在的 {user_id,object_id} 元组与确实存在但其“活动”标志设置为 false 的元组之间的语义差异是什么?当你只需要两个状态时,为什么要创建三个状态?
于 2012-09-10T22:57:18.353 回答