Merge pull request #1435 from wuwu123/master

Added config `use_default_value` for model-cache.
This commit is contained in:
李铭昕 2020-03-19 10:25:57 +08:00 committed by GitHub
commit b769c88a24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 533 additions and 10 deletions

View File

@ -14,14 +14,16 @@ composer require hyperf/model-cache
模型缓存的配置在 `databases` 中。示例如下
| 配置 | 类型 | 默认值 | 备注 |
|:---------------:|:------:|:---------------------------------------------:|:---------------------------------------:|
| handler | string | Hyperf\ModelCache\Handler\RedisHandler::class | 无 |
| cache_key | string | `mc:%s:m:%s:%s:%s` | `mc:缓存前缀:m:表名:主键 KEY:主键值` |
| prefix | string | db connection name | 缓存前缀 |
| ttl | int | 3600 | 超时时间 |
| empty_model_ttl | int | 60 | 查询不到数据时的超时时间 |
| load_script | bool | true | Redis 引擎下 是否使用 evalSha 代替 eval |
| 配置 | 类型 | 默认值 | 备注 |
|:-----------------:|:------:|:---------------------------------------------:|:---------------------------------------:|
| handler | string | Hyperf\ModelCache\Handler\RedisHandler::class | 无 |
| cache_key | string | `mc:%s:m:%s:%s:%s` | `mc:缓存前缀:m:表名:主键 KEY:主键值` |
| prefix | string | db connection name | 缓存前缀 |
| pool | string | default | 缓存池 |
| ttl | int | 3600 | 超时时间 |
| empty_model_ttl | int | 60 | 查询不到数据时的超时时间 |
| load_script | bool | true | Redis 引擎下 是否使用 evalSha 代替 eval |
| use_default_value | bool | false | 是否使用数据库默认值 |
```php
<?php
@ -51,6 +53,7 @@ return [
'ttl' => 3600 * 24,
'empty_model_ttl' => 3600,
'load_script' => true,
'use_default_value' => false,
]
],
];
@ -136,3 +139,11 @@ $models = User::findManyFromCache($ids);
// 删除用户数据 并自动删除缓存
User::query(true)->where('gender', '>', 1)->delete();
```
### 使用默认值
线上使用模型缓存时,如果已经建立了对应缓存,这时又因为逻辑变更,添加了新的字段,并且默认值不是 `0` `空字符` `null` 这类数据时,
在数据查询时,就会导致从缓存中查出来的数据与数据库中的不一致。
这种情况,我们可以修改 `use_default_value``true`,并添加 `Hyperf\DbConnection\Listener\InitTableCollectorListener``listener.php` 配置中就可以解决。

View File

@ -12,6 +12,8 @@ declare(strict_types=1);
namespace Hyperf\Database\Query\Processors;
use Hyperf\Database\Schema\Column;
class MySqlProcessor extends Processor
{
/**
@ -27,6 +29,26 @@ class MySqlProcessor extends Processor
}, $results);
}
public function processColumns($results)
{
$columns = [];
foreach ($results as $i => $value) {
$item = (object) $value;
$columns[$i] = new Column(
$item->table_schema,
$item->table_name,
$item->column_name,
$item->ordinal_position,
$item->column_default,
$item->is_nullable === 'YES',
$item->data_type,
$item->column_comment
);
}
return $columns;
}
/**
* Process the results of a column type listing query.
*

View File

@ -13,6 +13,7 @@ declare(strict_types=1);
namespace Hyperf\Database\Query\Processors;
use Hyperf\Database\Query\Builder;
use Hyperf\Database\Schema\Column;
class Processor
{
@ -54,4 +55,17 @@ class Processor
{
return $results;
}
/**
* @return Column[]
*/
public function processColumns(array $results)
{
$columns = [];
foreach ($results as $item) {
$columns[] = new Column(...array_values($item));
}
return $columns;
}
}

View File

