PHP SilverStripe ORM:重复键值违反了 DataObject 写入的唯一约束

PHP SilverStripe ORM: Duplicate key value violates unique constraint for DataObject write

我的网站上有一个功能,可以非常快速地将一堆值保存为相同的 DataObject 类型。大多数时候没问题,但偶尔会出错

ERROR: duplicate key value violates unique constraint ...

阅读我看到的文档:

SilverStripe does not use the database's built-in auto-numbering system. Instead, it will generate a new ID by adding 1 to the current maximum ID

之前查看代码,它看起来像是从主键中检索最大数量,插入具有该 ID 的记录,然后设置 DataObject 的值并再次写入。在我的负载平衡环境中,当发送这些多个条目时,我相信插入是使用相同的主键发生的,因此出现错误。

据我所知,这是一个我无法解决的问题。从其他 questions 和 doco 我无法设置复合主键。我唯一能想到的是 运行 一个自定义 sql 的创建,它确实使用了数据库的内置自动编号系统。

是否有更好的方法来处理此错误或设置复合主键的方法?

编辑

完整的错误是

Query failed: ERROR: duplicate key value violates unique constraint 'TABLE_pkey'
DETAIL: Key ('ID')=(136) already exists.

和语句:

INSERT INTO "TABLE" ("ClassName", "Name", "MemberID", "OtherTabeID", "Value", "LastEdited", "Created", "ID") VALUES (, , , , , , , ),Array) 

我读到这篇文章是因为它是从先前确定的值中插入 ID,而不是依赖于 DB 自动递增。对吗?

编辑 2

查看日志,看起来 INSERT 首先用 Created 字段完成,然后 select 语句完成以获得 ID:

SELECT last_value FROM "TABLENAME_ID_seq"

然后 UPDATE 完成并保存其他详细信息。

我觉得这可能是一种竞争条件,会导致保存到不正确的行,但不会导致我当前遇到的情况。理想情况下,任何 INSERT 都会有一个 returning "ID" 用于更新命令。

编辑 3

上述过程与我的堆栈跟踪相反,它显示插入内容不仅仅包含 Created:

pg_query_params(Resource id #154,INSERT INTO "TABLENAME" ("ClassName", "Name", "MemberID", "OTHERTABLEID", "Value", "LastEdited", "Created", "ID") VALUES (, , , , , , , ),Array) 
PostgreSQLConnector.php:200
PostgreSQLConnector->preparedQuery(INSERT INTO "TABLENAME" ("ClassName", "Name", "MemberID", "OTHERTABLEID", "Value", "LastEdited", "Created", "ID") VALUES (?, ?, ?, ?, ?, ?, ?, ?),Array,256) 
Database.php:143
SS_Database->{closure}(INSERT INTO "TABLENAME" ("ClassName", "Name", "MemberID", "OTHERTABLEID", "Value", "LastEdited", "Created", "ID") VALUES (?, ?, ?, ?, ?, ?, ?, ?)) 
Database.php:193
SS_Database->benchmarkQuery(INSERT INTO "TABLENAME" ("ClassName", "Name", "MemberID", "OTHERTABLEID", "Value", "LastEdited", "Created", "ID") VALUES (?, ?, ?, ?, ?, ?, ?, ?),Closure,Array) 
Database.php:146
SS_Database->preparedQuery(INSERT INTO "TABLENAME" ("ClassName", "Name", "MemberID", "OTHERTABLEID", "Value", "LastEdited", "Created", "ID") VALUES (?, ?, ?, ?, ?, ?, ?, ?),Array,256) 
DB.php:365
DB::prepared_query(INSERT INTO "TABLENAME" ("ClassName", "Name", "MemberID", "OTHERTABLEID", "Value", "LastEdited", "Created", "ID") VALUES (?, ?, ?, ?, ?, ?, ?, ?),Array) 
SQLExpression.php:121

编辑:记录的 SilverStripe 生成的密钥支持已损坏,并使用无法正常工作的标识符生成方法。但是,其中一位开发人员已确认这是一个文档错误,框架的真实行为不再是使用 max() 查询。所以问题不存在。

对于任何想知道为什么使用 max(...) 生成密钥是错误的人来说:这完全是并发不安全的。即使在子查询中。如果你这样做:

INSERT INTO my_table(id, ...)
VALUES
(
  (SELECT max(id) + 1 FROM my_table),
  ...
);

然后两个 SELECT 可以同时 运行。他们将得到相同的结果,然后两个 insert 将尝试插入相同的值。即使一个 insert 在另一个 select 运行 之前完成,如果它还没有 提交 ,另一个 select 赢了'看到新值。

只有先 LOCK TABLE 或在子查询中 SELECT ... FOR UPDATE 才安全。 SELECT ... FOR UPDATE 在这种情况下要慢得多。

因此,如果您可以修改 SilverStripe,请将其更改为至少在 SELECT max(...) 之前发送一个 LOCK TABLE mytable IN EXCLUSIVE MODE;,这样它既缓慢又笨拙,但也不会损坏。

或者,更好的是,将其修复为仅使用数据库序列。

如果业务确实需要无缝编号,请改用 UPDATE ... RETURNING ... 维护的计数器 table。 (如果您需要可移植性,则必须在同一事务中使用 SELECT ... FOR UPDATE 然后 UPDATE)。

更新:该框架在最新版本中不再使用该方法。

(删除了关于该方法是非建设性的脾气暴躁的咆哮)

文档中的注释已经过时(即使是 2007 年的 SilverStripe 2.1 也有正确的行为)并且文档描述的方法会导致竞争条件。

复杂的是 SilverStripe 使用多重 table 继承,SilverStripe 在这种情况下所做的是这样的:

  • 插入 SiteTree table
  • 获取生成的 ID
  • 使用相同的 ID
  • 插入页面 table(和其他 table)

它也可能对具有相同 ID 的 SiteTree table 进行后续更新写入。

遗憾的是,这不一定能帮助您解决问题,但至少可以解决问题的一个可能来源。

正如@CraigRinger 指出的那样,访问插入 ID 的方式是跨所有会话,而不是每个会话。 I've updated the PostgreSQL module to instead use currval() 这是基于会话的。

到目前为止,我还没有再次复制这个问题,但我并不完全相信这是问题的核心。如果它再次出错,我会更新它。