教义的find()和querybuilder()在PHPUnit测试中返回不同的结果

Gay*_*d.P 8 phpunit doctrine symfony

在我的PHPUnit测试方法中使用Doctrine和Symfony:

// Change username for user #1 (Sheriff Woody to Chuck Norris)
$form = $crawler->selectButton('Update')->form([
    'user[username]' => 'Chuck Norris',
]);
$client->submit($form);

// Find user #1
$user = $em->getRepository(User::class)->find(1);
dump($user); // Username = "Sheriff Woody"

$user = $em->createQueryBuilder()
        ->from(User::class, 'user')
        ->andWhere('user.id = :userId')
        ->setParameter('userId', 1)
        ->select('
            user
        ')
        ->getQuery()
        ->getOneOrNullResult()
    ;
dump($user); // Username = "Chuck Norris"
Run Code Online (Sandbox Code Playgroud)

为什么我的两种获取用户#1的方法返回不同的结果?

Jak*_*umi 2

诊断/解释

假设*您已经在该函数中创建了您正在通过爬虫编辑的用户对象,并检查它是否存在。这导致它成为一个托管实体。

数据的本质是不会与数据库神奇地同步,但必须有某种自动功能或执行某种方法来同步它。

find()方法将始终尝试使用缓存(除非明确关闭,另请参阅旁注)。如果您显式调用(或其变体之一),查询构建器不会getResult(),因为您明确希望执行查询。执行不同的查询可能会导致缓存未被命中,从而产生当前结果。(它应该更新第一个用户对象...)[已更新,由于 Arno Hilke 的评论]

(((旁注:保持对象同步很困难。这主要是为了在数据库中保持一致性,但所有的ACID都是需要的。任何与数据库对话的进程都应该假设,它只处理当前的状态)它的第一个查询,并且是数据库的唯一用户。除非必须满足额外的约束并且可能发生不一致的读取,在这种情况下应该提高隔离级别(另请参见:事务或更准确地说:隔离)。因此,自动同步是通常不需要。Doctrine 使用某些假设来提高性能(主要是:隔离/锁定是乐观的)。但是,在您的特定情况下,所有这些事情都没有实际意义......因为您实际上想要一个不可重复的读取。 )))

(*否则,您所看到的行为将是非常出乎意料的)

解决方案

一种简单的解决方案是,通过调用 主动且显式地同步数据库中的数据$em->refresh($user),或者在再次获取用户之前调用$em->clear(),这将分离所有实体(清除缓存,这可能会对性能产生显着影响)并且允许您再次调用find并返回正确的结果。

请注意,分离实体意味着之前从实体管理器返回的任何对象都应被丢弃并再次获取(而不是通过刷新)。

替代解决方案 1 - 一切都是请求

您可以不检查数据库,而是对显示用户名的页面执行不同的请求并检查它是否已更改。

替代解决方案 2 - 仅使用一个实体管理器

仅使用一个实体管理器(即:在单元测试中与请求的服务器共享实体管理器/数据库)可能是一种合理的解决方案,但它也带来了一系列问题。主要省略提交和刷新可能会避免检测。

替代解决方案 3 - 使用多个实体管理器

使用一个实体管理器来设置测试,因为服务器正在使用新的实体管理器来执行其工作,理论上您应该 - 为了实际上正确地执行此操作 - 创建另一个实体管理器来检查服务器的行为。

评论:替代解决方案 1,2 和 3 将在最高隔离级别下工作,而初始解决方案可能不会。