You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Doctrine wrapInTransaction未持久化实体触发Duplicate entry异常

问题现象
  • 运行环境:PHP 8.1 + Doctrine ORM v2.12.2 + MySQL 5.7.38
  • 业务逻辑:setValue方法内调用wrapInTransaction持久化AccountSettingModelImpl实体,单步调试确认实体字段全部填充正确,但事务执行后直接查库找不到对应记录
  • 异常表现:setValue被多次调用写入配置时,fetchSettingByKey查不到对应记录抛出SettingNotFoundException进入catch分支,Doctrine尝试新建同主键实体时触发Duplicate entry '1-rl.enabled' for key 'PRIMARY'主键重复错误
  • 特殊规律:仅当整个PHP请求执行完成后,部分数据才会最终落库,wrapInTransaction执行完毕后无法立即查询到持久化结果
相关代码

实体定义

/**
 * Class SettingModel
 * @package kor\classes\models
 * @Entity @Table(name="korAccountSettings")
 */
class AccountSettingModelImpl extends AbstractBaseModel implements AccountSettingModel
{
    /**
     * @Id
     * @ManyToOne(targetEntity="AccountModelImpl")
     * @JoinColumn(name="asAccountId", referencedColumnName="aId")
     * @var AccountModelImpl
     */
    private AccountModel $account;

    /**
     * @Id
     * @Column(name="asKey", type="text")
     * @var string
     */
    private string $key;

    /**
     * @Column(name="asVal", type="text", nullable=true)
     * @var mixed
     */
    private ?string $val;

    /**
     * @Column(name="asUpdated", type="datetime_immutable")
     * @var DateTime
     */
    private DateTimeInterface $updated;
}

setValue方法实现

public function setValue(string $sKey, $sVal)
{
    Application::getEntityManager()->wrapInTransaction(function (EntityManager $entityManager)
    use ($sKey, $sVal) {
        $accountModel = $this->getAccountModel();
        try {
            $settingModel = AccountSettingHelper::fetchSettingByKey($sKey, $accountModel);
            $settingModel->setVal($sVal);
            if(is_null($sVal) && $entityManager->contains($settingModel))
                $entityManager->remove($settingModel);
        } catch (SettingNotFoundException $e) {
            $settingModel = Application::newEntity(AccountSettingModel::class);
            $settingModel->setAccount($accountModel);
            $settingModel->setKey($sKey);
            $settingModel->setVal($sVal);
            $entityManager->persist($settingModel);
        }
    });
    return $sVal;
}

异常堆栈

An exception occurred while executing a query: Duplicate entry '1-rl.enabled' for key 'PRIMARY'
#0 OR\vendor\doctrine\dbal\src\Connection.php(1814): Doctrine\DBAL\Driver\API\MySQL\ExceptionConverter->convert() 
#1 OR\vendor\doctrine\dbal\src\Connection.php(1749): Doctrine\DBAL\Connection->handleDriverException() 
#2 OR\vendor\doctrine\dbal\src\Statement.php(184): Doctrine\DBAL\Connection->convertExceptionDuringQuery() 
#3 OR\vendor\doctrine\dbal\src\Statement.php(221): Doctrine\DBAL\Statement->execute() 
#4 OR\vendor\doctrine\orm\lib\Doctrine\ORM\Persisters\Entity\BasicEntityPersister.php(278): Doctrine\DBAL\Statement->executeStatement() 
#5 OR\vendor\doctrine\orm\lib\Doctrine\ORM\UnitOfWork.php(1129): Doctrine\ORM\Persisters\Entity\BasicEntityPersister->executeInserts() 
#6 OR\vendor\doctrine\orm\lib\Doctrine\ORM\UnitOfWork.php(426): Doctrine\ORM\UnitOfWork->executeInserts() 
#7 OR\vendor\doctrine\orm\lib\Doctrine\ORM\EntityManager.php(398): Doctrine\ORM\UnitOfWork->commit() 
#8 OR\vendor\doctrine\orm\lib\Doctrine\ORM\EntityManager.php(269): Doctrine\ORM\EntityManager->flush()
问题根因

不需要开启特殊配置,wrapInTransaction本身的逻辑就是闭包执行成功后自动调用flush()并提交事务,问题出在三个代码缺陷上:

  1. 主键字段类型定义非法:实体中作为复合主键之一的key字段映射类型为text,MySQL InnoDB引擎不支持TEXT/BLOB类型直接作为主键,必须指定前缀长度,实际建表时会生成带前缀长度的隐式主键,Doctrine生成查询、插入语句时不会自动适配这个前缀规则,导致主键匹配失败,既无法正确查询到已存在的记录,也无法准确追踪实体的持久化状态。
  2. 查询实例不统一:wrapInTransaction闭包已经传入了当前事务绑定的$entityManager实例,如果AccountSettingHelper::fetchSettingByKey内部是直接通过Application::getEntityManager()获取新的实例、或者直接写原生SQL走独立连接查询,就无法命中当前EntityManager的一级缓存,也读不到当前事务内未提交的变更。
  3. 时间字段无自动填充逻辑:updated字段标记为非空、类型为datetime_immutable,但代码中没有看到给该字段赋值的逻辑,即使其他字段填充正确,该字段为空也会导致SQL执行失败,部分场景下PHP请求结束时的隐式flush可能触发了字段的默认值填充,才会出现"请求结束才落库"的错觉。
修复方案
  • 修改主键字段定义:把asKey的列类型从text改为指定长度的string类型,对应MySQL的varchar,长度根据配置键的实际长度设置即可,比如128或255:
/**
 * @Id
 * @Column(name="asKey", type="string", length=128)
 * @var string
 */
private string $key;

修改后同步更新数据库表结构,将korAccountSettings表的asKey字段改为varchar(128),确保复合主键(asAccountId, asKey)定义合法。

  • 统一EntityManager实例:修改AccountSettingHelper::fetchSettingByKey方法,支持传入EntityManager参数,调用时传入闭包内的$entityManager实例,优先使用Repository的findOneBy方法按复合主键查询,命中一级缓存,避免跨实例查询导致的状态不一致。
  • 补全时间字段自动填充:给实体类添加生命周期回调,在实体创建、更新时自动给updated字段赋值,避免非空字段为空导致SQL执行失败。
  • 若需要在闭包内执行完持久化操作后立即让后续查询读到变更,可以在persist()/remove()操作后显式调用$entityManager->flush(),wrapInTransaction不会重复执行无变更的flush操作。

内容的提问来源于stack exchange,提问作者NueFox

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 09:03:34