Browse Source

Fixes #3611: Refactored query caching.

tags/2.0.0-rc
Qiang Xue 10 years ago
parent
commit
a3e2b94adf
  1. 95
      docs/guide/caching-data.md
  2. 5
      framework/CHANGELOG.md
  3. 11
      framework/UPGRADE.md
  4. 277
      framework/db/Command.php
  5. 124
      framework/db/Connection.php
  6. 49
      tests/unit/framework/db/CommandTest.php

95
docs/guide/caching-data.md

@ -245,20 +245,15 @@ Query caching requires a [[yii\db\Connection|DB connection]] and a valid `cache`
The basic usage of query caching is as follows, assuming `$db` is a [[yii\db\Connection]] instance:
```php
$duration = 60; // cache query results for 60 seconds.
$dependency = ...; // optional dependency
$db->beginCache($duration, $dependency);
$result = $db->cache(function ($db) {
// ...performs DB queries here...
// the result of the SQL query will be served from the cache
// if query caching is enabled and the query result is found in the cache
return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
$db->endCache();
});
```
As you can see, any SQL queries in between the `beginCache()` and `endCache()` calls will be cached.
If the result of the same query is found valid in the cache, the query will be skipped and the result
will be served from the cache instead.
Query caching can be used for [DAO](db-dao.md) as well as [ActiveRecord](db-active-record.md).
> Info: Some DBMS (e.g. [MySQL](http://dev.mysql.com/doc/refman/5.1/en/query-cache.html))
@ -269,13 +264,85 @@ Query caching can be used for [DAO](db-dao.md) as well as [ActiveRecord](db-acti
### Configurations <a name="query-caching-configs"></a>
Query caching has two configurable options through [[yii\db\Connection]]:
Query caching has three global configurable options through [[yii\db\Connection]]:
* [[yii\db\Connection::enableQueryCache|enableQueryCache]]: whether to turn on or off query caching.
It defaults to false. Note that to effectively turn on query caching, you also need to have a valid
cache, as specified by [[yii\db\Connection::queryCache|queryCache]].
* [[yii\db\Connection::queryCacheDuration|queryCacheDuration]]: this represents the number of seconds
that a query result can remain valid in the cache. The duration will be overwritten if you call
[[yii\db\Connection::beginCache()]] with an explicit duration parameter.
that a query result can remain valid in the cache. You can use 0 to indicate a query result should
remain in the cache forever. This property is the default value used when [[yii\db\Connection::cache()]]
is called without specifying a duration.
* [[yii\db\Connection::queryCache|queryCache]]: this represents the ID of the cache application component.
It defaults to `'cache'`. Query caching is enabled only when there is a valid cache application component.
It defaults to `'cache'`. Query caching is enabled only if there is a valid cache application component.
### Usages <<a name="query-caching-usages"></a>
You can use [[yii\db\Connection::cache()]] if you have multiple SQL queries that need to take advantage of
query caching. The usage is as follows,
```php
$duration = 60; // cache query results for 60 seconds.
$dependency = ...; // optional dependency
$result = $db->cache(function ($db) {
// ... perform SQL queries here ...
return $result;
}, $duration, $dependency);
```
Any SQL queries in the anonymous function will be cached for the specified duration with the specified dependency.
If the result of a query is found valid in the cache, the query will be skipped and the result will be served
from the cache instead. If you do not specify the `$duration` parameter, the value of
[[yii\db\Connection::queryCacheDuration|queryCacheDuration]] will be used instead.
Sometimes within `cache()`, you may want to disable query caching for some particular queries. You can use
[[yii\db\Connection::noCache()]] in this case.
```php
$result = $db->cache(function ($db) {
// SQL queries that use query caching
$db->noCache(function ($db) {
// SQL queries that do not use query caching
});
// ...
return $result;
});
```
If you just want to use query caching for a single query, you can call [[yii\db\Command::cache()]] when building
the command. For example,
```php
// use query caching and set query cache duration to be 60 seconds
$customer = $db->createCommand('SELECT * FROM customer WHERE id=1')->cache(60)->queryOne();
```
You can also use [[yii\db\Command::noCache()]] to disable query caching for a single command. For example,
```php
$result = $db->cache(function ($db) {
// SQL queries that use query caching
// do not use query caching for this command
$customer = $db->createCommand('SELECT * FROM customer WHERE id=1')->noCache()->queryOne();
// ...
return $result;
});
```
### Limitations <a name="query-caching-limitations"></a>

5
framework/CHANGELOG.md

@ -167,6 +167,11 @@ Yii Framework 2 Change Log
- Chg #3383: Added `$type` parameter to `IdentityInterface::findIdentityByAccessToken()` (qiangxue)
- Chg #3531: \yii\grid\GridView now allows any character (except ":") in the attribute part of the shorthand syntax for columns (rawtaz)
- Chg #3544: Added `$key` as a parameter to the callable specified via `yii\grid\DataColumn::value` (mdmunir)
- Chg #3611: Query caching is refactored. (qiangxue)
- `yii\db\Connection::beginCache()` and `endCache()` are removed.
- Added `yii\db\Connection::cache()` and `noCache()`.
- Added `Command::cache()` and `noCache()`.
- `yii\db\Connection::queryCacheDuration` is now used as a default cache duration parameter.
- Chg #3640: All cookies are now httpOnly by default in order to increase overall security (samdark)
- Chg #3687: Default `sourceLanguage` and `language` are now `en-US` in order for i18n formatter to work correctly (samdark)
- Chg #3804: Added `fileinfo` PHP extension to the basic requirement of Yii (Ragazzo)

11
framework/UPGRADE.md

@ -161,3 +161,14 @@ new ones save the following code as `convert.php` that should be placed in the s
// ...
];
```
* If you are using query caching, you should modify your relevant code as follows, as `beginCache()` and `endCache()` are
replaced by `cache()`:
```php
$db->cache(function ($db) {
// ... SQL queries that need to use query caching
}, $duration, $dependency);
```

277
framework/db/Command.php

@ -8,8 +8,8 @@
namespace yii\db;
use Yii;
use yii\base\Component;
use yii\base\NotSupportedException;
use yii\caching\Cache;
/**
* Command represents a SQL statement to be executed against a database.
@ -51,7 +51,7 @@ use yii\caching\Cache;
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Command extends \yii\base\Component
class Command extends Component
{
/**
* @var Connection the DB connection that this command is associated with
@ -73,6 +73,19 @@ class Command extends \yii\base\Component
*/
public $params = [];
/**
* @var integer the default number of seconds that query results can remain valid in cache.
* Use 0 to indicate that the cached data will never expire. And use a negative number to indicate
* query cache should not be used.
* @see cache()
*/
public $queryCacheDuration;
/**
* @var \yii\caching\Dependency the dependency to be associated with the cached query result for this command
* @see cache()
*/
public $queryCacheDependency;
/**
* @var array pending parameters to be bound to the current PDO statement.
*/
private $_pendingParams = [];
@ -81,6 +94,32 @@ class Command extends \yii\base\Component
*/
private $_sql;
/**
* Enables query cache for this command.
* @param integer $duration the number of seconds that query result of this command can remain valid in the cache.
* If this is not set, the value of [[Connection::queryCacheDuration]] will be used instead.
* Use 0 to indicate that the cached data will never expire.
* @param \yii\caching\Dependency $dependency the cache dependency associated with the cached query result.
* @return static the command object itself
*/
public function cache($duration = null, $dependency = null)
{
$this->queryCacheDuration = $duration === null ? $this->db->queryCacheDuration : $duration;
$this->queryCacheDependency = $dependency;
return $this;
}
/**
* Disables query cache for this command.
* @return static the command object itself
*/
public function noCache()
{
$this->queryCacheDuration = -1;
return $this;
}
/**
* Returns the SQL statement for this command.
* @return string the SQL statement to be executed
@ -155,6 +194,7 @@ class Command extends \yii\base\Component
public function prepare($forRead = null)
{
if ($this->pdoStatement) {
$this->bindPendingParams();
return;
}
@ -172,6 +212,7 @@ class Command extends \yii\base\Component
try {
$this->pdoStatement = $pdo->prepare($sql);
$this->bindPendingParams();
} catch (\Exception $e) {
$message = $e->getMessage() . "\nFailed to prepare SQL: $sql";
$errorInfo = $e instanceof \PDOException ? $e->errorInfo : null;
@ -205,8 +246,6 @@ class Command extends \yii\base\Component
{
$this->prepare();
$this->bindPendingParams();
if ($dataType === null) {
$dataType = $this->db->getSchema()->getPdoType($value);
}
@ -288,44 +327,6 @@ class Command extends \yii\base\Component
}
/**
* Executes the SQL statement.
* This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs.
* No result set will be returned.
* @return integer number of rows affected by the execution.
* @throws Exception execution failed
*/
public function execute()
{
$sql = $this->getSql();
$rawSql = $this->getRawSql();
Yii::info($rawSql, __METHOD__);
if ($sql == '') {
return 0;
}
$this->prepare(false);
$this->bindPendingParams();
$token = $rawSql;
try {
Yii::beginProfile($token, __METHOD__);
$this->pdoStatement->execute();
$n = $this->pdoStatement->rowCount();
Yii::endProfile($token, __METHOD__);
return $n;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw $this->db->getSchema()->convertException($e, $rawSql);
}
}
/**
* Executes the SQL statement and returns query result.
* This method is for executing a SQL query that returns result set, such as `SELECT`.
* @return DataReader the reader object for fetching the query result
@ -393,74 +394,6 @@ class Command extends \yii\base\Component
}
/**
* Performs the actual DB query of a SQL statement.
* @param string $method method of PDOStatement to be called
* @param integer $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php)
* for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used.
* @return mixed the method execution result
* @throws Exception if the query causes any problem
*/
private function queryInternal($method, $fetchMode = null)
{
$db = $this->db;
$rawSql = $this->getRawSql();
Yii::info($rawSql, 'yii\db\Command::query');
/* @var $cache \yii\caching\Cache */
if ($db->enableQueryCache && $method !== '') {
$cache = is_string($db->queryCache) ? Yii::$app->get($db->queryCache, false) : $db->queryCache;
}
if (isset($cache) && $cache instanceof Cache) {
$cacheKey = [
__CLASS__,
$method,
$db->dsn,
$db->username,
$rawSql,
];
if (($result = $cache->get($cacheKey)) !== false) {
Yii::trace('Query result served from cache', 'yii\db\Command::query');
return $result;
}
}
$this->prepare(true);
$this->bindPendingParams();
$token = $rawSql;
try {
Yii::beginProfile($token, 'yii\db\Command::query');
$this->pdoStatement->execute();
if ($method === '') {
$result = new DataReader($this);
} else {
if ($fetchMode === null) {
$fetchMode = $this->fetchMode;
}
$result = call_user_func_array([$this->pdoStatement, $method], (array) $fetchMode);
$this->pdoStatement->closeCursor();
}
Yii::endProfile($token, 'yii\db\Command::query');
if (isset($cache, $cacheKey) && $cache instanceof Cache) {
$cache->set($cacheKey, $result, $db->queryCacheDuration, $db->queryCacheDependency);
Yii::trace('Saved query result in cache', 'yii\db\Command::query');
}
return $result;
} catch (\Exception $e) {
Yii::endProfile($token, 'yii\db\Command::query');
throw $this->db->getSchema()->convertException($e, $rawSql);
}
}
/**
* Creates an INSERT command.
* For example,
*
@ -806,4 +739,128 @@ class Command extends \yii\base\Component
return $this->setSql($sql);
}
/**
* Executes the SQL statement.
* This method should only be used for executing non-query SQL statement, such as `INSERT`, `DELETE`, `UPDATE` SQLs.
* No result set will be returned.
* @return integer number of rows affected by the execution.
* @throws Exception execution failed
*/
public function execute()
{
$sql = $this->getSql();
$rawSql = $this->getRawSql();
Yii::info($rawSql, __METHOD__);
if ($sql == '') {
return 0;
}
$this->prepare(false);
$token = $rawSql;
try {
Yii::beginProfile($token, __METHOD__);
$this->pdoStatement->execute();
$n = $this->pdoStatement->rowCount();
Yii::endProfile($token, __METHOD__);
return $n;
} catch (\Exception $e) {
Yii::endProfile($token, __METHOD__);
throw $this->db->getSchema()->convertException($e, $rawSql);
}
}
/**
* Returns the effective query cache information.
* @return array the current query cache information, or null if query cache is not used.
*/
private function getQueryCacheInfo()
{
$info = $this->db->getQueryCacheInfo();
if (is_array($info)) {
if ($this->queryCacheDuration !== null) {
$info[1] = $this->queryCacheDuration;
}
if ($this->queryCacheDependency !== null) {
$info[2] = $this->queryCacheDependency;
}
if ($info[1] !== null && $info[1] >= 0) {
return $info;
}
}
return null;
}
/**
* Performs the actual DB query of a SQL statement.
* @param string $method method of PDOStatement to be called
* @param integer $fetchMode the result fetch mode. Please refer to [PHP manual](http://www.php.net/manual/en/function.PDOStatement-setFetchMode.php)
* for valid fetch modes. If this parameter is null, the value set in [[fetchMode]] will be used.
* @return mixed the method execution result
* @throws Exception if the query causes any problem
*/
private function queryInternal($method, $fetchMode = null)
{
$rawSql = $this->getRawSql();
Yii::info($rawSql, 'yii\db\Command::query');
if ($method !== '') {
$info = $this->getQueryCacheInfo();
if (is_array($info)) {
/* @var $cache \yii\caching\Cache */
$cache = $info[0];
$cacheKey = [
__CLASS__,
$method,
$this->db->dsn,
$this->db->username,
$rawSql,
];
if (($result = $cache->get($cacheKey)) !== false) {
Yii::trace('Query result served from cache', 'yii\db\Command::query');
return $result;
}
}
}
$this->prepare(true);
$token = $rawSql;
try {
Yii::beginProfile($token, 'yii\db\Command::query');
$this->pdoStatement->execute();
if ($method === '') {
$result = new DataReader($this);
} else {
if ($fetchMode === null) {
$fetchMode = $this->fetchMode;
}
$result = call_user_func_array([$this->pdoStatement, $method], (array) $fetchMode);
$this->pdoStatement->closeCursor();
}
Yii::endProfile($token, 'yii\db\Command::query');
} catch (\Exception $e) {
Yii::endProfile($token, 'yii\db\Command::query');
$this->db->resetQueryCacheInfo();
throw $this->db->getSchema()->convertException($e, $rawSql);
}
if (isset($cache, $cacheKey, $info)) {
$cache->set($cacheKey, $result, $info[1], $info[2]);
Yii::trace('Saved query result in cache', 'yii\db\Command::query');
}
return $result;
}
}

124
framework/db/Connection.php

@ -204,30 +204,21 @@ class Connection extends Component
* @var boolean whether to enable query caching.
* Note that in order to enable query caching, a valid cache component as specified
* by [[queryCache]] must be enabled and [[enableQueryCache]] must be set true.
*
* Methods [[beginCache()]] and [[endCache()]] can be used as shortcuts to turn on
* and off query caching on the fly.
* @see queryCacheDuration
* @see queryCache
* @see queryCacheDependency
* @see beginCache()
* @see endCache()
* @see cache()
* @see noCache()
*/
public $enableQueryCache = false;
/**
* @var integer number of seconds that query results can remain valid in cache.
* Defaults to 3600, meaning 3600 seconds, or one hour.
* @var integer the default number of seconds that query results can remain valid in cache.
* Use 0 to indicate that the cached data will never expire.
* Defaults to 3600, meaning 3600 seconds, or one hour. Use 0 to indicate that the cached data will never expire.
* The value of this property will be used when [[cache()]] is called without a cache duration.
* @see enableQueryCache
* @see cache()
*/
public $queryCacheDuration = 3600;
/**
* @var \yii\caching\Dependency the dependency that will be used when saving query results into cache.
* Defaults to null, meaning no dependency.
* @see enableQueryCache
*/
public $queryCacheDependency;
/**
* @var Cache|string the cache object or the ID of the cache application component
* that is used for query caching.
* @see enableQueryCache
@ -369,6 +360,10 @@ class Connection extends Component
* @var Connection the currently active slave connection
*/
private $_slave = false;
/**
* @var array query cache parameters for the [[cache()]] calls
*/
private $_queryCacheInfo = [];
/**
@ -381,29 +376,98 @@ class Connection extends Component
}
/**
* Turns on query caching.
* This method is provided as a shortcut to setting two properties that are related
* with query caching: [[queryCacheDuration]] and [[queryCacheDependency]].
* @param integer $duration the number of seconds that query results may remain valid in cache.
* If not set, it will use the value of [[queryCacheDuration]]. See [[queryCacheDuration]] for more details.
* @param \yii\caching\Dependency $dependency the dependency for the cached query result.
* See [[queryCacheDependency]] for more details.
* Uses query cache for the queries performed with the callable.
* When query caching is enabled ([[enableQueryCache]] is true and [[queryCache]] refers to a valid cache),
* queries performed within the callable will be cached and their results will be fetched from cache if available.
* For example,
*
* ```php
* // The customer will be fetched from cache if available.
* // If not, the query will be made against DB and cached for use next time.
* $customer = $db->cache(function (Connection $db) {
* return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
* });
* ```
*
* Note that query cache is only meaningful for queries that return results. For queries performed with
* [[Command::execute()]], query cache will not be used.
*
* @param callable $callable a PHP callable that contains DB queries which will make use of query cache.
* The signature of the callable is `function (Connection $db)`.
* @param integer $duration the number of seconds that query results can remain valid in the cache. If this is
* not set, the value of [[queryCacheDuration]] will be used instead.
* Use 0 to indicate that the cached data will never expire.
* @param \yii\caching\Dependency $dependency the cache dependency associated with the cached query results.
* @return mixed the return result of the callable
* @see enableQueryCache
* @see queryCache
* @see noCache()
*/
public function beginCache($duration = null, $dependency = null)
public function cache(callable $callable, $duration = null, $dependency = null)
{
$this->enableQueryCache = true;
if ($duration !== null) {
$this->queryCacheDuration = $duration;
$this->_queryCacheInfo[] = [$duration === null ? $this->queryCacheDuration : $duration, $dependency];
$result = call_user_func($callable, $this);
array_pop($this->_queryCacheInfo);
return $result;
}
/**
* Disables query cache temporarily.
* Queries performed within the callable will not use query cache at all. For example,
*
* ```php
* $db->cache(function (Connection $db) {
*
* // ... queries that use query cache ...
*
* return $db->noCache(function (Connection $db) {
* // this query will not use query cache
* return $db->createCommand('SELECT * FROM customer WHERE id=1')->queryOne();
* });
* });
* ```
*
* @param callable $callable a PHP callable that contains DB queries which should not use query cache.
* The signature of the callable is `function (Connection $db)`.
* @return mixed the return result of the callable
* @see enableQueryCache
* @see queryCache
* @see cache()
*/
public function noCache(callable $callable)
{
$this->_queryCacheInfo[] = false;
$result = call_user_func($callable, $this);
array_pop($this->_queryCacheInfo);
return $result;
}
/**
* Returns the current query cache information.
* This method is used internally by [[Command]].
* @return array the current query cache information, or null if query cache is not enabled.
* @internal
*/
public function getQueryCacheInfo()
{
$info = end($this->_queryCacheInfo);
if ($this->enableQueryCache) {
$cache = $this->queryCache instanceof Cache ? $this->queryCache : Yii::$app->get($this->queryCache, false);
if ($cache instanceof Cache) {
return is_array($info) ? [$cache, $info[0], $info[1]] : [$cache, null, null];
}
}
$this->queryCacheDependency = $dependency;
return null;
}
/**
* Turns off query caching.
* Cleans up the query cache information.
* This method is used internally by [[Command]].
* @internal
*/
public function endCache()
public function resetQueryCacheInfo()
{
$this->enableQueryCache = false;
$this->_queryCacheInfo = [];
}
/**

49
tests/unit/framework/db/CommandTest.php

@ -2,6 +2,8 @@
namespace yiiunit\framework\db;
use yii\caching\FileCache;
use yii\db\Connection;
use yii\db\DataReader;
/**
@ -240,6 +242,7 @@ class CommandTest extends DatabaseTestCase
$this->assertEquals(2, $command->execute());
}
/*
public function testInsert()
{
}
@ -299,6 +302,7 @@ class CommandTest extends DatabaseTestCase
public function testDropIndex()
{
}
*/
public function testIntegrityViolation()
{
@ -311,4 +315,49 @@ class CommandTest extends DatabaseTestCase
$command->execute();
$command->execute();
}
public function testQueryCache()
{
$db = $this->getConnection();
$db->enableQueryCache = true;
$db->queryCache = new FileCache(['cachePath' => '@yiiunit/runtime/cache']);
$command = $db->createCommand('SELECT name FROM customer WHERE id=:id');
$this->assertEquals('user1', $command->bindValue(':id', 1)->queryScalar());
$update = $db->createCommand('UPDATE customer SET name=:name WHERE id=:id');
$update->bindValues([':id' => 1, ':name' => 'user11'])->execute();
$this->assertEquals('user11', $command->bindValue(':id', 1)->queryScalar());
$db->cache(function (Connection $db) use ($command, $update) {
$this->assertEquals('user2', $command->bindValue(':id', 2)->queryScalar());
$update->bindValues([':id' => 2, ':name' => 'user22'])->execute();
$this->assertEquals('user2', $command->bindValue(':id', 2)->queryScalar());
$db->noCache(function () use ($command) {
$this->assertEquals('user22', $command->bindValue(':id', 2)->queryScalar());
});
$this->assertEquals('user2', $command->bindValue(':id', 2)->queryScalar());
}, 10);
$db->enableQueryCache = false;
$db->cache(function ($db) use ($command, $update) {
$this->assertEquals('user22', $command->bindValue(':id', 2)->queryScalar());
$update->bindValues([':id' => 2, ':name' => 'user2'])->execute();
$this->assertEquals('user2', $command->bindValue(':id', 2)->queryScalar());
}, 10);
$db->enableQueryCache = true;
$command = $db->createCommand('SELECT name FROM customer WHERE id=:id')->cache();
$this->assertEquals('user11', $command->bindValue(':id', 1)->queryScalar());
$update->bindValues([':id' => 1, ':name' => 'user1'])->execute();
$this->assertEquals('user11', $command->bindValue(':id', 1)->queryScalar());
$this->assertEquals('user1', $command->noCache()->bindValue(':id', 1)->queryScalar());
$command = $db->createCommand('SELECT name FROM customer WHERE id=:id');
$db->cache(function (Connection $db) use ($command, $update) {
$this->assertEquals('user11', $command->bindValue(':id', 1)->queryScalar());
$this->assertEquals('user1', $command->noCache()->bindValue(':id', 1)->queryScalar());
}, 10);
}
}

Loading…
Cancel
Save