Skip to content

Commit 7363052

Browse files
tonysmtaylorotwell
andauthored
[11.x] Reduce the number of queries with Cache::many and Cache::putMany methods in the database driver (#52209)
* Adds integration test for database store multiget * Tweaks the test * Tweaks the test and adds the many test with expired keys * Adds test for the putMany method * Tweaks the test * Reduce the number of database calls in the many methods of the database store cache driver * Fix tests * Make the get and put use the many and putMany methods so the logic is the same * Adds a test for fetching many with associative arrays * Tweaks types and fixes docblocks on non-interface methods * formatting --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent ee1166c commit 7363052

File tree

3 files changed

+187
-36
lines changed

3 files changed

+187
-36
lines changed

src/Illuminate/Cache/DatabaseStore.php

Lines changed: 95 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
use Illuminate\Database\PostgresConnection;
1010
use Illuminate\Database\QueryException;
1111
use Illuminate\Database\SqlServerConnection;
12+
use Illuminate\Support\Arr;
1213
use Illuminate\Support\InteractsWithTime;
1314
use Illuminate\Support\Str;
1415

1516
class DatabaseStore implements LockProvider, Store
1617
{
17-
use InteractsWithTime, RetrievesMultipleKeys;
18+
use InteractsWithTime;
1819

1920
/**
2021
* The database connection instance.
@@ -98,29 +99,56 @@ public function __construct(ConnectionInterface $connection,
9899
*/
99100
public function get($key)
100101
{
101-
$prefixed = $this->prefix.$key;
102-
103-
$cache = $this->table()->where('key', '=', $prefixed)->first();
102+
return $this->many([$key])[$key];
103+
}
104104

105-
// If we have a cache record we will check the expiration time against current
106-
// time on the system and see if the record has expired. If it has, we will
107-
// remove the records from the database table so it isn't returned again.
108-
if (is_null($cache)) {
109-
return;
105+
/**
106+
* Retrieve multiple items from the cache by key.
107+
*
108+
* Items not found in the cache will have a null value.
109+
*
110+
* @return array
111+
*/
112+
public function many(array $keys)
113+
{
114+
if (count($keys) === 0) {
115+
return [];
110116
}
111117

112-
$cache = is_array($cache) ? (object) $cache : $cache;
118+
$results = array_fill_keys($keys, null);
119+
120+
// First we will retrieve all of the items from the cache using their keys and
121+
// the prefix value. Then we will need to iterate through each of the items
122+
// and convert them to an object when they are currently in array format.
123+
$values = $this->table()
124+
->whereIn('key', array_map(function ($key) {
125+
return $this->prefix.$key;
126+
}, $keys))
127+
->get()
128+
->map(function ($value) {
129+
return is_array($value) ? (object) $value : $value;
130+
});
131+
132+
$currentTime = $this->currentTime();
113133

114134
// If this cache expiration date is past the current time, we will remove this
115135
// item from the cache. Then we will return a null value since the cache is
116136
// expired. We will use "Carbon" to make this comparison with the column.
117-
if ($this->currentTime() >= $cache->expiration) {
118-
$this->forgetIfExpired($key);
137+
[$values, $expired] = $values->partition(function ($cache) use ($currentTime) {
138+
return $cache->expiration > $currentTime;
139+
});
119140

120-
return;
141+
if ($expired->isNotEmpty()) {
142+
$this->forgetManyIfExpired($expired->pluck('key')->all(), prefixed: true);
121143
}
122144

123-
return $this->unserialize($cache->value);
145+
return Arr::map($results, function ($value, $key) use ($values) {
146+
if ($cache = $values->firstWhere('key', $this->prefix.$key)) {
147+
return $this->unserialize($cache->value);
148+
}
149+
150+
return $value;
151+
});
124152
}
125153

126154
/**
@@ -133,11 +161,30 @@ public function get($key)
133161
*/
134162
public function put($key, $value, $seconds)
135163
{
136-
$key = $this->prefix.$key;
137-
$value = $this->serialize($value);
164+
return $this->putMany([$key => $value], $seconds);
165+
}
166+
167+
/**
168+
* Store multiple items in the cache for a given number of seconds.
169+
*
170+
* @param int $seconds
171+
* @return bool
172+
*/
173+
public function putMany(array $values, $seconds)
174+
{
175+
$serializedValues = [];
176+
138177
$expiration = $this->getTime() + $seconds;
139178

140-
return $this->table()->upsert(compact('key', 'value', 'expiration'), 'key') > 0;
179+
foreach ($values as $key => $value) {
180+
$serializedValues[] = [
181+
'key' => $this->prefix.$key,
182+
'value' => $this->serialize($value),
183+
'expiration' => $expiration,
184+
];
185+
}
186+
187+
return $this->table()->upsert($serializedValues, 'key') > 0;
141188
}
142189

143190
/**
@@ -309,9 +356,7 @@ public function restoreLock($name, $owner)
309356
*/
310357
public function forget($key)
311358
{
312-
$this->table()->where('key', '=', $this->prefix.$key)->delete();
313-
314-
return true;
359+
return $this->forgetMany([$key]);
315360
}
316361

317362
/**
@@ -321,9 +366,38 @@ public function forget($key)
321366
* @return bool
322367
*/
323368
public function forgetIfExpired($key)
369+
{
370+
return $this->forgetManyIfExpired([$key]);
371+
}
372+
373+
/**
374+
* Remove all items from the cache.
375+
*
376+
* @param array $keys
377+
* @return bool
378+
*/
379+
protected function forgetMany(array $keys)
380+
{
381+
$this->table()->whereIn('key', array_map(function ($key) {
382+
return $this->prefix.$key;
383+
}, $keys))->delete();
384+
385+
return true;
386+
}
387+
388+
/**
389+
* Remove all expired items from the given set from the cache.
390+
*
391+
* @param array $keys
392+
* @param bool $prefixed
393+
* @return bool
394+
*/
395+
protected function forgetManyIfExpired(array $keys, bool $prefixed = false)
324396
{
325397
$this->table()
326-
->where('key', '=', $this->prefix.$key)
398+
->whereIn('key', $prefixed ? $keys : array_map(function ($key) {
399+
return $this->prefix.$key;
400+
}, $keys))
327401
->where('expiration', '<=', $this->getTime())
328402
->delete();
329403

tests/Cache/CacheDatabaseStoreTest.php

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,26 @@ public function testNullIsReturnedWhenItemNotFound()
2222
$store = $this->getStore();
2323
$table = m::mock(stdClass::class);
2424
$store->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($table);
25-
$table->shouldReceive('where')->once()->with('key', '=', 'prefixfoo')->andReturn($table);
26-
$table->shouldReceive('first')->once()->andReturn(null);
25+
$table->shouldReceive('whereIn')->once()->with('key', ['prefixfoo'])->andReturn($table);
26+
$table->shouldReceive('get')->once()->andReturn(collect([]));
2727

2828
$this->assertNull($store->get('foo'));
2929
}
3030

3131
public function testNullIsReturnedAndItemDeletedWhenItemIsExpired()
3232
{
3333
$store = $this->getMockBuilder(DatabaseStore::class)->onlyMethods(['forgetIfExpired'])->setConstructorArgs($this->getMocks())->getMock();
34-
$table = m::mock(stdClass::class);
35-
$store->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($table);
36-
$table->shouldReceive('where')->once()->with('key', '=', 'prefixfoo')->andReturn($table);
37-
$table->shouldReceive('first')->once()->andReturn((object) ['expiration' => 1]);
38-
$store->expects($this->once())->method('forgetIfExpired')->with($this->equalTo('foo'))->willReturn(null);
34+
35+
$getQuery = m::mock(stdClass::class);
36+
$getQuery->shouldReceive('whereIn')->once()->with('key', ['prefixfoo'])->andReturn($getQuery);
37+
$getQuery->shouldReceive('get')->once()->andReturn(collect([(object) ['key' => 'prefixfoo', 'expiration' => 1]]));
38+
39+
$deleteQuery = m::mock(stdClass::class);
40+
$deleteQuery->shouldReceive('whereIn')->once()->with('key', ['prefixfoo'])->andReturn($deleteQuery);
41+
$deleteQuery->shouldReceive('where')->once()->with('expiration', '<=', m::any())->andReturn($deleteQuery);
42+
$deleteQuery->shouldReceive('delete')->once()->andReturnNull();
43+
44+
$store->getConnection()->shouldReceive('table')->twice()->with('table')->andReturn($getQuery, $deleteQuery);
3945

4046
$this->assertNull($store->get('foo'));
4147
}
@@ -45,8 +51,8 @@ public function testDecryptedValueIsReturnedWhenItemIsValid()
4551
$store = $this->getStore();
4652
$table = m::mock(stdClass::class);
4753
$store->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($table);
48-
$table->shouldReceive('where')->once()->with('key', '=', 'prefixfoo')->andReturn($table);
49-
$table->shouldReceive('first')->once()->andReturn((object) ['value' => serialize('bar'), 'expiration' => 999999999999999]);
54+
$table->shouldReceive('whereIn')->once()->with('key', ['prefixfoo'])->andReturn($table);
55+
$table->shouldReceive('get')->once()->andReturn(collect([(object) ['key' => 'prefixfoo', 'value' => serialize('bar'), 'expiration' => 999999999999999]]));
5056

5157
$this->assertSame('bar', $store->get('foo'));
5258
}
@@ -56,8 +62,8 @@ public function testValueIsReturnedOnPostgres()
5662
$store = $this->getPostgresStore();
5763
$table = m::mock(stdClass::class);
5864
$store->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($table);
59-
$table->shouldReceive('where')->once()->with('key', '=', 'prefixfoo')->andReturn($table);
60-
$table->shouldReceive('first')->once()->andReturn((object) ['value' => base64_encode(serialize('bar')), 'expiration' => 999999999999999]);
65+
$table->shouldReceive('whereIn')->once()->with('key', ['prefixfoo'])->andReturn($table);
66+
$table->shouldReceive('get')->once()->andReturn(collect([(object) ['key' => 'prefixfoo', 'value' => base64_encode(serialize('bar')), 'expiration' => 999999999999999]]));
6167

6268
$this->assertSame('bar', $store->get('foo'));
6369
}
@@ -68,7 +74,7 @@ public function testValueIsUpserted()
6874
$table = m::mock(stdClass::class);
6975
$store->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($table);
7076
$store->expects($this->once())->method('getTime')->willReturn(1);
71-
$table->shouldReceive('upsert')->once()->with(['key' => 'prefixfoo', 'value' => serialize('bar'), 'expiration' => 61], 'key')->andReturnTrue();
77+
$table->shouldReceive('upsert')->once()->with([['key' => 'prefixfoo', 'value' => serialize('bar'), 'expiration' => 61]], 'key')->andReturnTrue();
7278

7379
$result = $store->put('foo', 'bar', 60);
7480
$this->assertTrue($result);
@@ -80,7 +86,7 @@ public function testValueIsUpsertedOnPostgres()
8086
$table = m::mock(stdClass::class);
8187
$store->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($table);
8288
$store->expects($this->once())->method('getTime')->willReturn(1);
83-
$table->shouldReceive('upsert')->once()->with(['key' => 'prefixfoo', 'value' => base64_encode(serialize("\0")), 'expiration' => 61], 'key')->andReturn(1);
89+
$table->shouldReceive('upsert')->once()->with([['key' => 'prefixfoo', 'value' => base64_encode(serialize("\0")), 'expiration' => 61]], 'key')->andReturn(1);
8490

8591
$result = $store->put('foo', "\0", 60);
8692
$this->assertTrue($result);
@@ -99,7 +105,7 @@ public function testItemsMayBeRemovedFromCache()
99105
$store = $this->getStore();
100106
$table = m::mock(stdClass::class);
101107
$store->getConnection()->shouldReceive('table')->once()->with('table')->andReturn($table);
102-
$table->shouldReceive('where')->once()->with('key', '=', 'prefixfoo')->andReturn($table);
108+
$table->shouldReceive('whereIn')->once()->with('key', ['prefixfoo'])->andReturn($table);
103109
$table->shouldReceive('delete')->once();
104110

105111
$store->forget('foo');

tests/Integration/Database/DatabaseCacheStoreTest.php

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,77 @@ public function testForgetIfExpiredOperationShouldNotDeleteUnExpired()
166166
$this->assertDatabaseHas($this->getCacheTableName(), ['key' => $this->withCachePrefix('foo')]);
167167
}
168168

169+
public function testMany()
170+
{
171+
$this->insertToCacheTable('first', 'a', 60);
172+
$this->insertToCacheTable('second', 'b', 60);
173+
174+
$store = $this->getStore();
175+
176+
$this->assertEquals([
177+
'first' => 'a',
178+
'second' => 'b',
179+
'third' => null,
180+
], $store->get(['first', 'second', 'third']));
181+
182+
$this->assertEquals([
183+
'first' => 'a',
184+
'second' => 'b',
185+
'third' => null,
186+
], $store->many(['first', 'second', 'third']));
187+
}
188+
189+
public function testManyWithExpiredKeys()
190+
{
191+
$this->insertToCacheTable('first', 'a', 0);
192+
$this->insertToCacheTable('second', 'b', 60);
193+
194+
$this->assertEquals([
195+
'first' => null,
196+
'second' => 'b',
197+
'third' => null,
198+
], $this->getStore()->many(['first', 'second', 'third']));
199+
200+
$this->assertDatabaseMissing($this->getCacheTableName(), ['key' => $this->withCachePrefix('first')]);
201+
}
202+
203+
public function testManyAsAssociativeArray()
204+
{
205+
$this->insertToCacheTable('first', 'cached', 60);
206+
207+
$result = $this->getStore()->many([
208+
'first' => 'aa',
209+
'second' => 'bb',
210+
'third',
211+
]);
212+
213+
$this->assertEquals([
214+
'first' => 'cached',
215+
'second' => 'bb',
216+
'third' => null,
217+
], $result);
218+
}
219+
220+
public function testPutMany()
221+
{
222+
$store = $this->getStore();
223+
224+
$store->putMany($data = [
225+
'first' => 'a',
226+
'second' => 'b',
227+
], 60);
228+
229+
$this->assertEquals($data, $store->many(['first', 'second']));
230+
$this->assertDatabaseHas($this->getCacheTableName(), [
231+
'key' => $this->withCachePrefix('first'),
232+
'value' => serialize('a'),
233+
]);
234+
$this->assertDatabaseHas($this->getCacheTableName(), [
235+
'key' => $this->withCachePrefix('second'),
236+
'value' => serialize('b'),
237+
]);
238+
}
239+
169240
public function testResolvingSQLiteConnectionDoesNotThrowExceptions()
170241
{
171242
$originalConfiguration = config('database');
@@ -203,7 +274,7 @@ protected function insertToCacheTable(string $key, $value, $ttl = 60)
203274
->insert(
204275
[
205276
'key' => $this->withCachePrefix($key),
206-
'value' => $value,
277+
'value' => serialize($value),
207278
'expiration' => Carbon::now()->addSeconds($ttl)->getTimestamp(),
208279
]
209280
);

0 commit comments

Comments
 (0)