mirror of
https://gitee.com/hyperf/hyperf.git
synced 2024-11-30 10:47:44 +08:00
Merge pull request #2054 from limingxinleo/2.0-model-cache
Added eager load relation for model-cache.
This commit is contained in:
commit
8920f3e3ff
@ -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
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
```
|
||||
|
51
src/model-cache/src/EagerLoad/EagerLoader.php
Normal file
51
src/model-cache/src/EagerLoad/EagerLoader.php
Normal 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());
|
||||
}
|
||||
}
|
57
src/model-cache/src/EagerLoad/EagerLoaderBuilder.php
Normal file
57
src/model-cache/src/EagerLoad/EagerLoaderBuilder.php
Normal 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();
|
||||
}
|
||||
}
|
43
src/model-cache/src/Listener/EagerLoadListener.php
Normal file
43
src/model-cache/src/Listener/EagerLoadListener.php
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
59
src/model-cache/tests/Stub/BookModel.php
Normal file
59
src/model-cache/tests/Stub/BookModel.php
Normal 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');
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
55
src/model-cache/tests/Stub/ImageModel.php
Normal file
55
src/model-cache/tests/Stub/ImageModel.php
Normal 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();
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user