-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Do not fetch result cache twice in case of cache miss #4189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| <?php | ||
|
|
||
| namespace Doctrine\DBAL\Tests\Cache; | ||
|
|
||
| use Doctrine\Common\Cache\Cache; | ||
| use Doctrine\DBAL\Cache\CachingResult; | ||
| use Doctrine\DBAL\Driver\Result; | ||
| use PHPUnit\Framework\TestCase; | ||
|
|
||
| class CachingResultTest extends TestCase | ||
| { | ||
| /** @var string */ | ||
| private $cacheKey = 'cacheKey'; | ||
|
|
||
| /** @var string */ | ||
| private $realKey = 'realKey'; | ||
|
|
||
| /** @var int */ | ||
| private $lifetime = 3600; | ||
|
|
||
| /** @var array<string, mixed> */ | ||
| private $resultData = ['id' => 123, 'field' => 'value']; | ||
|
|
||
| /** @var array<string, mixed> */ | ||
| private $cachedData; | ||
|
|
||
| /** @var Cache */ | ||
| private $cache; | ||
|
|
||
| /** @var Result */ | ||
| private $result; | ||
|
|
||
| protected function setUp(): void | ||
| { | ||
| $this->result = $this->createMock(Result::class); | ||
| $this->result->expects(self::exactly(2)) | ||
| ->method('fetchAssociative') | ||
| ->will($this->onConsecutiveCalls($this->resultData, false)); | ||
|
|
||
| $this->cache = $this->createMock(Cache::class); | ||
| $this->cache->expects(self::exactly(1)) | ||
| ->method('save') | ||
| ->willReturnCallback(function (string $id, $data, int $ttl): void { | ||
| $this->assertEquals($this->cacheKey, $id, 'The cache key should match the given one'); | ||
| $this->assertEquals($this->lifetime, $ttl, 'The cache key ttl should match the given one'); | ||
| $this->cachedData = $data; | ||
| }); | ||
| } | ||
|
|
||
| public function testShouldSaveResultToCache(): void | ||
| { | ||
| $cachingResult = new CachingResult( | ||
| $this->result, | ||
| $this->cache, | ||
| $this->cacheKey, | ||
| $this->realKey, | ||
| $this->lifetime, | ||
| ['otherRealKey' => 'resultValue'] | ||
| ); | ||
|
|
||
| do { | ||
| $row = $cachingResult->fetchAssociative(); | ||
| } while ($row !== false); | ||
|
|
||
| $this->assertContains( | ||
| $this->resultData, | ||
| $this->cachedData[$this->realKey], | ||
| 'CachingResult should cache data from the given result' | ||
| ); | ||
|
|
||
| $this->assertEquals( | ||
| 'resultValue', | ||
| $this->cachedData['otherRealKey'], | ||
| 'CachingResult should not change other keys from cache' | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -225,6 +225,64 @@ public function testFetchAllSavesCache(): void | |||||
| self::assertCount(1, $layerCache->fetch('testcachekey')); | ||||||
| } | ||||||
|
|
||||||
| public function testCacheQueriedOnlyOnceForCacheMiss(): void | ||||||
| { | ||||||
| $layerCache = $this->createMock(ArrayCache::class); | ||||||
| $layerCache->expects(self::once()) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd put this under the unit category since it meant to cover the logic of the unit that calls the fetch and it doesn't depend on the actual implementation of the cache.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It can't be a unit test as it covers the full use case. Right now, there are two invocations of In my PR it is executed in the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO it should be either a functional test the tests certain functionality (hence not mocking any dependencies) or a unit test that covers a given unit and mocks the dependencies. The purpose of such a hybrid test is unclear. If it fails, it's unclear whether the case is broken or a specific unit is because the test doesn't cover either of the two.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I may change it to
Functional testing performs tests of the whole library by sending input to its end-user endpoint and examining output (without checking internal structure). In our case, cache-object is just another type of the input. And we're checking whether
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's your take on "Don't mock what you don't own?"
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't get this question and how it is related to this topic. Let me explain one more time:
I'm mocking my own object, that implements Why $cache = new ArrayCache();
$cache->fetch('test_key');
self::assertEquals(1, $cache->getStats()[Cache::STATS_MISSES]);It will fail. The right value for $layerCache->expects(self::once())->method('fetch')I will have to write the following code self::assertEquals(2, $layerCache->getStats()[Cache::STATS_MISSES]);It's completely unclear why I'm using 2 here, while the whole idea of this patch is to remove the double fetch. And if they remove this "namespace version" feature in the TL;DR:
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh I see, you really need a mock since you are adding expectations to it.
It's more like a stub in fact, that's what got me confused. It's easier to use as a stub, but can't be used as a mock since you can't know which methods were called, and instead just get some distorted stats.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does interaction between the units of code (the cache and the underlying statement) need to be tested in a functional way? It's obviously a unit-level requirement, not a functional requirement. The contract you're talking about is a unit-level contract. A cache is a non-functional unit by definition since it's not meant to change the behavior of the system. It only changes the internal implementation details.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Any interaction is an "interaction between units of code". The question is where these units of code reside. Cache is not part of DBAL.
Cache DOES change the behavior of the system. The system is working differently depending on what the cache object returns. I see you don't understand (or don't want to understand) why this test is functional. I can delete it if you want. Or I can copy it to the "tests/Cache" directory.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Please treat others more kindly, especially when they take that much time to answer to you. Assuming either ignorance or malice like this will not help. As for your definition of behavior, it's debatable. To me, it would be more about direct inputs and outputs / the public interface of the system. Whether the system calls systems external systems to create the output should not be relevant in a functional test IMO. |
||||||
| ->method('fetch') | ||||||
| ->willReturn(false); | ||||||
|
|
||||||
| $layerCache->expects(self::once()) | ||||||
| ->method('save'); | ||||||
SenseException marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
|
||||||
| $result = $this->connection->executeQuery( | ||||||
| 'SELECT * FROM caching WHERE test_int > 500', | ||||||
| [], | ||||||
| [], | ||||||
| new QueryCacheProfile(0, 'testcachekey', $layerCache) | ||||||
| ); | ||||||
| $result->fetchAllAssociative(); | ||||||
| } | ||||||
|
|
||||||
| public function testDifferentQueriesMayBeSavedToOneCacheKey(): void | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test passes even without the fix. Is it really needed?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It shows how many queries may be stored in one cache key. This logic wasn't explicitly shown in other tests and you weren't aware of it, so I think it's better to leave this test. However, it might be taken out to another PR, as it doesn't directly relates to this one. |
||||||
| { | ||||||
| $layerCache = new ArrayCache(); | ||||||
|
|
||||||
| $this->executeTwoCachedQueries($layerCache); | ||||||
| self::assertCount(2, $this->sqlLogger->queries, 'Two queries executed'); | ||||||
| self::assertCount(2, $layerCache->fetch('testcachekey'), 'Both queries are saved to cache'); | ||||||
|
|
||||||
| $this->executeTwoCachedQueries($layerCache); | ||||||
| self::assertCount(2, $this->sqlLogger->queries, 'Consecutive queries are fetched from cache'); | ||||||
|
|
||||||
| $layerCache->delete('testcachekey'); | ||||||
| $this->executeTwoCachedQueries($layerCache); | ||||||
| self::assertCount( | ||||||
| 4, | ||||||
| $this->sqlLogger->queries, | ||||||
| 'Deleting one cache key leads to deleting cache for both queris' | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| private function executeTwoCachedQueries(ArrayCache $cache): void | ||||||
| { | ||||||
| $result = $this->connection->executeQuery( | ||||||
| 'SELECT * FROM caching WHERE test_int > 500', | ||||||
| [], | ||||||
| [], | ||||||
| new QueryCacheProfile(0, 'testcachekey', $cache) | ||||||
| ); | ||||||
| $result->fetchAllAssociative(); | ||||||
|
|
||||||
| $result = $this->connection->executeQuery( | ||||||
| 'SELECT * FROM caching WHERE test_int > 400', | ||||||
| [], | ||||||
| [], | ||||||
| new QueryCacheProfile(0, 'testcachekey', $cache) | ||||||
| ); | ||||||
| $result->fetchAllAssociative(); | ||||||
| } | ||||||
|
|
||||||
| public function testFetchColumn(): void | ||||||
| { | ||||||
| $query = $this->connection->getDatabasePlatform() | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does a test need flow control logic? Do we not know how many rows are expected to be fetched here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this particular test, we know that the 2nd
fetchAssociativeshould befalse. But that is not what is tested here. This block is a standard way to fetch all rows.I may use
Doctrine\DBAL\Driver\FetchUtils::fetchAllAssociativeinstead of this block if you like.