8
votes

With Doctrine and Symfony in my PHPUnit test method :

// 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"

Why my two methods to fetch the user #1 return different results ?

1
This seems like a caching issue to me. Can you verify this assumption by calling $em->clear before the first query to see if clearing the cache causes the right result to be returned? Furthermore, can you provide more details about your problem? Are you calling both queries in the same test case? Could you provide your full test case(s) and controller action?Arno Hilke
Have you tried to move ->select() before ->from()?Preciel

1 Answers

2
votes

diagnosis / explanation

I assume* you already created the User object you're editing via crawler before in that function and checked that it is there. This leads to it being a managed entity.

It is in the nature of data, to not sync itself magically with the database, but some automatism must be in place or some method executed to sync it.

The find() method will always try to use the cache (unless explicitly turned off, also see side note). The query builder won't, if you explicitly call getResult() (or one of its varieties), since you explicitly want a query to be executed. Executing a different query might lead to the cache not being hit, producing the current result. (it should update the first user object though ...) [updated, due to comment from Arno Hilke]

((( side note: Keeping objects in sync is hard. It's mainly about having consistency in the database, but all of ACID is wanted. Any process talking to the database should assume, that it only is working with the state at the moment of its first query, and is the only user of the database. Unless additional constraints must be met and inconsistent reads can occur, in which case isolation levels should be raised (See also: transactions or more precisely: isolation). So, automatically syncing is usually not wanted. Doctrine uses certain assumptions for performance gains (mainly: isolation / locking is optimistic). However, in your particular case, all of those things are of no actual concern... since you actually want a non-repeatable read. )))

(* otherwise, the behavior you're seeing would be really unexpected)

solution

One easy solution would be, to actively and explicitly sync the data from the database by either calling $em->refresh($user), or - before fetching the user again - to call $em->clear(), which will detach all entities (clearing the cache, which might have a noticable performance impact) and allowing you to call find again with the proper results being returned.

Please note, that detaching entities means, that any object previously returned from the entity manager should be discarded and fetched again (not via refresh).

alternate solution 1 - everything is requests

instead of checking the database, you could instead do a different request to a page that displays the user's name and checks that it has changed.

alternate solution 2 - using only one entity manager

using only one entity manager (that is: sharing the entity manager / database in the unit test with the server on the request) may be a reasonable solution, but it comes with its own set of problems. mainly omitted commits and flushes may avoid detection.

alternate solution 3 - using multiple entity managers

using one entity manager to set up the test, since the server is using a new entity manager to perform its work, you should theoretically - to do this actually properly - create yet another entity manager to check the server's behavior.

comment: the alternate solutions 1,2 and 3 would work with the highest isolation level, the initial solution probably wouldn't.