
在 Symfony/Api Platform 功能测试中,使用 RefreshDatabaseTrait 时 Doctrine Repository 缓存导致 count() 返回过期数据,根本原因在于 EntityManager 一级缓存未同步事务内变更,需主动清除或重建上下文。
在 symfony/api platform 功能测试中,使用 `refreshdatabasetrait` 时 doctrine repository 缓存导致 `count()` 返回过期数据,根本原因在于 entitymanager 一级缓存未同步事务内变更,需主动清除或重建上下文。
在基于 Api Platform 的功能测试中,RefreshDatabaseTrait(来自 Hautelook/Alice)通过事务包装(transactional wrapping)实现测试隔离:每个测试运行在独立数据库事务中,执行完毕后自动回滚。这一机制虽保证了数据洁净性,却也带来一个易被忽视的陷阱——Doctrine EntityManager 的一级缓存(Unit of Work)不会因外部 HTTP 请求触发的数据库变更而自动失效。
你的测试代码中,DELETE /employees/{uuid} 是通过 HttpClient 发起的 独立 HTTP 请求,该请求由另一个内核实例(即另一个 Kernel 实例)处理,并在其专属的 EntityManager 中完成实体删除与 flush。而当前测试方法内持有的 $employeeRepository,其底层 EntityManager 仍处于原始事务上下文中,缓存中仍保留着事务开始前的 Employee 实体快照(包括已加载的集合状态)。因此调用 $employeeRepository->count([]) 时,Doctrine 可能直接返回缓存结果(如 10),而非重新查询数据库。
✅ 正确解决方案(推荐三种方式)
方案一:手动清空 EntityManager(最轻量、最常用)
// 在 Assert 阶段,刷新 EntityManager 状态 $em = $employeeRepository->getEntityManager(); // 或 $employeeRepository->getEm()(若自定义了此方法) $em->clear(); // 清除一级缓存中所有托管实体 static::assertEquals(9, $employeeRepository->count([]));
⚠️ 注意:
clear()会清空整个 UoW,适合简单断言;若需保留部分实体,可改用detach($entity)或refresh($entity)。
方案二:强制重新获取 Repository(语义清晰,推荐用于复杂断言)
// 重新从容器获取全新 Repository 实例(连带新的 EntityManager 上下文) $employeeRepository = static::getContainer()->get(EmployeeRepository::class); // 此时 count 将发起真实 SQL 查询 static::assertEquals(9, $employeeRepository->count([]));
该方式无需依赖 clear(),天然规避缓存干扰,且符合“测试即黑盒”的设计哲学——每次断言都基于最新状态。
方案三:禁用查询缓存(全局配置,适用于高频调试)
在 phpunit.xml.dist 的测试环境配置中添加:
<php><env name="APP_ENV" value="test"></env><env name="DOCTRINE__ORM__DEFAULT__ENTITY_MANAGER__QUERY_CACHE_DRIVER" value="null"></env></php>
或在测试 setUp() 中临时覆盖:
$this->getContainer()->get('doctrine')->getManager()->getConfiguration()
->setResultCacheImpl(null);
? 补充说明:为什么不是“嵌套事务”问题?
RefreshDatabaseTrait 并不使用嵌套事务(MySQL 不原生支持 SAVEPOINT 回滚至指定点的完整嵌套语义),而是采用 外层事务 + 内部子事务模拟(通过 beginTransaction()/rollback() 控制)。HTTP 请求触发的是完全独立的请求生命周期,其数据库操作与测试主线程无事务共享,因此问题本质是 跨上下文缓存不一致,而非事务隔离级别缺陷。
✅ 最佳实践建议
- ✅ 始终在 HTTP 操作后 重新获取 Repository 或显式清空 EntityManager;
- ✅ 避免在测试中复用跨请求生命周期的 Repository 实例;
- ✅ 对关键断言(如
count,findOneBy,exists)优先使用static::getContainer()->get(...)重建服务; - ❌ 不要依赖
RefreshDatabaseTrait自动同步 ORM 状态——它只管数据库层面,不管内存缓存。
通过以上任一方式,即可彻底解决 count() 返回陈旧数据的问题,让功能测试真正反映 API 的实际行为。