@ -147,6 +147,22 @@ class Builder
return $this->connection->getPostProcessor()->processColumnListing($results);
}
/**
* Get the column.
*
* @return array
*/
public function getColumns()
{
$results = $this->connection->selectFromWriteConnection(
$this->grammar->compileColumns(),
[
$this->connection->getDatabaseName(),
]
);
return $this->connection->getPostProcessor()->processColumns($results);
}
/**
* Modify a table on the schema.
*

View File

@ -0,0 +1,111 @@
<?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\Database\Schema;
class Column
{
/**
* @var string
*/
protected $schema;
/**
* @var string
*/
protected $table;
/**
* @var string
*/
protected $name;
/**
* @var int
*/
protected $position;
/**
* @var mixed
*/
protected $default;
/**
* @var bool
*/
protected $isNullable;
/**
* @var string
*/
protected $type;
/**
* @var string
*/
protected $comment;
public function __construct(string $schema, string $table, string $name, int $position, $default, bool $isNullable, string $type, string $comment)
{
$this->schema = $schema;
$this->table = $table;
$this->name = $name;
$this->position = $position;
$this->default = $default;
$this->isNullable = $isNullable;
$this->type = $type;
$this->comment = $comment;
}
public function getSchema(): string
{
return $this->schema;
}
public function getTable(): string
{
return $this->table;
}
public function getName(): string
{
return $this->name;
}
public function getPosition(): int
{
return $this->position;
}
/**
* @return mixed
*/
public function getDefault()
{
return $this->default;
}
public function isNullable(): bool
{
return $this->isNullable;
}
public function getType(): string
{
return $this->type;
}
public function getComment(): string
{
return $this->comment;
}
}

View File

@ -53,6 +53,14 @@ class MySqlGrammar extends Grammar
return 'select `column_name`, `data_type`, `column_comment` from information_schema.columns where `table_schema` = ? and `table_name` = ? order by ORDINAL_POSITION';
}
/**
* Compile the query to determine the list of columns.
*/
public function compileColumns(): string
{
return 'select `table_schema`, `table_name`, `column_name`, `ordinal_position`, `column_default`, `is_nullable`, `data_type`, `column_comment` from information_schema.columns where `table_schema` = ? order by ORDINAL_POSITION';
}
/**
* Compile a create table command.
*

View File

@ -50,6 +50,21 @@ class MySqlBuilder extends Builder
return $this->connection->getPostProcessor()->processColumnListing($results);
}
/**
* Get the column.
*
* @return array
*/
public function getColumns()
{
$results = $this->connection->select(
$this->grammar->compileColumns(),
[$this->connection->getDatabaseName()]
);
return $this->connection->getPostProcessor()->processColumns($results);
}
/**
* Get the column type listing for a given table.
*

View File

@ -12,7 +12,13 @@ declare(strict_types=1);
namespace HyperfTest\Database;
use Hyperf\Database\Connection;
use Hyperf\Database\ConnectionResolverInterface;
use Hyperf\Database\Query\Processors\MySqlProcessor;
use Hyperf\Database\Schema\Column;
use Hyperf\Database\Schema\Grammars\MySqlGrammar;
use Hyperf\Database\Schema\MySqlBuilder;
use HyperfTest\Database\Stubs\ContainerStub;
use PHPUnit\Framework\TestCase;
/**
@ -35,4 +41,20 @@ class MySqlProcessorTest extends TestCase
$this->assertEquals($expected, $processor->processColumnListing($listing));
}
public function testProcessColumns()
{
$container = ContainerStub::getContainer();
/** @var Connection $connection */
$connection = $container->get(ConnectionResolverInterface::class)->connection();
$connection->setSchemaGrammar(new MySqlGrammar());
$builder = new MySqlBuilder($connection);
$columns = $builder->getColumns();
$this->assertTrue(is_array($columns));
$this->assertTrue(count($columns) > 0);
$this->assertInstanceOf(Column::class, $columns[0]);
}
}

View File

@ -0,0 +1,53 @@
<?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\Database\Stubs;
use Hyperf\Database\ConnectionResolver;
use Hyperf\Database\ConnectionResolverInterface;
use Hyperf\Database\Connectors\ConnectionFactory;
use Hyperf\Database\Connectors\MySqlConnector;
use Hyperf\Utils\ApplicationContext;
use Mockery;
use Psr\Container\ContainerInterface;
class ContainerStub
{
public static function getContainer()
{
$container = Mockery::mock(ContainerInterface::class);
ApplicationContext::setContainer($container);
$container->shouldReceive('has')->andReturn(true);
$container->shouldReceive('get')->with('db.connector.mysql')->andReturn(new MySqlConnector());
$connector = new ConnectionFactory($container);
$dbConfig = [
'driver' => 'mysql',
'host' => 'localhost',
'database' => 'hyperf',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
];
$connection = $connector->make($dbConfig);
$resolver = new ConnectionResolver(['default' => $connection]);
$container->shouldReceive('get')->with(ConnectionResolverInterface::class)->andReturn($resolver);
return $container;
}
}

