我有一个表ContentAddressedFiles
,其中hash
,size
和extension
列的组合为UNIQUE
。我想创建一个存储过程,在调用时,将使用给定值将新记录插入到表中。如果已存在这些值的记录,我想返回该现有记录。这是我的方法:
CREATE OR REPLACE FUNCTION INIT_CAF( id_in_case_of_new UUID, _hash VARCHAR(255), _size INTEGER, _extension VARCHAR(255), _mimeType VARCHAR(255))
RETURNS "ContentAddressedFiles"
AS $$
DECLARE
caf "ContentAddressedFiles"%ROWTYPE;
BEGIN
INSERT INTO "ContentAddressedFiles" (id, hash, size, extension, "mimeType", "createdAt", "updatedAt")
VALUES( id_in_case_of_new, _hash, _size, _extension, _mimeType, NOW(), NOW() ) RETURNING * INTO caf;
RETURN caf;
EXCEPTION WHEN unique_violation THEN
SELECT * FROM "ContentAddressedFiles" INTO caf WHERE "hash" = _hash AND "size" = _size AND "extension" = _extension;
IF NOT FOUND THEN
RAISE EXCEPTION 'This should never happen.';
END IF;
RETURN caf;
END;
$$ LANGUAGE plpgsql;
但是,当我从并发事务中调用该过程时,我始终得到异常:
EXCEPTION: This should never happen.
这怎么可能?该程序似乎无法SELECT
之前失败INSERT
的原因(不是id
发生冲突,它只是<hash, size, extension>
的元组。
答案 0 :(得分:1)
评论回答了如何避免这个问题的问题;我将在这里解释为什么 PostgreSQL以观察到的方式运行。
原因是函数中的INSERT
和SELECT
语句看到数据库的不同快照(状态),因为事务以默认隔离级别运行READ COMMITTED
。在该隔离级别,每个语句都会获得一个新的数据库快照。
对观察到的行为的解释必须是并发事务删除或修改失败的INSERT
和后面的SELECT
语句之间的行,以便导致{的约束违反的行INSERT
运行时,{1}}不再存在。
有两种方法可以解决这个问题:
选择更高的隔离级别:然后两个语句都将看到数据库的相同快照,SELECT
将找到阻止INSERT
的行,即使它有在此期间被改变了。这没问题,只是意味着整个事务在拍摄快照时逻辑上发生。
使用CTE将两个语句作为单个语句运行,就像评论建议中给出的解决方案一样。然后他们也会看到相同的数据库快照。