Merge pull request #2054 from limingxinleo/2.0-model-cache

Added eager load relation for model-cache.
This commit is contained in:
李铭昕 2020-07-07 10:20:11 +08:00 committed by GitHub
commit 8920f3e3ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 382 additions and 1 deletions

View File

@ -9,6 +9,7 @@
## Added
- [#2042](https://github.com/hyperf/hyperf/pull/2042) Added `ScanFileDriver` to watch file changes for `hyperf/watcher`.
- [#2054](https://github.com/hyperf/hyperf/pull/2054) Added eager load relation for model-cache.
## Optimized

View File

@ -146,3 +146,47 @@ User::query(true)->where('gender', '>', 1)->delete();
当生产环境使用了模型缓存时,如果已经建立了对应缓存数据,但此时又因为逻辑变更,添加了新的字段,并且默认值不是 `0`、`空字符`、`null` 这类数据时,就会导致在数据查询时,从缓存中查出来的数据与数据库中的数据不一致。
对于这种情况,我们可以修改 `use_default_value``true`,并添加 `Hyperf\DbConnection\Listener\InitTableCollectorListener``listener.php` 配置中,使 Hyperf 应用在启动时主动去获取数据库的字段信息,并在获取缓存数据时与之比较并进行缓存数据修正。
### EagerLoad
当我们使用模型关系时,可以通过 `load` 解决 `N+1` 的问题,但仍然需要查一次数据库。模型缓存通过重写了 `ModelBuilder`,可以让用户尽可能的从缓存中拿到对应的模型。
> 本功能不支持 `morphTo` 和不是只有 `whereIn` 查询的关系模型。
以下提供两种方式:
1. 配置 EagerLoadListener直接使用 `loadCache` 方法。
修改 `listeners.php` 配置
```php
return [
Hyperf\ModelCache\Listener\EagerLoadListener::class,
];
```
通过 `loadCache` 方法,加载对应的模型关系。
```php
$books = Book::findManyFromCache([1,2,3]);
$books->loadCache(['user']);
foreach ($books as $book){
var_dump($book->user);
}
```
2. 使用 EagerLoader
```php
use Hyperf\ModelCache\EagerLoad\EagerLoader;
use Hyperf\Utils\ApplicationContext;
$books = Book::findManyFromCache([1,2,3]);
$loader = ApplicationContext::getContainer()->get(EagerLoader::class);
$loader->load($books, ['user']);
foreach ($books as $book){
var_dump($book->user);
}
```

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ModelCache\EagerLoad;
use Hyperf\Database\Connection;
use Hyperf\Database\Model\Builder;
use Hyperf\Database\Model\Collection;
use Hyperf\Database\Model\Model;
use Hyperf\Database\Query\Builder as QueryBuilder;
class EagerLoader
{
public function load(Collection $collection, array $relations)
{
if ($collection->isNotEmpty()) {
/** @var Model $first */
$first = $collection->first();
$query = $first->registerGlobalScopes($this->newBuilder($first))->with($relations);
$collection->fill($query->eagerLoadRelations($collection->all()));
}
}
protected function newBuilder(Model $model): Builder
{
$builder = new EagerLoaderBuilder($this->newBaseQueryBuilder($model));
return $builder->setModel($model);
}
/**
* Get a new query builder instance for the connection.
*
* @return \Hyperf\Database\Query\Builder
*/
protected function newBaseQueryBuilder(Model $model)
{
/** @var Connection $connection */
$connection = $model->getConnection();
return new QueryBuilder($connection, $connection->getQueryGrammar(), $connection->getPostProcessor());
}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ModelCache\EagerLoad;
use Closure;
use Hyperf\Database\Model\Builder;
use Hyperf\Database\Model\Relations\Relation;
use Hyperf\ModelCache\CacheableInterface;
use Hyperf\Utils\Arr;
class EagerLoaderBuilder extends Builder
{
protected function eagerLoadRelation(array $models, $name, Closure $constraints)
{
// First we will "back up" the existing where conditions on the query so we can
// add our eager constraints. Then we will merge the wheres that were on the
// query back to it in order that any where conditions might be specified.
$relation = $this->getRelation($name);
$relation->addEagerConstraints($models);
$constraints($relation);
// Once we have the results, we just match those back up to their parent models
// using the relationship instance. Then we just return the finished arrays
// of models which have been eagerly hydrated and are readied for return.
return $relation->match(
$relation->initRelation($models, $name),
$this->getEagerModels($relation),
$name
);
}
protected function getEagerModels(Relation $relation)
{
$wheres = $relation->getQuery()->getQuery()->wheres;
$model = $relation->getModel();
$column = sprintf('%s.%s', $model->getTable(), $model->getKeyName());
if (count($wheres) === 1
&& $model instanceof CacheableInterface
&& Arr::get($wheres[0], 'type') === 'InRaw'
&& Arr::get($wheres[0], 'column') === $column) {
return $model::findManyFromCache($wheres[0]['values'] ?? []);
}
return $relation->getEager();
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ModelCache\Listener;
use Hyperf\Database\Model\Collection;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Framework\Event\BootApplication;
use Hyperf\ModelCache\EagerLoad\EagerLoader;
use Psr\Container\ContainerInterface;
class EagerLoadListener implements ListenerInterface
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function listen(): array
{
return [
BootApplication::class,
];
}
public function process(object $event)
{
$eagerLoader = $this->container->get(EagerLoader::class);
Collection::macro('loadCache', function ($parameters) use ($eagerLoader) {
$eagerLoader->load($this, $parameters);
});
}
}

View File

@ -11,9 +11,14 @@ declare(strict_types=1);
*/
namespace HyperfTest\ModelCache;
use Hyperf\Database\Model\Relations\Relation;
use Hyperf\DbConnection\Listener\InitTableCollectorListener;
use Hyperf\ModelCache\EagerLoad\EagerLoader;
use Hyperf\ModelCache\Listener\EagerLoadListener;
use Hyperf\Redis\RedisProxy;
use HyperfTest\ModelCache\Stub\BookModel;
use HyperfTest\ModelCache\Stub\ContainerStub;
use HyperfTest\ModelCache\Stub\ImageModel;
use HyperfTest\ModelCache\Stub\UserExtModel;
use HyperfTest\ModelCache\Stub\UserHiddenModel;
use HyperfTest\ModelCache\Stub\UserModel;
@ -257,6 +262,56 @@ class ModelCacheTest extends TestCase
$this->assertEquals(array_keys($model->getAttributes()), array_keys($model3->getAttributes()));
}
public function testEagerLoad()
{
$container = ContainerStub::mockContainer();
/** @var \Redis $redis */
$redis = $container->make(RedisProxy::class, ['pool' => 'default']);
$redis->del('{mc:default:m:user}:id:1', '{mc:default:m:user}:id:2');
$this->assertSame(0, $redis->exists('{mc:default:m:user}:id:1', '{mc:default:m:user}:id:2'));
$books = BookModel::query()->get();
$loader = new EagerLoader();
$loader->load($books, ['user']);
$this->assertSame(2, $redis->exists('{mc:default:m:user}:id:1', '{mc:default:m:user}:id:2'));
}
public function testEagerLoadMacro()
{
$container = ContainerStub::mockContainer();
$listener = new EagerLoadListener($container);
$listener->process(new \stdClass());
/** @var \Redis $redis */
$redis = $container->make(RedisProxy::class, ['pool' => 'default']);
$redis->del('{mc:default:m:user}:id:1', '{mc:default:m:user}:id:2');
$this->assertSame(0, $redis->exists('{mc:default:m:user}:id:1', '{mc:default:m:user}:id:2'));
$books = BookModel::query()->get();
$books->loadCache(['user']);
$this->assertSame(2, $redis->exists('{mc:default:m:user}:id:1', '{mc:default:m:user}:id:2'));
}
public function testEagerLoadMorphTo()
{
ContainerStub::mockContainer();
Relation::morphMap([
'user' => UserModel::class,
'book' => BookModel::class,
]);
$images = ImageModel::findManyFromCache([1, 2, 3]);
$loader = new EagerLoader();
$loader->load($images, ['imageable']);
$this->assertInstanceOf(UserModel::class, $images->shift()->imageable);
$this->assertInstanceOf(UserModel::class, $images->shift()->imageable);
$this->assertInstanceOf(BookModel::class, $images->shift()->imageable);
}
public function testWhenAddedNewColumn()
{
$container = ContainerStub::mockContainer();

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\ModelCache\Stub;
use Hyperf\ModelCache\Cacheable;
use Hyperf\ModelCache\CacheableInterface;
use HyperfTest\Database\Stubs\Model\Model;
/**
* @property int $id
* @property int $user_id
* @property string $title
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class BookModel extends Model implements CacheableInterface
{
use Cacheable;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'book';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['id', 'user_id', 'title', 'created_at', 'updated_at'];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = ['id' => 'integer', 'user_id' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];
public function user()
{
return $this->belongsTo(UserModel::class, 'user_id', 'id');
}
public function image()
{
return $this->morphOne(ImageModel::class, 'imageable');
}
}

View File

@ -26,6 +26,7 @@ use Hyperf\Di\Container;
use Hyperf\Event\EventDispatcher;
use Hyperf\Event\ListenerProvider;
use Hyperf\Framework\Logger\StdoutLogger;
use Hyperf\ModelCache\EagerLoad\EagerLoader;
use Hyperf\ModelCache\Handler\RedisHandler;
use Hyperf\ModelCache\Handler\RedisStringHandler;
use Hyperf\ModelCache\Manager;
@ -69,7 +70,7 @@ class ContainerStub
'databases' => [
'default' => [
'driver' => 'mysql',
'host' => 'localhost',
'host' => '127.0.0.1',
'database' => 'hyperf',
'username' => 'root',
'password' => '',
@ -157,6 +158,7 @@ class ContainerStub
});
$container->shouldReceive('get')->with(Manager::class)->andReturn(new Manager($container));
$container->shouldReceive('get')->with(PhpSerializerPacker::class)->andReturn(new PhpSerializerPacker());
$container->shouldReceive('get')->with(EagerLoader::class)->andReturn(new EagerLoader());
return $container;
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\ModelCache\Stub;
use Hyperf\ModelCache\Cacheable;
use Hyperf\ModelCache\CacheableInterface;
use HyperfTest\Database\Stubs\Model\Model;
/**
* @property int $id
* @property string $url
* @property int $imageable_id
* @property string $imageable_type
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class ImageModel extends Model implements CacheableInterface
{
use Cacheable;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'images';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = ['id', 'url', 'imageable_id', 'imageable_type', 'created_at', 'updated_at'];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = ['id' => 'integer', 'imageable_id' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];
public function imageable()
{
return $this->morphTo();
}
}

View File

@ -46,4 +46,9 @@ class UserModel extends Model implements CacheableInterface
* @var array
*/
protected $casts = ['id' => 'integer', 'gender' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];
public function image()
{
return $this->morphOne(ImageModel::class, 'imageable');
}
}

View File

@ -115,6 +115,15 @@ class Collection implements ArrayAccess, Arrayable, Countable, IteratorAggregate
return new HigherOrderCollectionProxy($this, $key);
}
/**
* @param mixed $items
*/
public function fill($items = [])
{
$this->items = $this->getArrayableItems($items);
return $this;
}
/**
* Create a new collection instance if the value isn't one already.
* @param mixed $items