View File

@ -0,0 +1,63 @@
<?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\DbConnection\Collector;
use Hyperf\Database\Schema\Column;
class TableCollector
{
/**
* @var array
*/
protected $data = [];
/**
* @param Column[] $columns
*/
public function set(string $pool, string $table, array $columns)
{
$this->data[$pool][$table] = $columns;
}
public function add(string $pool, Column $column)
{
$this->data[$pool][$column->getTable()][$column->getName()] = $column;
}
/**
* @return Column[]
*/
public function get(string $pool, ?string $table = null): array
{
if ($table === null) {
return $this->data[$pool] ?? [];
}
return $this->data[$pool][$table] ?? [];
}
public function has(string $pool, ?string $table = null): bool
{
return ! empty($this->get($pool, $table));
}
public function getDefaultValue(string $connectName, string $table): array
{
$tablseData = $this->get($connectName, $table);
$list = [];
foreach ($tablseData as $column) {
$list[$column->getName()] = $column->getDefault();
}
return $list;
}
}

View File

@ -0,0 +1,91 @@
<?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\DbConnection\Listener;
use Hyperf\Command\Event\BeforeHandle;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Contract\ContainerInterface;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Database\ConnectionResolverInterface;
use Hyperf\DbConnection\Collector\TableCollector;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Framework\Event\AfterWorkerStart;
use Hyperf\Process\Event\BeforeProcessHandle;
class InitTableCollectorListener implements ListenerInterface
{
/**
* @var ContainerInterface
*/
protected $container;
/**
* @var ConfigInterface
*/
protected $config;
/**
* @var StdoutLoggerInterface
*/
protected $logger;
/**
* @var TableCollector
*/
protected $collector;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
$this->config = $container->get(ConfigInterface::class);
$this->logger = $container->get(StdoutLoggerInterface::class);
$this->collector = $container->get(TableCollector::class);
}
public function listen(): array
{
return [
BeforeHandle::class,
AfterWorkerStart::class,
BeforeProcessHandle::class,
];
}
public function process(object $event)
{
try {
$databases = $this->config->get('databases', []);
$pools = array_keys($databases);
foreach ($pools as $name) {
$this->initTableCollector($name);
}
} catch (\Throwable $throwable) {
$this->logger->error((string) $throwable);
}
}
public function initTableCollector($pool)
{
if ($this->collector->has($pool)) {
return;
}
$connection = $this->container->get(ConnectionResolverInterface::class)->connection($pool);
$columns = $connection->getSchemaBuilder()->getColumns();
foreach ($columns as $column) {
$this->collector->add($pool, $column);
}
}
}

View File

@ -0,0 +1,27 @@
<?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\DbConnection;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class TableCollectorTest extends TestCase
{
public function testExample()
{
$this->assertTrue(true);
}
}

View File

@ -36,6 +36,7 @@ return [
'ttl' => 3600 * 24,
'empty_model_ttl' => 3600,
'load_script' => true,
'use_default_value' => false,
],
],
];

View File

