Кэширование в ORM
Каждое приложение уникально, у нас могут быть модели, данные которых часто изменяются, и другие, которые редко изменяются. Доступ к базам данных часто является одной из самых распространенных узких мест с точки зрения производительности. Это связано со сложными процессами подключения/общения, которые PHP должен выполнять в каждом запросе для получения данных из базы данных. Поэтому, если мы хотим добиться хорошей производительности, нам необходимо добавить несколько уровней кэширования там, где приложение этого требует.
В этой главе объясняются возможные точки, в которых можно реализовать кэширование для повышения производительности. Фреймворк предоставляет инструменты для реализации кэша там, где вы этого хотите, в соответствии с архитектурой вашего приложения.
Кэширование наборов результатов
Хорошо зарекомендовавший себя метод для избежания постоянного доступа к базе данных — кэширование наборов результатов, которые не меняются часто, с использованием системы с более быстрым доступом (обычно память).
Когда Phalcon\Mvc\Model требует службы для кэширования наборов результатов, он запрашивает её у контейнера Dependency Injector с согласованным именем «modelsCache».
Поскольку Phalcon предоставляет компонент для кэширования любых данных, мы объясним, как интегрировать его с моделями. Сначала необходимо зарегистрировать его как службу в контейнере служб:
//Set the models cache service
$di->set('modelsCache', function() {
//Cache data for one day by default
$frontCache = new \Phalcon\Cache\Frontend\Data(array(
"lifetime" => 86400
));
//Memcached connection settings
$cache = new \Phalcon\Cache\Backend\Memcache($frontCache, array(
"host" => "localhost",
"port" => "11211"
));
return $cache;
});
У вас есть полный контроль над созданием и настройкой кэша перед использованием, регистрируя службу как анонимную функцию. После правильной настройки кэша можно кэшировать наборы результатов следующим образом:
// Get products without caching
$products = Products::find();
// Just cache the resultset. The cache will expire in 1 hour (3600 seconds)
$products = Products::find(array(
"cache" => array("key" => "my-cache")
));
// Cache the resultset for only for 5 minutes
$products = Products::find(array(
"cache" => array("key" => "my-cache", "lifetime" => 300)
));
// Using a custom cache
$products = Products::find(array("cache" => $myCache));
Кэширование также можно применить к наборам результатов, сгенерированным с использованием отношений:
// Query some post
$post = Post::findFirst();
// Get comments related to a post, also cache it
$comments = $post->getComments(array(
"cache" => array("key" => "my-key")
));
// Get comments related to a post, setting lifetime
$comments = $post->getComments(array(
"cache" => array("key" => "my-key", "lifetime" => 3600)
));
Когда кэшированный набор результатов нужно сделать недействительным, можно просто удалить его из кэша, используя указанный ранее ключ.
Обратите внимание, что не все наборы результатов должны быть кэшированы. Результаты, которые очень часто меняются, не должны кэшироваться, поскольку они быстро становятся недействительными, и кэширование в этом случае влияет на производительность. Кроме того, большие наборы данных, которые не меняются часто, могут быть кэшированы, но это решение, которое разработчик должен принять на основе доступного механизма кэширования и того, приемлемо ли влияние на производительность простого извлечения этих данных в первую очередь.
Переопределение find/findFirst
Как показано выше, эти методы доступны в моделях, которые наследуют от Phalcon\Mvc\Model:
class Robots extends Phalcon\Mvc\Model
{
public static function find($parameters=null)
{
return parent::find($parameters);
}
public static function findFirst($parameters=null)
{
return parent::findFirst($parameters);
}
}
Таким образом, вы перехватываете все вызовы этих методов, позволяя добавлять уровень кэширования или выполнять запрос, если кэша нет. Например, очень простая реализация кэша использует статическое свойство, чтобы избежать многократного запроса записи в одном запросе:
class Robots extends Phalcon\Mvc\Model
{
protected static $_cache = array();
/**
* Implement a method that returns a string key based
* on the query parameters
*/
protected static function _createKey($parameters)
{
$uniqueKey = array();
foreach ($parameters as $key => $value) {
if (is_scalar($value)) {
$uniqueKey[] = $key . ':' . $value;
} else {
if (is_array($value)) {
$uniqueKey[] = $key . ':[' . self::_createKey($value) .']';
}
}
}
return join(',', $uniqueKey);
}
public static function find($parameters=null)
{
//Create an unique key based on the parameters
$key = self::_createKey($parameters);
if (!isset(self::$_cache[$key])) {
//Store the result in the memory cache
self::$_cache[$key] = parent::find($parameters);
}
//Return the result in the cache
return self::$_cache[$key];
}
public static function findFirst($parameters=null)
{
// ...
}
}
Обращение к базе данных многократно медленнее, чем вычисление ключа кэша, вы можете свободно реализовывать стратегию генерации ключа, которая вам больше подходит. Обратите внимание, что хороший ключ максимально избегает коллизий, что означает, что разные ключи возвращают не связанные записи параметрам поиска.
В приведенном выше примере мы использовали кэш в памяти, он полезен как кэш первого уровня. После того, как у нас есть кэш в памяти, мы можем реализовать кэш второго уровня, например, APC/XCache или базу данных NoSQL:
public static function find($parameters=null)
{
//Create an unique key based on the parameters
$key = self::_createKey($parameters);
if (!isset(self::$_cache[$key])) {
//We're using APC as second cache
if (apc_exists($key)) {
$data = apc_fetch($key);
//Store the result in the memory cache
self::$_cache[$key] = $data;
return $data;
}
//There are no memory or apc cache
$data = parent::find($parameters);
//Store the result in the memory cache
self::$_cache[$key] = $data;
//Store the result in APC
apc_store($key, $data);
return $data;
}
//Return the result in the cache
return self::$_cache[$key];
}
Это предоставляет вам полный контроль над тем, как следует реализовывать кэши для каждой модели, если эта стратегия общая для нескольких моделей, вы можете создать базовый класс для всех из них:
class CacheableModel extends Phalcon\Mvc\Model
{
protected static function _createKey($parameters)
{
// .. create a cache key based on the parameters
}
public static function find($parameters=null)
{
//.. custom caching strategy
}
public static function findFirst($parameters=null)
{
//.. custom caching strategy
}
}
Затем используйте этот класс в качестве базового класса для каждой модели «Cacheable»:
class Robots extends CacheableModel
{
}
Принудительное кэширование
Ранее мы видели, как Phalcon\Mvc\Model имеет встроенную интеграцию с компонентом кэширования, предоставляемым фреймворком. Чтобы сделать запись/набор результатов кэшируемыми, мы передаём ключ «cache» в массив параметров:
// Cache the resultset for only for 5 minutes
$products = Products::find(array(
"cache" => array("key" => "my-cache", "lifetime" => 300)
));
Это даёт нам свободу кэширования конкретных запросов, однако, если мы хотим глобально кэшировать каждый запрос, выполненный над моделью, мы можем переопределить метод find/findFirst, чтобы принудительно кэшировать каждый запрос:
class Robots extends Phalcon\Mvc\Model
{
protected static function _createKey($parameters)
{
// .. create a cache key based on the parameters
}
public static function find($parameters=null)
{
//Convert the parameters to an array
if (!is_array($parameters)) {
$parameters = array($parameters);
}
//Check if a cache key wasn't passed
//and create the cache parameters
if (!isset($parameters['cache'])) {
$parameters['cache'] = array(
"key" => self::_createKey($parameters),
"lifetime" => 300
);
}
return parent::find($parameters);
}
public static function findFirst($parameters=null)
{
//...
}
}
Кэширование запросов PHQL
Все запросы в ORM, независимо от уровня синтаксиса, используемого для их создания, обрабатываются в рамках PHQL. Этот язык даёт вам гораздо большую свободу в создании запросов любого типа. Конечно, эти запросы можно кэшировать:
$phql = "SELECT * FROM Cars WHERE name = :name:";
$query = $this->modelsManager->createQuery($phql);
$query->cache(array(
"key" => "cars-by-name",
"lifetime" => 300
));
$cars = $query->execute(array(
'name' => 'Audi'
));
Если вы не хотите использовать неявный кэш, просто сохраните результат в своём любимом кэше:
$phql = "SELECT * FROM Cars WHERE name = :name:";
$cars = $this->modelsManager->executeQuery($phql, array(
'name' => 'Audi'
));
apc_store('my-cars', $cars);
Многократно используемые связанные записи
Некоторые модели могут иметь отношения с другими моделями. Это позволяет легко проверять записи, которые относятся к экземплярам в памяти:
//Get some invoice $invoice = Invoices::findFirst(); //Get the customer related to the invoice $customer = $invoice->customer; //Print his/her name echo $customer->name, "\n";
Этот пример очень простой: запрашивается клиент и может использоваться по мере необходимости, например, для отображения его имени. Это также относится, если мы получаем набор счетов-фактур, чтобы показать клиентов, соответствующих этим счетам-фактурам:
//Get a set of invoices
// SELECT * FROM invoices;
foreach (Invoices::find() as $invoice) {
//Get the customer related to the invoice
// SELECT * FROM customers WHERE id = ?;
$customer = $invoice->customer;
//Print his/her name
echo $customer->name, "\n";
}
У клиента может быть один или несколько счетов, что означает, что клиент может быть ненужно запрошен несколько раз. Чтобы этого избежать, мы можем отметить отношение как многократно используемое, таким образом, мы сообщим ORM автоматически повторно использовать записи вместо многократного их запроса:
class Invoices extends \Phalcon\Mvc\Model
{
public function initialize()
{
$this->belongsTo("customers_id", "Customer", "id", array(
'reusable' => true
));
}
}
Этот кэш работает только в памяти, что означает, что кэшированные данные освобождаются при завершении запроса. Вы можете добавить более сложный кэш для этого сценария, переопределив менеджер моделей:
class CustomModelsManager extends \Phalcon\Mvc\Model\Manager
{
/**
* Returns a reusable object from the cache
*
* @param string $modelName
* @param string $key
* @return object
*/
public function getReusableRecords($modelName, $key){
//If the model is Products use the APC cache
if ($modelName == 'Products'){
return apc_fetch($key);
}
//For the rest, use the memory cache
return parent::getReusableRecords($modelName, $key);
}
/**
* Stores a reusable record in the cache
*
* @param string $modelName
* @param string $key
* @param mixed $records
*/
public function setReusableRecords($modelName, $key, $records){
//If the model is Products use the APC cache
if ($modelName == 'Products'){
apc_store($key, $records);
return;
}
//For the rest, use the memory cache
parent::setReusableRecords($modelName, $key, $records);
}
}
Не забудьте зарегистрировать настраиваемого менеджера моделей в DI:
$di->setShared('modelsManager', function() {
return new CustomModelsManager();
});
Кэширование связанных записей
Когда запрашивается связанная запись, ORM внутренне создаёт соответствующее условие и получает необходимые записи с помощью find/findFirst в целевой модели в соответствии со следующей таблицей:
| Тип | Описание | Неявный метод |
|---|---|
| Belongs-To | Возвращает экземпляр модели связанной записи напрямую | findFirst |
| Has-One | Возвращает экземпляр модели связанной записи напрямую | findFirst |
| Has-Many | Возвращает коллекцию экземпляров моделей целевой модели | find |
Это означает, что при получении связанной записи вы можете перехватить способ получения этих данных, реализовав соответствующий метод:
//Get some invoice
$invoice = Invoices::findFirst();
//Get the customer related to the invoice
$customer = $invoice->customer; // Invoices::findFirst('...');
//Same as above
$customer = $invoice->getCustomer(); // Invoices::findFirst('...');
Соответственно, мы могли бы заменить метод findFirst в модели счетов-фактур и реализовать наиболее подходящий кэш:
class Invoices extends Phalcon\Mvc\Model
{
public static function findFirst($parameters=null)
{
//.. custom caching strategy
}
}
Рекурсивное кэширование связанных записей
В этом сценарии мы предполагаем, что при каждом запросе результата мы также получаем связанные записи. Если мы храним найденные записи вместе со связанными сущностями, возможно, мы сможем немного сократить затраты на получение всех сущностей:
class Invoices extends Phalcon\Mvc\Model
{
protected static function _createKey($parameters)
{
// .. create a cache key based on the parameters
}
protected static function _getCache($key)
{
// returns data from a cache
}
protected static function _setCache($key)
{
// stores data in the cache
}
public static function find($parameters=null)
{
//Create a unique key
$key = self::_createKey($parameters);
//Check if there are data in the cache
$results = self::_getCache($key);
// Valid data is an object
if (is_object($results)) {
return $results;
}
$results = array();
$invoices = parent::find($parameters);
foreach ($invoices as $invoice) {
//Query the related customer
$customer = $invoice->customer;
//Assign it to the record
$invoice->customer = $customer;
$results[] = $invoice;
}
//Store the invoices in the cache + their customers
self::_setCache($key, $results);
return $results;
}
public function initialize()
{
// add relations and initialize other stuff
}
}
Получение счетов-фактур из кэша уже получает данные клиента одним запросом, снижая общую нагрузку операции. Обратите внимание, что этот процесс также можно выполнить с PHQL, используя альтернативное решение:
class Invoices extends \Phalcon\Mvc\Model
{
public function initialize()
{
// add relations and initialize other stuff
}
protected static function _createKey($conditions, $params)
{
// .. create a cache key based on the parameters
}
public function getInvoicesCustomers($conditions, $params=null)
{
$phql = "SELECT Invoices.*, Customers.*
FROM Invoices JOIN Customers WHERE " . $conditions;
$query = $this->getModelsManager()->executeQuery($phql);
$query->cache(array(
"key" => self::_createKey($conditions, $params),
"lifetime" => 300
));
return $query->execute($params);
}
}
Кэширование на основе условий
В этом сценарии кэш реализуется условно в зависимости от текущих условий. В зависимости от диапазона, в котором находится первичный ключ, мы выбираем другой бэкенд кэша:
| Тип | Бэкенд кэша |
|---|---|
| 1 - 10000 | mongo1 |
| 10000 - 20000 | mongo2 |
| > 20000 | mongo3 |
Самый простой способ — добавить статический метод в модель, который выбирает правильный кэш для использования:
class Robots extends \Phalcon\Mvc\Model
{
public static function queryCache($initial, $final)
{
if ($initial >= 1 && $final < 10000) {
return self::find(array(
'id >= ' . $initial . ' AND id <= '.$final,
'cache' => array('service' => 'mongo1')
));
}
if ($initial >= 10000 && $final <= 20000) {
return self::find(array(
'id >= ' . $initial . ' AND id <= '.$final,
'cache' => array('service' => 'mongo2')
));
}
if ($initial > 20000) {
return self::find(array(
'id >= ' . $initial,
'cache' => array('service' => 'mongo3')
));
}
}
}
Этот подход решает проблему, однако, если мы хотим добавить другие параметры, такие как заказы или условия, нам придётся создать более сложный метод. Кроме того, этот метод не работает, если данные получены с использованием связанных записей или find/findFirst:
$robots = Robots::find('id < 1000');
$robots = Robots::find('id > 100 AND type = "A"');
$robots = Robots::find('(id > 100 AND type = "A") AND id < 2000');
$robots = Robots::find(array(
'(id > ?0 AND type = "A") AND id < ?1',
'bind' => array(100, 2000),
'order' => 'type'
));
Для достижения этого нам нужно перехватить промежуточное представление (IR), сгенерированное парсером PHQL, и таким образом настроить кэш максимально возможным образом:
Первое — создать пользовательский билдер, чтобы мы могли сгенерировать полностью настраиваемый запрос:
class CustomQueryBuilder extends Phalcon\Mvc\Model\Query\Builder
{
public function getQuery()
{
$query = new CustomQuery($this->getPhql());
$query->setDI($this->getDI());
return $query;
}
}
Вместо непосредственного возвращения Phalcon\Mvc\Model\Query, наш пользовательский билдер возвращает экземпляр CustomQuery, этот класс выглядит так:
class CustomQuery extends Phalcon\Mvc\Model\Query
{
/**
* The execute method is overridden
*/
public function execute($params=null, $types=null)
{
//Parse the intermediate representation for the SELECT
$ir = $this->parse();
//Check if the query has conditions
if (isset($ir['where'])) {
//The fields in the conditions can have any order
//We need to recursively check the conditions tree
//to find the info we're looking for
$visitor = new CustomNodeVisitor();
//Recursively visits the nodes
$visitor->visit($ir['where']);
$initial = $visitor->getInitial();
$final = $visitor->getFinal();
//Select the cache according to the range
//...
//Check if the cache has data
//...
}
//Execute the query
$result = $this->_executeSelect($ir, $params, $types);
//cache the result
//...
return $result;
}
}
Реализация помощника (CustomNodeVisitor), который рекурсивно проверяет условия, ища поля, которые указывают возможный диапазон для использования в кэше:
class CustomNodeVisitor
{
protected $_initial = 0;
protected $_final = 25000;
public function visit($node)
{
switch ($node['type']) {
case 'binary-op':
$left = $this->visit($node['left']);
$right = $this->visit($node['right']);
if (!$left || !$right) {
return false;
}
if ($left=='id') {
if ($node['op'] == '>') {
$this->_initial = $right;
}
if ($node['op'] == '=') {
$this->_initial = $right;
}
if ($node['op'] == '>=') {
$this->_initial = $right;
}
if ($node['op'] == '<') {
$this->_final = $right;
}
if ($node['op'] == '<=') {
$this->_final = $right;
}
}
break;
case 'qualified':
if ($node['name'] == 'id') {
return 'id';
}
break;
case 'literal':
return $node['value'];
default:
return false;
}
}
public function getInitial()
{
return $this->_initial;
}
public function getFinal()
{
return $this->_final;
}
}
Наконец, мы можем заменить метод find в модели Robots, чтобы использовать созданные нами пользовательские классы:
class Robots extends Phalcon\Mvc\Model
{
public static function find($parameters=null)
{
if (!is_array($parameters)) {
$parameters = array($parameters);
}
$builder = new CustomQueryBuilder($parameters);
$builder->from(get_called_class());
if (isset($parameters['bind'])) {
return $builder->getQuery()->execute($parameters['bind']);
} else {
return $builder->getQuery()->execute();
}
}
}
Кэширование планов PHQL
Так же, как и большинство современных систем баз данных, PHQL внутренне кэширует план выполнения. Если один и тот же запрос выполняется несколько раз, PHQL повторно использует ранее сгенерированный план, повышая производительность. Для лучшего использования этого разработчикам рекомендуется создавать все SQL-запросы, передавая переменные параметры как связанные параметры:
for ($i = 1; $i <= 10; $i++) {
$phql = "SELECT * FROM Store\Robots WHERE id = " . $i;
$robots = $this->modelsManager->executeQuery($phql);
//...
}
В приведенном выше примере было сгенерировано десять планов, увеличивая использование памяти и обработку в приложении. Переписывание кода для использования связанных параметров уменьшает обработку как для ORM, так и для системы баз данных:
$phql = "SELECT * FROM Store\Robots WHERE id = ?0";
for ($i = 1; $i <= 10; $i++) {
$robots = $this->modelsManager->executeQuery($phql, array($i));
//...
}
Производительность также можно повысить, повторно используя запрос PHQL:
$phql = "SELECT * FROM Store\Robots WHERE id = ?0";
$query = $this->modelsManager->createQuery($phql);
for ($i = 1; $i <= 10; $i++) {
$robots = $query->execute($phql, array($i));
//...
}
Планы выполнения запросов, включающие подготовленные заявления, также кэшируются большинством систем баз данных, что сокращает общее время выполнения, а также защищает ваше приложение от SQL-инъекций.
© 2011–2016 Phalcon Framework Team
Licensed under the Creative Commons Attribution License 3.0.
https://docs.phalconphp.com/en/2.0.0/reference/models-cache.html