├── .gitignore ├── .travis.yml ├── README.md ├── composer.json ├── doc └── Pea.svg ├── phpunit.xml ├── src ├── Blueprint.php ├── Cache.php ├── Meta.php ├── Model.php ├── QueryBuilder.php ├── RedisCache.php ├── RedisMeta.php ├── SchemaFacade.php └── ServiceProvider.php └── test ├── CacheTest.php ├── MetaTest.php ├── ModelTest.php └── TestCase.php /.gitignore: -------------------------------------------------------------------------------- 1 | .phpcd 2 | vendor 3 | composer.lock 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.6 5 | - 7.0 6 | - hhvm 7 | 8 | env: 9 | global: 10 | - setup=basic 11 | 12 | matrix: 13 | include: 14 | - php: 5.6 15 | env: setup=stable 16 | 17 | sudo: false 18 | 19 | install: 20 | - if [[ $setup = 'basic' ]]; then travis_retry composer install --no-interaction --prefer-source; fi 21 | - if [[ $setup = 'stable' ]]; then travis_retry composer update --prefer-source --no-interaction --prefer-stable; fi 22 | - if [[ $setup = 'lowest' ]]; then travis_retry composer update --prefer-source --no-interaction --prefer-lowest --prefer-stable; fi 23 | 24 | script: vendor/bin/phpunit 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pea 2 | 3 | [![Join the chat at https://gitter.im/angejia/pea](https://badges.gitter.im/angejia/pea.svg)](https://gitter.im/angejia/pea?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![Build Status](https://travis-ci.org/angejia/pea.svg?branch=master)](https://travis-ci.org/angejia/pea) 5 | 6 | Laravel Eloquent 的缓存层。 7 | 8 | ## 特色 9 | 10 | - 行级缓存 11 | - 表级缓存 12 | - 自动过期 13 | 14 | 更多细节参考[wiki](../../wiki)。 15 | 16 | ## 安装 17 | 18 | ``` 19 | composer require angejia/pea:dev-master 20 | ``` 21 | 22 | ## 使用 23 | 24 | 在`config/app.php`中添加`Angejia\Pea\ServiceProvider`,然后使用`Angejia\Pea\Model`替换`Illuminate\Database\Eloquent\Model`。 最后在模型中设置`protected`属性`$needCache`为`true`即可开启缓存支持。 25 | 26 | ```php 27 | class UserModel extends \Angejia\Pea\Model 28 | { 29 | protected $needCache = true; 30 | } 31 | ``` 32 | 33 | 如果你有专门的 Redis 缓存实例,可以通过`config/database.php`指定。具体参见[wiki](../../wiki/Laravel-配置)。 34 | 35 | --- 36 | [安个家](http://www.angejia.com/)出品。 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angejia/pea", 3 | "description": "Eloquent Cache Layer", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "吕海涛", 8 | "email": "git@lvht.net" 9 | } 10 | ], 11 | "autoload": { 12 | "psr-4": { 13 | "Angejia\\Pea\\": "src" 14 | } 15 | }, 16 | "autoload-dev": { 17 | "psr-4": { 18 | "Angejia\\Pea\\": "test" 19 | } 20 | }, 21 | "require": { 22 | "illuminate/database": "^5.1", 23 | "illuminate/redis": "^5.1" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "^5.0", 27 | "mockery/mockery": "^0.9.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /doc/Pea.svg: -------------------------------------------------------------------------------- 1 | 2 |
:User
:User
:EloquentBuilder
:EloquentBuilder
findMany([1,2])
findMany([1,2])
get()
get()
getModels()
getModels()
:PeaQueryBuilder
:PeaQueryBuilder
isAweful()
isAweful()
isNormal()
isNormal()
getSimple()
getSimple()
buildCacheKeys()
buildCacheKeys()
parent::get()
parent::get()
buildCacheKeys()
<span>buildCacheKeys()</span>
buildRowCacheKey()
buildRowCacheKey()
:PeaMeta
:PeaMeta
:PeaCache
:PeaCache
find([1,2])
find([1,2])
[user1, user2]
[user1, user2]
find([1,2])
find([1,2])
return
return
whereIn('id', [1,2])
whereIn('id', [1,2])
get()
get()
return
return
getConnectionName()
getConnectionName()
return
return
hydrate()
hydrate()
return
return
get($keys)
get($keys)
return
return
set($keys)
set($keys)
return
return
return
return
prefix()
prefix()
-------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./test/ 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Blueprint.php: -------------------------------------------------------------------------------- 1 | toSql($connection, $grammar); 17 | if (!$statements) { 18 | throw new \RuntimeException('migration must be created with Blueprint'); 19 | } 20 | 21 | foreach ($statements as $statement) { 22 | $connection->statement($statement); 23 | } 24 | 25 | $db = $connection->getDatabaseName(); 26 | $table = $this->getTable(); 27 | $this->meta->flushAll($db, $table); 28 | } 29 | 30 | public function setMeta(Meta $meta) 31 | { 32 | $this->meta = $meta; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Cache.php: -------------------------------------------------------------------------------- 1 | 'value1', 13 | * 'key2' => 'value2', 14 | * ] 15 | */ 16 | function get($keys); 17 | 18 | /** 19 | * 批量设置缓存内容 20 | * 21 | * @param array $key_value 待缓存键值对 22 | */ 23 | function set($keyValue); 24 | 25 | /** 26 | * 清理缓存内容 27 | * 28 | * @param array $keys 缓存索引列表 29 | */ 30 | function del($keys); 31 | } 32 | -------------------------------------------------------------------------------- /src/Meta.php: -------------------------------------------------------------------------------- 1 | needCache && !self::$disableReadCache; 14 | } 15 | 16 | /** 17 | * 判断更新数据库的时候是否需要更新缓存 18 | */ 19 | public function needFlushCache() 20 | { 21 | return $this->needCache; 22 | } 23 | 24 | public function primaryKey() 25 | { 26 | return $this->primaryKey; 27 | } 28 | 29 | public function table() 30 | { 31 | return $this->table; 32 | } 33 | 34 | protected function newBaseQueryBuilder() 35 | { 36 | $conn = $this->getConnection(); 37 | 38 | $grammar = $conn->getQueryGrammar(); 39 | 40 | $queryBuilder = new QueryBuilder( 41 | $conn, $grammar, $conn->getPostProcessor()); 42 | $queryBuilder->setModel($this); 43 | 44 | return $queryBuilder; 45 | } 46 | 47 | public function newEloquentBuilder($query) 48 | { 49 | $builder = new Builder($query); 50 | 51 | $builder->macro('key', function (Builder $builder) { 52 | return $builder->getQuery()->key(); 53 | }); 54 | 55 | $builder->macro('flush', function (Builder $builder) { 56 | return $builder->getQuery()->flush(); 57 | }); 58 | 59 | return $builder; 60 | } 61 | 62 | /** 63 | * 关闭查询数据库的时候读取缓存的逻辑(更新数据库的时候还会更新缓存) 64 | */ 65 | public static function disableReadCache() 66 | { 67 | self::$disableReadCache = true; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/QueryBuilder.php: -------------------------------------------------------------------------------- 1 | model) { 19 | return false; 20 | } 21 | 22 | return $this->model->needCache(); 23 | } 24 | 25 | private function needFlushCache() 26 | { 27 | // TODO 如果没有设置 model,则认为不用处理缓存逻辑 28 | if (!$this->model) { 29 | return false; 30 | } 31 | 32 | return $this->model->needFlushCache(); 33 | } 34 | 35 | public function setModel(Model $model) 36 | { 37 | $this->model = $model; 38 | } 39 | 40 | /** 41 | * @return Cache 42 | */ 43 | protected function getCache() 44 | { 45 | return Container::getInstance()->make(Cache::class); 46 | } 47 | 48 | /** 49 | * @return Meta 50 | */ 51 | protected function getMeta() 52 | { 53 | return Container::getInstance()->make(Meta::class); 54 | } 55 | 56 | public function get($columns = ['*']) 57 | { 58 | if ($this->model) { 59 | $this->fireEvent('get'); 60 | } 61 | 62 | if (!$this->needCache()) 63 | { 64 | return parent::get($columns); 65 | } 66 | 67 | if (!$this->columns && self::hasRawColumn($columns)) { 68 | $this->columns = $columns; 69 | } 70 | 71 | if ($this->isAwful()) { 72 | return $this->getAwful(); 73 | } elseif ($this->isNormal()) { 74 | return $this->getAwful(); 75 | } else { 76 | return $this->getSimple(); 77 | } 78 | } 79 | 80 | private static function hasRawColumn($columns) 81 | { 82 | if (!$columns) { 83 | return false; 84 | } 85 | 86 | foreach ($columns as $column) { 87 | if ($column instanceof Expression) { 88 | return true; 89 | } 90 | } 91 | 92 | return false; 93 | } 94 | 95 | private function getAwful() 96 | { 97 | $key = $this->buildAwfulCacheKey(); 98 | $cache = $this->getCache(); 99 | $result = $cache->get([$key]); 100 | if (array_key_exists($key, $result)) { 101 | $this->fireEvent('hit.awful'); 102 | return $result[$key]; 103 | } 104 | 105 | $this->fireEvent('miss.awful'); 106 | 107 | $result = parent::get(); 108 | $cache->set([ 109 | $key => $result, 110 | ]); 111 | 112 | return $result; 113 | } 114 | 115 | private function buildAwfulCacheKey() 116 | { 117 | return $this->buildTableCacheKey($this->toSql(), $this->getBindings()); 118 | } 119 | 120 | /** 121 | * 判断当前查询是否未「复杂查询」,判断标准 122 | * 1. 含有 max, sum 等汇聚函数 123 | * 2. 包含 distinct 指令 124 | * 3. 包含分组 125 | * 4. 包含连表 126 | * 5. 包含联合 127 | * 6. 包含子查询 128 | * 7. 包含原生(raw)语句 129 | * 8. 包含排序 TODO 优化此类情形 130 | * 131 | * 复杂查询使用表级缓存,命中率较低 132 | */ 133 | private function isAwful() 134 | { 135 | if (self::hasRawColumn($this->columns)) { 136 | return true; 137 | } 138 | 139 | return $this->aggregate 140 | or $this->distinct 141 | or $this->groups 142 | or $this->joins 143 | or $this->orders 144 | or $this->unions 145 | or !$this->wheres 146 | or array_key_exists('Exists', $this->wheres) 147 | or array_key_exists('InSub', $this->wheres) 148 | or array_key_exists('NotExists', $this->wheres) 149 | or array_key_exists('NotInSub', $this->wheres) 150 | or array_key_exists('Sub', $this->wheres) 151 | or array_key_exists('raw', $this->wheres); 152 | } 153 | 154 | private function getNormal() 155 | { 156 | $primaryKeyName = $this->model->primaryKey(); 157 | // 查询主键列表 158 | $rows = $this->getAwful([$primaryKeyName]); 159 | $ids = array_map(function ($row) use($primaryKeyName) { 160 | return $row->$primaryKeyName; 161 | }, $rows); 162 | 163 | // 没查到结果则直接返回空数组 164 | if (!$ids) { 165 | return []; 166 | } 167 | 168 | // 根据主键查询结果 169 | $originWheres = $this->wheres; 170 | $originWhereBindings = $this->bindings['where']; 171 | $originLimit = $this->limit; 172 | $originOffset = $this->offset; 173 | 174 | $this->wheres = []; 175 | $this->bindings['where'] = []; 176 | $this->limit = null; 177 | $this->offset = null; 178 | $this->whereIn($primaryKeyName, $ids); 179 | $rows = $this->getSimple(); 180 | 181 | $this->wheres = $originWheres; 182 | $this->bindings['where'] = $originWhereBindings; 183 | $this->limit = $originLimit; 184 | $this->offset = $originOffset; 185 | 186 | return $rows; 187 | } 188 | 189 | /** 190 | * 判断当前查询是否未「普通查询」 191 | * 192 | * 普通查询需要转化成简单查询 193 | */ 194 | private function isNormal() 195 | { 196 | return !$this->isAwful() && !$this->isSimple(); 197 | } 198 | 199 | /** 200 | * 简单查询,只根据主键过滤结果集 201 | */ 202 | private function getSimple() 203 | { 204 | $primaryKeyName = $this->model->primaryKey(); 205 | $cacheKeys = $this->buildCacheKeys(); 206 | $keyId = array_flip($cacheKeys); 207 | 208 | $cache = $this->getCache(); 209 | $cachedRows = $cache->get(array_values($cacheKeys)); 210 | foreach ($cachedRows as $key => $row) { 211 | unset($cacheKeys[$keyId[$key]]); 212 | } 213 | 214 | // TODO 如何处理顺序 215 | $cachedRows = array_filter(array_values($cachedRows), function ($row) { 216 | return $row !== []; 217 | }); 218 | 219 | $missedIds = array_keys($cacheKeys); 220 | if (!$missedIds) { 221 | $this->fireEvent('hit.simple.1000'); 222 | return $cachedRows; 223 | } 224 | 225 | if (count($cachedRows) === 0) { 226 | $this->fireEvent('miss.simple'); 227 | } else { 228 | $cachedNum = count($cachedRows); 229 | $missedNum = count($missedIds); 230 | $percent = (int)($cachedNum / ($cachedNum + $missedNum) * 1000); 231 | $this->fireEvent('hit.simple.' . $percent); 232 | } 233 | 234 | $originWheres = $this->wheres; 235 | $originWhereBindings = $this->bindings['where']; 236 | $originColumns = $this->columns; 237 | $this->wheres = []; 238 | $this->bindings['where'] = []; 239 | $this->whereIn($primaryKeyName, $missedIds); 240 | $this->columns = null; 241 | 242 | $missedRows = array_fill_keys($missedIds, []); 243 | foreach (parent::get() as $row) { 244 | $missedRows[$row->$primaryKeyName] = $row; 245 | } 246 | 247 | $this->wheres = $originWheres; 248 | $this->bindings['where'] = $originWhereBindings; 249 | $this->columns = $originColumns; 250 | 251 | $toCachRows = []; 252 | $toCachIds = array_keys($missedRows); 253 | $toCachKeys = $this->buildRowCacheKey($toCachIds); 254 | foreach ($missedRows as $id => $row) { 255 | $toCachRows[$toCachKeys[$id]] = $row; 256 | } 257 | if ($toCachRows) { 258 | $cache->set($toCachRows); 259 | } 260 | $missedRows = array_filter(array_values($missedRows), function ($row) { 261 | return $row !== []; 262 | }); 263 | 264 | return array_merge($cachedRows, $missedRows); 265 | } 266 | 267 | private function buildCacheKeys() 268 | { 269 | $where = current($this->wheres); 270 | if ($where['type'] == 'In') { 271 | $ids = $where['values']; 272 | } else { 273 | $ids = [$where['value']]; 274 | } 275 | 276 | $cacheKeys = $this->buildRowCacheKey($ids); 277 | 278 | return $cacheKeys; 279 | } 280 | 281 | /** 282 | * 「简单查询」就是只根据主键过滤结果集的查询,有以下两种形式: 283 | * 1. select * from foo where id = 1; 284 | * 2. select * from foo where id in (1, 2, 3); 285 | */ 286 | private function isSimple() 287 | { 288 | if ($this->isAwful()) { 289 | return false; 290 | } 291 | 292 | if (!$this->wheres) { 293 | return false; 294 | } 295 | 296 | if (count($this->wheres) > 1) { 297 | return false; 298 | } 299 | 300 | $where = current($this->wheres); 301 | 302 | if ($where['type'] === 'Nested') { 303 | return false; 304 | } 305 | 306 | $id = $this->model->primaryKey(); 307 | $tableId = $this->model->table() . '.' . $this->model->primaryKey(); 308 | if (!in_array($where['column'], [$id, $tableId])) { 309 | return false; 310 | } 311 | 312 | if ($where['type'] === 'In') { 313 | return true; 314 | } 315 | 316 | if ($where['type'] === 'Basic') { 317 | if ($where['operator'] === '=') { 318 | return true; 319 | } 320 | } 321 | 322 | return false; 323 | } 324 | 325 | public function delete($id = null) 326 | { 327 | if ($this->needFlushCache()) { 328 | // 清空表级缓存 329 | $meta = $this->getMeta(); 330 | $meta->flush($this->db(), $this->model->table()); 331 | $this->flushAffectingRowCache(); 332 | } 333 | 334 | return parent::delete($id); 335 | } 336 | 337 | /** 338 | * 查找受影响的 ID,清空相关行级缓存 339 | */ 340 | private function flushAffectingRowCache() 341 | { 342 | $keyName = $this->model->primaryKey(); 343 | $toDeleteRows = parent::get(); 344 | $ids = []; 345 | foreach ($toDeleteRows as $row) { 346 | $ids[] = $row->$keyName; 347 | } 348 | if ($ids) { 349 | $cacheKeys = $this->buildRowCacheKey($ids); 350 | $cache = $this->getCache(); 351 | $cache->del(array_values($cacheKeys)); 352 | } 353 | } 354 | 355 | public function update(array $values) 356 | { 357 | if ($this->needFlushCache()) { 358 | // 清空表级缓存 359 | $meta = $this->getMeta(); 360 | $meta->flush($this->db(), $this->model->table()); 361 | 362 | $this->flushAffectingRowCache(); 363 | } 364 | return parent::update($values); 365 | } 366 | 367 | public function insert(array $values) 368 | { 369 | if ($this->needFlushCache()) { 370 | // 清空表级缓存 371 | $meta = $this->getMeta(); 372 | $meta->flush($this->db(), $this->model->table()); 373 | 374 | if (! is_array(reset($values))) { 375 | $values = [$values]; 376 | } 377 | $toClearIds = []; 378 | foreach ($values as $value) { 379 | $toClearIds[] = $value[$this->model->primaryKey()]; 380 | } 381 | $toClearKeys = $this->buildRowCacheKey($toClearIds); 382 | $this->getCache()->del(array_values($toClearKeys)); 383 | } 384 | 385 | return parent::insert($values); 386 | } 387 | 388 | public function insertGetId(array $values, $sequence = null) 389 | { 390 | if ($this->needFlushCache()) { 391 | // 清空表级缓存 392 | $meta = $this->getMeta(); 393 | $meta->flush($this->db(), $this->model->table()); 394 | } 395 | 396 | $id = parent::insertGetId($values, $sequence); 397 | 398 | if ($this->needFlushCache()) { 399 | $key = $this->buildRowCacheKey([$id])[$id]; 400 | $this->getCache()->del([$key]); 401 | } 402 | 403 | return $id; 404 | } 405 | 406 | /** 407 | * 获取当前查询所用的数据库名称 408 | */ 409 | private function db() 410 | { 411 | return $this->connection->getDatabaseName(); 412 | } 413 | 414 | /** 415 | * 构造行级缓存索引 416 | */ 417 | private function buildRowCacheKey($keyValues) 418 | { 419 | $meta = $this->getMeta(); 420 | $prefix = $meta->prefix($this->db(), $this->model->table()); 421 | 422 | $keys = []; 423 | foreach ($keyValues as $keyValue) { 424 | $keys[$keyValue] = md5($prefix . ':' . $keyValue); 425 | } 426 | 427 | return $keys; 428 | } 429 | 430 | /** 431 | * 构造表级缓存索引 432 | */ 433 | private function buildTableCacheKey($sql, $bindings) 434 | { 435 | $meta = $this->getMeta(); 436 | $parts = [ 437 | $meta->prefix($this->db(), $this->model->table(), true), 438 | $sql, 439 | json_encode($bindings), 440 | ]; 441 | 442 | return md5(implode(':', $parts)); 443 | } 444 | 445 | /** 446 | * 返回查询对应的缓存 key 447 | * 448 | * 仅供调试使用! 449 | */ 450 | public function key() 451 | { 452 | if ($this->isSimple()) { 453 | return $this->buildCacheKeys(); 454 | } elseif ($this->isAwful()) { 455 | return $this->buildAwfulCacheKey(); 456 | } else { 457 | return $this->buildAwfulCacheKey(); 458 | } 459 | } 460 | 461 | /** 462 | * 过期当前表所有缓存 463 | */ 464 | public function flush() 465 | { 466 | $this->getMeta()->flushAll($this->db(), $this->model->table()); 467 | } 468 | 469 | private function fireEvent($name, $data = []) 470 | { 471 | /** @var $container Container */ 472 | $container = Container::getInstance(); 473 | if (!$container->bound(Dispatcher::class)) { 474 | return; 475 | } 476 | 477 | $data['table'] = $this->model->table(); 478 | $data['db'] = $this->db(); 479 | 480 | $event = 'angejia.pea.' . $name; 481 | Container::getInstance()->make(Dispatcher::class)->fire($event, $data); 482 | } 483 | } 484 | -------------------------------------------------------------------------------- /src/RedisCache.php: -------------------------------------------------------------------------------- 1 | redis = $redis; 15 | } 16 | 17 | public function get($keys) 18 | { 19 | if (!$keys) { 20 | return []; 21 | } 22 | 23 | $keyValue = array_combine($keys, $this->redis->mget($keys)); 24 | array_walk($keyValue, function (&$item) { 25 | $item = json_decode($item); 26 | }); 27 | $keyValue = array_filter($keyValue, function ($value) { 28 | return !is_null($value); 29 | }); 30 | 31 | return $keyValue; 32 | } 33 | 34 | public function set($keyValue) 35 | { 36 | $pipe = $this->redis->pipeline(); 37 | 38 | foreach ($keyValue as $key => $value) { 39 | if (!is_null($value)) { 40 | $value = json_encode($value); 41 | $pipe->setex($key, 86400, $value); // 缓存 1 天 42 | } 43 | } 44 | 45 | return $pipe->execute(); 46 | } 47 | 48 | public function del($keys) 49 | { 50 | return $this->redis->del($keys); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/RedisMeta.php: -------------------------------------------------------------------------------- 1 | redis = $redis; 20 | } 21 | 22 | public function prefix($db, $table, $isForTable = false) 23 | { 24 | $version = $this->getSchemaVersion($db, $table); 25 | if ($isForTable) { 26 | $version = $version . ':' . $this->getUpdateVersion($db, $table); 27 | } 28 | 29 | return implode(':', [ 30 | $this->prefix, 31 | $db, 32 | $table, 33 | $version, 34 | ]); 35 | } 36 | 37 | private function getSchemaVersion($db, $table) 38 | { 39 | $key = implode(':', [ 40 | $this->prefix, 41 | self::KEY_SCHEMA_VERSION, 42 | $db, 43 | $table, 44 | ]); 45 | 46 | $key = md5($key); 47 | 48 | return $this->redis->get($key) ?: 0; 49 | } 50 | 51 | private function getUpdateVersion($db, $table) 52 | { 53 | $key = implode(':', [ 54 | $this->prefix, 55 | self::KEY_UPDATE_VERSION, 56 | $db, 57 | $table, 58 | ]); 59 | 60 | $key = md5($key); 61 | 62 | return $this->redis->get($key) ?: 0; 63 | } 64 | 65 | public function flush($db, $table) 66 | { 67 | $key = implode(':', [ 68 | $this->prefix, 69 | self::KEY_UPDATE_VERSION, 70 | $db, 71 | $table, 72 | ]); 73 | 74 | $key = md5($key); 75 | 76 | $this->redis->incr($key); 77 | } 78 | 79 | public function flushAll($db, $table) 80 | { 81 | $key = implode(':', [ 82 | $this->prefix, 83 | self::KEY_SCHEMA_VERSION, 84 | $db, 85 | $table, 86 | ]); 87 | 88 | $key = md5($key); 89 | 90 | $this->redis->incr($key); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/SchemaFacade.php: -------------------------------------------------------------------------------- 1 | connection($name)->getSchemaBuilder(); 31 | 32 | $builder->blueprintResolver(function ($table, $callback) { 33 | $blueprint = new Blueprint($table, $callback); 34 | $blueprint->setMeta(static::$app[Meta::class]); 35 | 36 | return $blueprint; 37 | }); 38 | 39 | return $builder; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | alias('Schema', SchemaFacade::class); 11 | } 12 | 13 | public function register() 14 | { 15 | // TODO 缓存 redis 实例可以配置 16 | $this->app->singleton(Meta::class, function () { 17 | return new RedisMeta($this->getRedis()); 18 | }); 19 | $this->app->singleton(Cache::class, function () { 20 | return new RedisCache($this->getRedis()); 21 | }); 22 | } 23 | 24 | /** 25 | * 默认取名称为 pea 的 Redis 实例;如果没有,则取 default 。 26 | */ 27 | private function getRedis() 28 | { 29 | $redis = $this->app->make('redis'); 30 | return $redis->connection('pea') ?: $redis->connection(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/CacheTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('mget')->with([ 13 | 'a', 14 | 'b', 15 | 'c', 16 | ])->andReturn([ 17 | 1, 18 | "null", 19 | '[]', 20 | ]); 21 | 22 | $cache = new RedisCache($redis); 23 | $result = $cache->get(['a', 'b', 'c']); 24 | $this->assertEquals([ 'a' => 1 , 'c' => [] ], $result); 25 | } 26 | 27 | public function testSet() 28 | { 29 | $redis = M::mock(Redis::class); 30 | $pipe = M::mock('pipe'); 31 | $pipe->shouldReceive('setex')->with('a', 86400, '[1,2]'); 32 | $pipe->shouldReceive('setex')->with('c', 86400, '[]'); 33 | $pipe->shouldReceive('execute'); 34 | $redis->shouldReceive('pipeline')->andReturn($pipe); 35 | 36 | $cache = new RedisCache($redis); 37 | $cache->set([ 38 | 'a' => [1, 2], 39 | 'b' => null, 40 | 'c' => [], 41 | ]); 42 | } 43 | 44 | public function testDel() 45 | { 46 | $redis = M::mock(Redis::class); 47 | $redis->shouldReceive('del')->with([ 48 | 'a', 49 | 'b', 50 | ]); 51 | 52 | $cache = new RedisCache($redis); 53 | $cache->del(['a', 'b']); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/MetaTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('incr')->with('1031d621441d2f689f95870d86225cf2'); 13 | 14 | $meta = new RedisMeta($redis); 15 | $meta->flush('angejia', 'user'); 16 | } 17 | 18 | public function testFlushAll() 19 | { 20 | $redis = M::mock(Redis::class); 21 | $redis->shouldReceive('incr')->with('2d23e940e190c4fefe3955fe8cf5c8a8'); 22 | 23 | $meta = new RedisMeta($redis); 24 | $meta->flushAll('angejia', 'user'); 25 | } 26 | 27 | public function testPrefixForRow() 28 | { 29 | $redis = M::mock(Redis::class); 30 | $redis->shouldReceive('get')->with('2d23e940e190c4fefe3955fe8cf5c8a8') 31 | ->andReturn(1); 32 | 33 | $meta = new RedisMeta($redis); 34 | $prefix = $meta->prefix('angejia', 'user'); 35 | $this->assertEquals('pea:angejia:user:1', $prefix); 36 | } 37 | 38 | public function testPrefixForTable() 39 | { 40 | $redis = M::mock(Redis::class); 41 | // schema version 42 | $redis->shouldReceive('get')->with('2d23e940e190c4fefe3955fe8cf5c8a8') 43 | ->andReturn(1); 44 | // update version 45 | $redis->shouldReceive('get')->with('1031d621441d2f689f95870d86225cf2') 46 | ->andReturn(6); 47 | 48 | $meta = new RedisMeta($redis); 49 | $prefix = $meta->prefix('angejia', 'user', true); 50 | $this->assertEquals('pea:angejia:user:1:6', $prefix); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/ModelTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('getDatabaseName')->andReturn('angejia'); 40 | $conn->shouldReceive('getQueryGrammar')->andReturn(new Grammar); 41 | $conn->shouldReceive('getPostProcessor')->andReturn(new Processor); 42 | 43 | $this->conn = $conn; 44 | 45 | // 让所有 Model 使用我们伪造的数据库连接 46 | $resolver = M::mock(ConnectionResolverInterface::class); 47 | $resolver->shouldReceive('connection') 48 | ->andReturnUsing(function () { 49 | return $this->conn; 50 | }); 51 | User::setConnectionResolver($resolver); 52 | 53 | // 模拟 Meta 服务 54 | $meta = M::mock(Meta::class); 55 | $meta->shouldReceive('prefix') 56 | // 查找 angejia.user 表主键缓存 key 前缀 57 | ->with('angejia', 'user') 58 | // 缓存 key 前缀全部使用空字符串 '' 59 | ->andReturn(''); 60 | $meta->shouldReceive('prefix') 61 | // 查找 angejia.user 表主键缓存 key 前缀 62 | ->with('angejia', 'user', true) 63 | // 缓存 key 前缀全部使用空字符串 '' 64 | ->andReturn(''); 65 | $this->meta = $meta; 66 | 67 | // 模拟 Cache 服务 68 | $cache = M::mock(Cache::class); 69 | $this->cache = $cache; 70 | 71 | // 注入依赖的服务 72 | $this->app->bind(Meta::class, function () { 73 | return $this->meta; 74 | }); 75 | $this->app->bind(Cache::class, function () { 76 | return $this->cache; 77 | }); 78 | } 79 | 80 | public function testModelTable() 81 | { 82 | $user = new User; 83 | $this->assertEquals('user', $user->table()); 84 | } 85 | 86 | public function testModelNeedCache() 87 | { 88 | $user = new User; 89 | $this->assertTrue($user->needCache()); 90 | } 91 | 92 | public function testOneCachedSimpleGet() 93 | { 94 | $this->cache->shouldReceive('get') 95 | // 查询 id 为 1 的缓存 96 | ->with([ 97 | '3558193cd9818af7fe4d2c2f5bd9d00f', 98 | ]) 99 | // 模拟全部命中缓存 100 | ->andReturn([ 101 | '3558193cd9818af7fe4d2c2f5bd9d00f' => (object) [ 'id' => 1, 'name' => '海涛', ], 102 | ]); 103 | 104 | $dispatcher = M::Mock(Dispatcher::class); 105 | $dispatcher->shouldReceive('fire')->with('angejia.pea.get', ['table' => 'user', 'db' => 'angejia']); 106 | $dispatcher->shouldReceive('fire')->with('angejia.pea.hit.simple.1000', ['table' => 'user', 'db' => 'angejia']); 107 | $this->app->instance(Dispatcher::class, $dispatcher); 108 | 109 | // 查询 id 为 1 的记录,应该命中缓存 110 | $u1 = User::find(1); 111 | 112 | $this->assertEquals('海涛', $u1->name); 113 | } 114 | 115 | public function testOneMissedSimpleGet() 116 | { 117 | // 模拟数据库返回查询未命中缓存的数据 118 | $this->conn->shouldReceive('select') 119 | ->with('select * from "user" where "id" in (?) limit 1', [1], true) 120 | ->andReturn([ 121 | (object) [ 'id' => 1, 'name' => '海涛', ], 122 | ]); 123 | 124 | 125 | // 模拟缓存查询结果 126 | $this->cache->shouldReceive('set') 127 | ->with([ 128 | '3558193cd9818af7fe4d2c2f5bd9d00f' => (object) [ 'id' => 1, 'name' => '海涛', ], 129 | ]); 130 | $this->cache->shouldReceive('get') 131 | // 查询 id 为 1 的缓存 132 | ->with([ 133 | '3558193cd9818af7fe4d2c2f5bd9d00f', 134 | ]) 135 | // 模拟全部没有命中缓存 136 | ->andReturn([]); 137 | 138 | $dispatcher = M::Mock(Dispatcher::class); 139 | $dispatcher->shouldReceive('fire')->with('angejia.pea.get', ['table' => 'user', 'db' => 'angejia']); 140 | $dispatcher->shouldReceive('fire')->with('angejia.pea.miss.simple', ['table' => 'user', 'db' => 'angejia']); 141 | $this->app->instance(Dispatcher::class, $dispatcher); 142 | 143 | // 查询 id 为 1 的记录,应该命中缓存 144 | $u1 = User::find(1); 145 | 146 | $this->assertEquals('海涛', $u1->name); 147 | } 148 | 149 | public function testAllCachedSimpleGet() 150 | { 151 | $this->cache->shouldReceive('get') 152 | // 查询 id 为 1 和 2 的缓存 153 | ->with([ 154 | '3558193cd9818af7fe4d2c2f5bd9d00f', 155 | '343a10e6c2480e111dd3e9e564eb7966', 156 | ]) 157 | // 模拟全部命中缓存 158 | ->andReturn([ 159 | '3558193cd9818af7fe4d2c2f5bd9d00f' => (object) [ 'id' => 1, 'name' => '海涛', ], 160 | '343a10e6c2480e111dd3e9e564eb7966' => (object) [ 'id' => 2, 'name' => '涛涛', ], 161 | ]); 162 | 163 | $dispatcher = M::Mock(Dispatcher::class); 164 | $dispatcher->shouldReceive('fire')->with('angejia.pea.get', ['table' => 'user', 'db' => 'angejia']); 165 | $dispatcher->shouldReceive('fire')->with('angejia.pea.hit.simple.1000', ['table' => 'user', 'db' => 'angejia']); 166 | $this->app->instance(Dispatcher::class, $dispatcher); 167 | 168 | 169 | // 查询 id 为 1 和 2 的记录,应该全部命中缓存 170 | // TODO 此处顺序是个问题 171 | list($u1, $u2) = User::find([1, 2]); 172 | 173 | $this->assertEquals('海涛', $u1->name); 174 | $this->assertEquals('涛涛', $u2->name); 175 | } 176 | 177 | public function testPartialCachedSimpleGet() 178 | { 179 | $this->cache->shouldReceive('get') 180 | ->with([ 181 | '3558193cd9818af7fe4d2c2f5bd9d00f', 182 | '343a10e6c2480e111dd3e9e564eb7966', 183 | ]) 184 | // 缓存中只有 id 为 1 的数据 185 | ->andReturn([ 186 | '3558193cd9818af7fe4d2c2f5bd9d00f' => (object) [ 'id' => 1, 'name' => '海涛', ], 187 | ]); 188 | 189 | // 模拟数据库返回查询未命中缓存的数据 190 | $this->conn->shouldReceive('select') 191 | ->with('select * from "user" where "id" in (?)', [2], true) 192 | ->andReturn([ 193 | (object) [ 'id' => 2, 'name' => '涛涛', ], 194 | ]); 195 | 196 | // 查询完成后需要将数据写入缓存 197 | $this->cache->shouldReceive('set') 198 | ->with([ 199 | '343a10e6c2480e111dd3e9e564eb7966' => (object) [ 'id' => 2, 'name' => '涛涛', ], 200 | ]); 201 | 202 | $dispatcher = M::Mock(Dispatcher::class); 203 | $dispatcher->shouldReceive('fire')->with('angejia.pea.get', ['table' => 'user', 'db' => 'angejia']); 204 | $dispatcher->shouldReceive('fire')->with('angejia.pea.hit.simple.500', ['table' => 'user', 'db' => 'angejia']); 205 | $this->app->instance(Dispatcher::class, $dispatcher); 206 | 207 | list($u1, $u2) = User::find([1, 2]); 208 | 209 | $this->assertEquals('海涛', $u1->name); 210 | $this->assertEquals('涛涛', $u2->name); 211 | } 212 | 213 | public function testAllMissedSimpleGet() 214 | { 215 | $this->cache->shouldReceive('get') 216 | ->with([ 217 | '3558193cd9818af7fe4d2c2f5bd9d00f', 218 | '343a10e6c2480e111dd3e9e564eb7966', 219 | ]) 220 | // 缓存中只有 id 为 1 的数据 221 | ->andReturn([]); 222 | 223 | // 模拟数据库返回查询未命中缓存的数据 224 | $this->conn->shouldReceive('select') 225 | ->with('select * from "user" where "id" in (?, ?)', [1, 2], true) 226 | ->andReturn([ 227 | (object) [ 'id' => 1, 'name' => '海涛', ], 228 | (object) [ 'id' => 2, 'name' => '涛涛', ], 229 | ]); 230 | 231 | // 查询完成后需要将数据写入缓存 232 | $this->cache->shouldReceive('set') 233 | ->with([ 234 | '343a10e6c2480e111dd3e9e564eb7966' => (object) [ 'id' => 2, 'name' => '涛涛', ], 235 | '3558193cd9818af7fe4d2c2f5bd9d00f' => (object) [ 'id' => 1, 'name' => '海涛', ], 236 | ]); 237 | 238 | $dispatcher = M::Mock(Dispatcher::class); 239 | $dispatcher->shouldReceive('fire')->with('angejia.pea.get', ['table' => 'user', 'db' => 'angejia']); 240 | $dispatcher->shouldReceive('fire')->with('angejia.pea.miss.simple', ['table' => 'user', 'db' => 'angejia']); 241 | $this->app->instance(Dispatcher::class, $dispatcher); 242 | 243 | list($u1, $u2) = User::find([1, 2]); 244 | 245 | $this->assertEquals('海涛', $u1->name); 246 | $this->assertEquals('涛涛', $u2->name); 247 | } 248 | 249 | public function testFlushCacheForInsert() 250 | { 251 | $pdo = M::mock('\PDO'); 252 | // 模拟数据库返回自增主键 ID 253 | $pdo->shouldReceive('lastInsertId')->andReturn(1); 254 | $this->conn->shouldReceive('getPdo')->andReturn($pdo); 255 | $this->conn->shouldReceive('insert'); 256 | 257 | // 模拟刷新表级缓存 258 | $this->meta->shouldReceive('flush')->with('angejia', 'user'); 259 | 260 | $this->cache->shouldReceive('del')->with([ 261 | '3558193cd9818af7fe4d2c2f5bd9d00f', 262 | ]); 263 | 264 | $user = new User; 265 | $user->name = '海涛'; 266 | $user->save(); 267 | $this->assertEquals(1, $user->id); 268 | } 269 | 270 | public function testFlushCacheForBatchInsert() 271 | { 272 | $pdo = M::mock('\PDO'); 273 | // 模拟数据库返回自增主键 ID 274 | $pdo->shouldReceive('lastInsertId')->andReturn(1); 275 | $this->conn->shouldReceive('getPdo')->andReturn($pdo); 276 | $this->conn->shouldReceive('insert'); 277 | 278 | // 模拟刷新表级缓存 279 | $this->meta->shouldReceive('flush')->with('angejia', 'user'); 280 | 281 | $this->cache->shouldReceive('del')->with([ 282 | '3558193cd9818af7fe4d2c2f5bd9d00f', 283 | '343a10e6c2480e111dd3e9e564eb7966', 284 | ]); 285 | 286 | User::insert([ 287 | ['id' => 1, 'name' => '海涛'], 288 | ['id' => 2, 'name' => 'haitao'], 289 | ]); 290 | } 291 | 292 | public function testFlushCacheForUpdateOne() 293 | { 294 | // 模拟数据库更新操作 295 | $this->conn->shouldReceive('update'); 296 | // 模拟刷新表级缓存 297 | $this->meta->shouldReceive('flush')->with('angejia', 'user'); 298 | // 模拟刷新行级缓存 299 | $this->cache->shouldReceive('del') 300 | ->with(['3558193cd9818af7fe4d2c2f5bd9d00f']); 301 | 302 | $this->cache->shouldReceive('get') 303 | // 查询 id 为 1 的缓存 304 | ->with([ 305 | '3558193cd9818af7fe4d2c2f5bd9d00f', 306 | ]) 307 | // 模拟全部命中缓存 308 | ->andReturn([ 309 | '3558193cd9818af7fe4d2c2f5bd9d00f' => (object) [ 'id' => 1, 'name' => '海涛', ], 310 | ]); 311 | 312 | // 模拟返回受到影响的数据,用于清理缓存 313 | $this->conn->shouldReceive('select') 314 | ->andReturn([ 315 | (object) [ 'id' => 1, 'name' => '海涛', ], 316 | ]); 317 | 318 | 319 | $user = User::find(1); 320 | $user->name = '海涛2'; 321 | $user->save(); 322 | } 323 | 324 | public function testFlushCacheForDeleteOne() 325 | { 326 | // 模拟数据库更新 327 | $this->conn->shouldReceive('delete'); 328 | 329 | // 模拟刷新表级缓存 330 | $this->meta->shouldReceive('flush')->with('angejia', 'user'); 331 | // 模拟刷新行级缓存 332 | $this->cache->shouldReceive('del') 333 | ->with(['3558193cd9818af7fe4d2c2f5bd9d00f']); 334 | 335 | // 模拟数据库返回首次查询结果 336 | $this->cache->shouldReceive('get') 337 | // 查询 id 为 1 的缓存 338 | ->with([ 339 | '3558193cd9818af7fe4d2c2f5bd9d00f', 340 | ]) 341 | // 模拟全部命中缓存 342 | ->andReturn([ 343 | '3558193cd9818af7fe4d2c2f5bd9d00f' => (object) [ 'id' => 1, 'name' => '海涛', ], 344 | ]); 345 | 346 | // 模拟返回受到影响的数据,用于清理缓存 347 | $this->conn->shouldReceive('select') 348 | ->andReturn([ 349 | (object) [ 'id' => 1, 'name' => '海涛', ], 350 | ]); 351 | 352 | $user = User::find(1); 353 | $user->delete(); 354 | } 355 | 356 | /** 357 | * 复杂查询,命中缓存 358 | */ 359 | public function testCachedAwfulGet() 360 | { 361 | $this->cache->shouldReceive('get') 362 | ->with([ 363 | 'a52d401e05fd1cc438bd070bc4c1c14f', 364 | ]) 365 | ->andReturn([ 366 | 'a52d401e05fd1cc438bd070bc4c1c14f' => [ 367 | (object) [ 'id' => 1, 'name' => '海涛', ], 368 | ] 369 | ]); 370 | 371 | $dispatcher = M::Mock(Dispatcher::class); 372 | $dispatcher->shouldReceive('fire')->with('angejia.pea.get', ['table' => 'user', 'db' => 'angejia']); 373 | $dispatcher->shouldReceive('fire')->with('angejia.pea.hit.awful', ['table' => 'user', 'db' => 'angejia']); 374 | $this->app->instance(Dispatcher::class, $dispatcher); 375 | 376 | $users = User::where('status', 1)->orderBy('id', 'desc')->get(); 377 | $user0 = $users[0]; 378 | $this->assertEquals(1, $user0->id); 379 | } 380 | 381 | /** 382 | * 复杂查询未命中缓存 383 | */ 384 | public function testMissAwfulGet() 385 | { 386 | // 模拟数据库返回结果 387 | $this->conn->shouldReceive('select') 388 | ->with('select * from "user" where "status" = ? order by "id" desc', [1], true) 389 | ->andReturn([ 390 | (object) [ 'id' => 1, 'name' => '海涛', ], 391 | ]); 392 | // 模拟未命中缓存 393 | $this->cache->shouldReceive('get') 394 | ->with([ 395 | 'a52d401e05fd1cc438bd070bc4c1c14f', 396 | ]) 397 | ->andReturn([]); 398 | // 模拟缓存查询结果 399 | $this->cache->shouldReceive('set') 400 | ->with([ 401 | 'a52d401e05fd1cc438bd070bc4c1c14f' => [ 402 | (object) [ 'id' => 1, 'name' => '海涛', ], 403 | ] 404 | ]); 405 | 406 | $dispatcher = M::Mock(Dispatcher::class); 407 | $dispatcher->shouldReceive('fire')->with('angejia.pea.get', ['table' => 'user', 'db' => 'angejia']); 408 | $dispatcher->shouldReceive('fire')->with('angejia.pea.miss.awful', ['table' => 'user', 'db' => 'angejia']); 409 | $this->app->instance(Dispatcher::class, $dispatcher); 410 | 411 | $uers = User::where('status', 1)->orderBy('id', 'desc')->get(); 412 | } 413 | 414 | /** 415 | * 复杂查询未命中缓存 416 | */ 417 | public function testRawAwfulGet() 418 | { 419 | // 模拟数据库返回结果 420 | $this->conn->shouldReceive('select') 421 | ->with('select count(*) from "user" where "id" = ?', [1], true) 422 | ->andReturn([ 423 | (object) ["count(*)" => 1], 424 | ]); 425 | // 模拟未命中缓存 426 | $this->cache->shouldReceive('get') 427 | ->with([ 428 | '122a32ae3f8656ba95b87700de71506c', 429 | ]) 430 | ->andReturn([]); 431 | // 模拟缓存查询结果 432 | $this->cache->shouldReceive('set') 433 | ->with([ 434 | '122a32ae3f8656ba95b87700de71506c' => [ 435 | (object) ["count(*)" => 1], 436 | ] 437 | ]); 438 | 439 | $dispatcher = M::Mock(Dispatcher::class); 440 | $dispatcher->shouldReceive('fire')->with('angejia.pea.get', ['table' => 'user', 'db' => 'angejia']); 441 | $dispatcher->shouldReceive('fire')->with('angejia.pea.miss.awful', ['table' => 'user', 'db' => 'angejia']); 442 | $this->app->instance(Dispatcher::class, $dispatcher); 443 | 444 | $data = User::where('id', 1)->select(new Expression('count(*)'))->get(); 445 | $this->assertEquals(1, $data[0]['count(*)']); 446 | } 447 | 448 | /** 449 | * 普通查询,全部命中缓存 450 | */ 451 | public function testAllCachedNormalGet() 452 | { 453 | // 模拟命中主键 1 和 2,通过表级缓存返回 454 | $this->cache->shouldReceive('get') 455 | ->with([ 456 | 'b7f40265619c7ef9fc80ff41bee68632', 457 | ]) 458 | ->andReturn([ 459 | 'b7f40265619c7ef9fc80ff41bee68632' => [ 460 | (object) [ 'id' => 1, ], 461 | (object) [ 'id' => 2, ], 462 | ] 463 | ]); 464 | 465 | $this->cache->shouldReceive('get') 466 | ->with([ 467 | '3558193cd9818af7fe4d2c2f5bd9d00f', 468 | '343a10e6c2480e111dd3e9e564eb7966', 469 | ]) 470 | // 缓存中只有 id 为 1 的数据 471 | ->andReturn([ 472 | '3558193cd9818af7fe4d2c2f5bd9d00f' => (object) [ 'id' => 1, 'name' => '海涛', ], 473 | '343a10e6c2480e111dd3e9e564eb7966' => (object) [ 'id' => 2, 'name' => '涛涛', ], 474 | ]); 475 | 476 | $users = User::where('status', 1)->take(2)->skip(1)->get(); 477 | $user0 = $users[0]; 478 | $this->assertEquals(1, $user0->id); 479 | } 480 | 481 | /** 482 | * 普通查询,部分命中缓存 483 | */ 484 | public function testPartialCachedNormalGet() 485 | { 486 | $this->conn->shouldReceive('select') 487 | ->with('select * from "user" where "id" in (?)', [2], true) 488 | ->andReturn([ 489 | (object) [ 'id' => 2, 'name' => '涛涛', ], 490 | ]); 491 | 492 | // 查询完成后需要将数据写入缓存 493 | $this->cache->shouldReceive('set') 494 | ->with([ 495 | '343a10e6c2480e111dd3e9e564eb7966' => (object) [ 'id' => 2, 'name' => '涛涛', ], 496 | ]); 497 | 498 | // 模拟命中主键 1 和 2,通过表级缓存返回 499 | $this->cache->shouldReceive('get') 500 | ->with([ 501 | 'b7f40265619c7ef9fc80ff41bee68632', 502 | ]) 503 | ->andReturn([ 504 | 'b7f40265619c7ef9fc80ff41bee68632' => [ 505 | (object) [ 'id' => 1, ], 506 | (object) [ 'id' => 2, ], 507 | ] 508 | ]); 509 | 510 | $this->cache->shouldReceive('get') 511 | ->with([ 512 | '3558193cd9818af7fe4d2c2f5bd9d00f', 513 | '343a10e6c2480e111dd3e9e564eb7966', 514 | ]) 515 | // 缓存中只有 id 为 1 的数据 516 | ->andReturn([ 517 | '3558193cd9818af7fe4d2c2f5bd9d00f' => (object) [ 'id' => 1, 'name' => '海涛', ], 518 | ]); 519 | 520 | $users = User::where('status', 1)->take(2)->skip(1)->get(); 521 | $user0 = $users[0]; 522 | $this->assertEquals(1, $user0->id); 523 | } 524 | 525 | /** 526 | * 普通查询,空结果集 527 | */ 528 | public function testEmptyNormalGet() 529 | { 530 | // 模拟获取主键列表,返回空结果 531 | $this->conn->shouldReceive('select') 532 | ->with('select * from "user" where "status" = ? limit 2 offset 1', [1], true) 533 | ->andReturn([]); 534 | 535 | // 查询完成后需要将数据写入缓存 536 | $this->cache->shouldReceive('set') 537 | ->with([ 538 | 'b7f40265619c7ef9fc80ff41bee68632' => [], 539 | ]); 540 | 541 | // 模拟未命中表级缓存 542 | $this->cache->shouldReceive('get') 543 | ->with([ 544 | 'b7f40265619c7ef9fc80ff41bee68632', 545 | ]) 546 | ->andReturn([]); 547 | 548 | $users = User::where('status', 1)->take(2)->skip(1)->get(); 549 | $this->assertEquals(0, count($users)); 550 | } 551 | 552 | /** 553 | * 获取当前查询缓存 key 554 | */ 555 | public function testQueryCacheKey() 556 | { 557 | // 简单查询 558 | $key = User::where('id', 1)->key(); 559 | $this->assertEquals([1 => '3558193cd9818af7fe4d2c2f5bd9d00f' ], $key); 560 | $key = User::whereIn('id', [1, 2])->key(); 561 | $this->assertEquals([ 562 | 1 => '3558193cd9818af7fe4d2c2f5bd9d00f', 563 | 2 => '343a10e6c2480e111dd3e9e564eb7966', 564 | ], $key); 565 | 566 | // 复杂查询 567 | $key = User::where('id', 2)->orderBy('id', 'desc')->key(); 568 | $this->assertEquals('791b4459a33f6b3d4929b5b7ea9de084', $key); 569 | } 570 | 571 | /** 572 | * 刷新当前表所有缓存 573 | */ 574 | public function testFlushCache() 575 | { 576 | $this->meta->shouldReceive('flushAll')->with('angejia', 'user'); 577 | 578 | User::flush(); 579 | } 580 | public function testEmptySimpleGet() 581 | { 582 | // 模拟获取主键列表,返回空结果 583 | $this->conn->shouldReceive('select') 584 | ->with('select * from "user" where "id" in (?, ?)', [1, 2], true) 585 | ->andReturn([ 586 | (object) ['id' => 2, 'name' => '海涛'], 587 | ]); 588 | 589 | // 查询完成后需要将数据写入缓存 590 | $this->cache->shouldReceive('set') 591 | ->with([ 592 | '3558193cd9818af7fe4d2c2f5bd9d00f' => [], 593 | '343a10e6c2480e111dd3e9e564eb7966' => (object) ['id' => 2, 'name' => '海涛'], 594 | ]); 595 | 596 | // 模拟未命中表级缓存 597 | $this->cache->shouldReceive('get') 598 | ->with([ 599 | '3558193cd9818af7fe4d2c2f5bd9d00f', 600 | '343a10e6c2480e111dd3e9e564eb7966', 601 | ]) 602 | ->andReturn([]); 603 | 604 | $users = User::whereIn('id', [1, 2])->get(); 605 | $this->assertEquals(1, count($users)); 606 | $user = $users[0]; 607 | $this->assertEquals('海涛', $user->name); 608 | } 609 | 610 | public function testEmptySimpleHitGet() 611 | { 612 | // 模拟命中表级空缓存 613 | $this->cache->shouldReceive('get') 614 | ->with([ 615 | '3558193cd9818af7fe4d2c2f5bd9d00f', 616 | ]) 617 | ->andReturn([ 618 | '3558193cd9818af7fe4d2c2f5bd9d00f' => [], 619 | ]); 620 | 621 | $users = User::where('id', 1)->get(); 622 | $this->assertEquals(0, count($users)); 623 | } 624 | } 625 | 626 | class User extends Model 627 | { 628 | protected $table = 'user'; 629 | protected $needCache = true; 630 | public $timestamps = false; 631 | } 632 | -------------------------------------------------------------------------------- /test/TestCase.php: -------------------------------------------------------------------------------- 1 | app = Container::getInstance(); 17 | } 18 | } 19 | --------------------------------------------------------------------------------