Spec-Zone.ru › Phalcon 3

Кэширование в ORM

Каждое приложение уникально; у нас могут быть модели, данные которых часто меняются, и другие, которые редко меняются. Доступ к базам данных часто является одной из самых частых узких мест с точки зрения производительности. Это связано со сложными процессами подключения/общения, которые PHP должен выполнять в каждом запросе для получения данных из базы данных. Поэтому, если мы хотим добиться хорошей производительности, нам нужно добавить некоторые уровни кэширования там, где это необходимо для приложения.

В этой главе объясняются возможные точки, где можно реализовать кэширование для повышения производительности. Фреймворк предоставляет инструменты для реализации кэша там, где это необходимо, в соответствии с архитектурой вашего приложения.

Кэширование наборов результатов

Хорошо зарекомендовавший себя метод для избежания постоянного доступа к базе данных — это кэширование наборов результатов, которые не изменяются часто, используя систему с более быстрым доступом (обычно память).

Когда Phalcon\Mvc\Model требует службы для кэширования наборов результатов, он запросит её у контейнера Dependency Injector с конвенциональным именем «modelsCache».

Так как Phalcon предоставляет компонент для кэширования любого типа данных, мы объясним, как интегрировать его с моделями. Сначала нужно зарегистрировать его как службу в контейнере сервисов:

use Phalcon\Cache\Frontend\Data as FrontendData;
use Phalcon\Cache\Backend\Memcache as BackendMemcache;

