假设我有一个模型文档。
Document
document_id -> int
client_id -> int
我希望文档 ID 根据客户端 ID 自动递增,以避免竞争条件。我对 document_id、client_id 有一个独特的约束。
client_id document_id
1 1
1 2
2 1
1 3
方法 1:现在,我在 before_create 回调上设置文档 id,但它会导致竞争条件,并在同一客户端同时收到多个文档时设置相同的值。
创建之前:设置标识符 def 设置标识符 self.document_id = client.documents.maximum(:document_id).next 结束
同时创建 2 个文档时,会引发以下错误。
ActiveRecord::RecordNotUnique PG::UniqueViolation:错误:重复的键值违反了唯一约束
方法 2:尝试在 postgresql 上使用数据库触发器。效果很好。但唯一的问题是,更新的值最初并不反映在分配的变量上。我需要对变量进行强制重新加载。
数据库功能:
CREATE OR REPLACE FUNCTION set_document_identifier()
RETURNS TRIGGER AS $$
DECLARE
max_identifier INTEGER;
BEGIN
SELECT MAX(document_id) INTO max_identifier
FROM documents
WHERE client_id = NEW.client_id;
IF max_identifier IS NULL THEN
max_identifier := 1;
END IF;
NEW.document_id := max_identifier;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
数据库触发器:
CREATE TRIGGER before_insert_trigger_documents
BEFORE INSERT ON documents
FOR EACH ROW
EXECUTE FUNCTION set_document_identifier();
导轨:
doc = Document.create!(client_id: 1)
doc.document_id -> nil
doc.reload.document_id -> 1
rails 是否提供任何其他可能的 OTB 解决方案或可以在触发器上完成的解决方法,以便在事务提交之前设置该值?
显然,您不能为每个
client_id
提供唯一的递增值,并且还不能让多个用户同时生成自己的下一个值。宇宙就是这样运作的。
处理此问题的最简单方法是(正如您所发现的)在数据库中有一个完成此操作的点。
您还应该 (a) 适当锁定以防止并发插入,或者 (b) 预期重复的键错误并处理它们。
document_id
设置为 1
。下一次插入相同的 client_id
时只会发现 1
,这使得它成为 max()
,并且该插入也将使用相同的 1
值。你会一直得到1
:演示。如果您添加 +1
,它将实现 工作:demo2。
SELECT MAX(document_id)+1 INTO max_identifier
client_id
插入多个文档时,它们都会找到并使用相同的 max()
,从而导致两个具有相同 id 的不同文档。如果您使用生成的、基于种子的序列,就像我提到的类似线程中那样,它也适用于并发插入:demo3
create function seeded_sequence_nextval(seed text,
owner_table regclass default null,
owner_column text default 'ctid')
returns int as $f$
declare sequence_name text:=concat_ws('__','seeded_sequence',
owner_table,
owner_column,
seed);
begin execute format('create sequence if not exists %I owned by %s;',
sequence_name,
case when owner_table is null then 'none'
else format('%s.%I',owner_table,owner_column)
end);
return nextval(format('%I',sequence_name));
end $f$ language plpgsql;
CREATE OR REPLACE FUNCTION set_document_identifier()
RETURNS TRIGGER AS $f$
BEGIN
SELECT seeded_sequence_nextval(NEW.client_id::text,'documents','document_id')
INTO NEW.document_id;
RETURN NEW;
END;
$f$ LANGUAGE plpgsql;
CREATE TRIGGER before_insert_trigger_documents BEFORE INSERT ON documents
FOR EACH ROW EXECUTE FUNCTION set_document_identifier();