@ -45,6 +45,12 @@ class Config
*/
protected $emptyModelTtl = 60;
/**
* Whether to use default value when resolved from cache.
* @var bool
*/
protected $useDefaultValue = false;
/**
* @var bool
*/
@ -72,6 +78,9 @@ class Config
if (isset($values['empty_model_ttl'])) {
$this->emptyModelTtl = $values['empty_model_ttl'];
}
if (isset($values['use_default_value'])) {
$this->useDefaultValue = (bool) $values['use_default_value'];
}
}
public function getCacheKey(): string
@ -85,6 +94,17 @@ class Config
return $this;
}
public function isUseDefaultValue(): bool
{
return $this->useDefaultValue;
}
public function setUseDefaultValue(bool $useDefaultValue): Config
{
$this->useDefaultValue = $useDefaultValue;
return $this;
}
public function getPrefix(): string
{
return $this->prefix;

View File

@ -15,6 +15,7 @@ namespace Hyperf\ModelCache;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Database\Model\Collection;
use Hyperf\DbConnection\Collector\TableCollector;
use Hyperf\DbConnection\Model\Model;
use Hyperf\ModelCache\Handler\HandlerInterface;
use Hyperf\ModelCache\Handler\RedisHandler;
@ -37,10 +38,17 @@ class Manager
*/
protected $logger;
/**
* @var TableCollector
*/
protected $collector;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
$this->logger = $container->get(StdoutLoggerInterface::class);
$this->collector = $container->get(TableCollector::class);
$config = $container->get(ConfigInterface::class);
if (! $config->has('databases')) {
throw new \InvalidArgumentException('config databases is not exist!');
@ -73,7 +81,9 @@ class Manager
$key = $this->getCacheKey($id, $instance, $handler->getConfig());
$data = $handler->get($key);
if ($data) {
return $instance->newFromBuilder($data);
return $instance->newFromBuilder(
$this->getAttributes($handler->getConfig(), $instance, $data)
);
}
// Fetch it from database, because it not exist in cache handler.
@ -143,7 +153,7 @@ class Manager
}
$map = [];
foreach ($items as $item) {
$map[$item[$primaryKey]] = $item;
$map[$item[$primaryKey]] = $this->getAttributes($handler->getConfig(), $instance, $item);
}
$result = [];
@ -236,4 +246,16 @@ class Manager
return $result;
}
protected function getAttributes(Config $config, Model $model, array $data)
{
if (! $config->isUseDefaultValue()) {
return $data;
}
$defaultData = $this->collector->getDefaultValue(
$model->getConnectionName(),
$model->getTable()
);
return array_replace($defaultData, $data);
}
}

View File

@ -15,6 +15,7 @@ namespace HyperfTest\ModelCache;
use Hyperf\Config\Config;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\DbConnection\Collector\TableCollector;
use Hyperf\Utils\ApplicationContext;
use HyperfTest\ModelCache\Stub\ManagerStub;
use HyperfTest\ModelCache\Stub\ModelStub;
@ -43,6 +44,7 @@ class ManagerTest extends TestCase
$container->shouldReceive('get')->once()->with(ConfigInterface::class)->andReturn(new Config($this->getConfig()));
$container->shouldReceive('make')->with(ContainerInterface::class)->andReturn($container);
$container->shouldReceive('get')->with(EventDispatcherInterface::class)->andReturn(null);
$container->shouldReceive('get')->with(TableCollector::class)->andReturn(new TableCollector());
ApplicationContext::setContainer($container);

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace HyperfTest\ModelCache;
use Hyperf\DbConnection\Listener\InitTableCollectorListener;
use Hyperf\Redis\RedisProxy;
use HyperfTest\ModelCache\Stub\ContainerStub;
use HyperfTest\ModelCache\Stub\UserExtModel;
@ -228,4 +229,25 @@ class ModelCacheTest extends TestCase
$this->assertSame($model->getAttributes(), $model2->getAttributes());
$this->assertEquals(array_keys($model->getAttributes()), array_keys($model3->getAttributes()));
}
public function testWhenAddedNewColumn()
{
$container = ContainerStub::mockContainer();
$listener = new InitTableCollectorListener($container);
$listener->process((object) []);
$model = UserHiddenModel::query()->find(1);
$model->deleteCache();
$model = UserModel::findFromCache(1);
$model = UserModel::findFromCache(1);
$this->assertArrayHasKey('gender', $model->toArray());
/** @var \Redis $redis */
$redis = $container->make(RedisProxy::class, ['pool' => 'default']);
$redis->hDel('{mc:default:m:user}:id:1', 'gender');
$model = UserModel::findFromCache(1);
$this->assertArrayHasKey('gender', $model->toArray());
}
}

View File

@ -18,6 +18,7 @@ use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Database\ConnectionResolverInterface;
use Hyperf\Database\Connectors\ConnectionFactory;
use Hyperf\Database\Connectors\MySqlConnector;
use Hyperf\DbConnection\Collector\TableCollector;
use Hyperf\DbConnection\ConnectionResolver;
use Hyperf\DbConnection\Frequency;
use Hyperf\DbConnection\Pool\DbPool;
@ -43,6 +44,7 @@ class ContainerStub
public static function mockContainer()
{
$container = Mockery::mock(Container::class);
$container->shouldReceive('get')->with(TableCollector::class)->andReturn(new TableCollector());
$factory = new PoolFactory($container);
$container->shouldReceive('get')->with(PoolFactory::class)->andReturn($factory);
@ -81,6 +83,7 @@ class ContainerStub
'ttl' => 3600 * 24,
'empty_model_ttl' => 3600,
'load_script' => true,
'use_default_value' => true,
],
'pool' => [
'min_connections' => 1,