// Set the models cache service
$di->set(
    "modelsCache",
    function () {
        // Cache data for one day by default
        $frontCache = new FrontendData(
            [
                "lifetime" => 86400,
            ]
        );

        // Memcached connection settings
        $cache = new BackendMemcache(
            $frontCache,
            [
                "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(
    [
        "cache" => [
            "key" => "my-cache",
        ],
    ]
);

// Cache the resultset for only for 5 minutes
$products = Products::find(
    [
        "cache" => [
            "key"      => "my-cache",
            "lifetime" => 300,
        ],
    ]
);

// Use the 'cache' service from the DI instead of 'modelsCache'
$products = Products::find(
    [
        "cache" => [
            "key"     => "my-cache",
            "service" => "cache",
        ],
    ]
);

Кэширование также можно применить к наборам результатов, созданным с помощью отношений:

// Query some post
$post = Post::findFirst();

// Get comments related to a post, also cache it
$comments = $post->getComments(
    [
        "cache" => [
            "key" => "my-key",
        ],
    ]
);

// Get comments related to a post, setting lifetime
$comments = $post->getComments(
    [
        "cache" => [
            "key"      => "my-key",
            "lifetime" => 3600,
        ],
    ]
);

Если кэшированный набор результатов необходимо сделать недействительным, его можно просто удалить из кэша, используя указанный ранее ключ.

Обратите внимание, что не все наборы результатов следует кэшировать. Результаты, которые очень часто меняются, не следует кэшировать, так как они быстро становятся недействительными, и кэширование в этом случае ухудшает производительность. Кроме того, большие наборы данных, которые не меняются часто, могут быть кэшированы, но это решение разработчик должен принять, основываясь на доступном механизме кэширования и на том, приемлемо ли ухудшение производительности для простого извлечения этих данных в первую очередь.

Принудительное кэширование

Ранее мы видели, как Phalcon\Mvc\Model интегрируется с компонентом кэширования, предоставляемым фреймворком. Чтобы сделать запись/набор результатов кэшируемой, мы передаём ключ ‘cache’ в массив параметров:

// Cache the resultset for only for 5 minutes
$products = Products::find(
    [
        "cache" => [
            "key"      => "my-cache",
            "lifetime" => 300,
        ],
    ]
);

Это даёт нам свободу кэшировать определённые запросы, однако если мы хотим глобально кэшировать каждый запрос, выполняемый над моделью, мы можем переопределить метод find()/:code:`findFirst() , чтобы принудительно кэшировать каждый запрос:

use Phalcon\Mvc\Model;

class Robots extends Model
{
    /**
     * Implement a method that returns a string key based
     * on the query parameters
     */
    protected static function _createKey($parameters)
    {
        $uniqueKey = [];

        foreach ($parameters as $key => $value) {
            if (is_scalar($value)) {
                $uniqueKey[] = $key . ":" . $value;
            } elseif (is_array($value)) {
                $uniqueKey[] = $key . ":[" . self::_createKey($value) . "]";
            }
        }

        return join(",", $uniqueKey);
    }

    public static function find($parameters = null)
    {
        // Convert the parameters to an array
        if (!is_array($parameters)) {
            $parameters = [$parameters];
        }

        // Check if a cache key wasn't passed
        // and create the cache parameters
        if (!isset($parameters["cache"])) {
            $parameters["cache"] = [
                "key"      => self::_createKey($parameters),
                "lifetime" => 300,
            ];
        }

        return parent::find($parameters);
    }

    public static function findFirst($parameters = null)
    {
        // ...
    }
}

Доступ к базе данных в несколько раз медленнее, чем вычисление ключа кэша. Вы можете свободно реализовать любую стратегию генерации ключей, которая вам подходит. Обратите внимание, что хороший ключ по возможности избегает коллизий — это означает, что разные ключи должны возвращать не связанные записи.

Это даёт вам полный контроль над тем, как должен быть реализован кэш для каждой модели. Если эта стратегия распространена на несколько моделей, вы можете создать базовый класс для всех из них:

use Phalcon\Mvc\Model;

class CacheableModel extends 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
{

}

Кэширование запросов PHQL

Независимо от синтаксиса, используемого для их создания, все запросы в ORM обрабатываются внутри с помощью PHQL. Этот язык даёт вам гораздо больше свободы для создания всех видов запросов. Конечно, эти запросы можно кэшировать:

$phql = "SELECT * FROM Cars WHERE name = :name:";

$query = $this->modelsManager->createQuery($phql);

$query->cache(
    [
        "key"      => "cars-by-name",
        "lifetime" => 300,
    ]
);

$cars = $query->execute(
    [
        "name" => "Audi",
    ]
);

Переиспользуемые связанные записи

Некоторые модели могут иметь отношения с другими моделями. Это позволяет нам легко проверять записи, относящиеся к экземплярам в памяти:

// 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;
$invoices = Invoices::find();

foreach ($invoices 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 автоматически переиспользовать записи из памяти вместо повторного запроса их снова и снова:

use Phalcon\Mvc\Model;

class Invoices extends Model
{
    public function initialize()
    {
        $this->belongsTo(
            "customers_id",
            "Customer",
            "id",
            [
                "reusable" => true,
            ]
        );
    }
}

Обратите внимание, что этот тип кэша работает только в памяти, это означает, что кэшированные данные освобождаются при завершении запроса.

Кэширование связанных записей

При запросе связанной записи 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() в модели счетов-фактур и реализовать кэш, который мы считаем наиболее подходящим:

use Phalcon\Mvc\Model;

class Invoices extends Model
{
    public static function findFirst($parameters = null)
    {
        // ... Custom caching strategy
    }
}

Кэширование связанных записей рекурсивно

В этом случае мы предполагаем, что каждый раз, когда мы запрашиваем результат, мы также получаем связанные с ним записи. Если мы храним найденные записи вместе с их связанными сущностями, возможно, мы могли бы немного уменьшить издержки, необходимые для получения всех сущностей:

use Phalcon\Mvc\Model;

class Invoices extends 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, $results)
    {
        // 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 = [];

        $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, используя альтернативное решение:

use Phalcon\Mvc\Model;

class Invoices extends 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(
            [
                "key"      => self::_createKey($conditions, $params),
                "lifetime" => 300,
            ]
        );

        return $query->execute($params);
    }

}

Кэширование на основе условий

В этом случае кэш реализуется по-разному в зависимости от полученных условий. Мы можем решить, что бэкенд кэша должен определяться первичным ключом:

Тип Бэкенд кэша
1 - 10000 mongo1
10000 - 20000 mongo2
> 20000 mongo3

Самый простой способ добиться этого — добавить статический метод в модель, который выбирает правильный кэш для использования:

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public static function queryCache($initial, $final)
    {
        if ($initial >= 1 && $final < 10000) {
            $service = "mongo1";
        } elseif ($initial >= 10000 && $final <= 20000) {
            $service = "mongo2";
        } elseif ($initial > 20000) {
            $service = "mongo3";
        }

        return self::find(
            [
                "id >= " . $initial . " AND id <= " . $final,
                "cache" => [
                    "service" => $service,
                ],
            ]
        );
    }
}

Этот подход решает проблему, однако если мы хотим добавить другие параметры, такие как заказы или условия, нам пришлось бы создать более сложный метод. Кроме того, этот метод не работает, если данные получены с помощью связанных записей или 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(
    [
        "(id > ?0 AND type = 'A') AND id < ?1",
        "bind"  => [100, 2000],
        "order" => "type",
    ]
);

Для достижения этого нам нужно перехватить промежуточное представление (IR), генерируемое парсером PHQL, и тем самым настроить кэш насколько это возможно:

Первое — создать пользовательский билдер, чтобы мы могли сгенерировать полностью настраиваемый запрос:

use Phalcon\Mvc\Model\Query\Builder as QueryBuilder;

class CustomQueryBuilder extends QueryBuilder
{
    public function getQuery()
    {
        $query = new CustomQuery($this->getPhql());

        $query->setDI($this->getDI());

        return $query;
    }
}

Вместо непосредственного возвращения Phalcon\Mvc\Model\Query, наш пользовательский билдер возвращает экземпляр CustomQuery, этот класс выглядит так:

use Phalcon\Mvc\Model\Query as ModelQuery;

class CustomQuery extends ModelQuery
{
    /**
     * 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, чтобы использовать созданные нами пользовательские классы:

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public static function find($parameters = null)
    {
        if (!is_array($parameters)) {
            $parameters = [$parameters];
        }

        $builder = new CustomQueryBuilder($parameters);

        $builder->from(get_called_class());

        $query = $builder->getQuery();

        if (isset($parameters["bind"])) {
            return $query->execute($parameters["bind"]);
        } else {
            return $query->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,
        [
            $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,
        [
            $i,
        ]
    );

    // ...
}

Планы выполнения запросов, включающих предварительно подготовленные операторы, также кэшируются большинством систем баз данных, сокращая общее время выполнения, а также защищая ваше приложение от SQL-инъекций.

© 2011–2017 Phalcon Framework Team
Licensed under the Creative Commons Attribution License 3.0.
https://docs.phalconphp.com/en/latest/reference/models-cache.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API