Documentation

缓存对象关系映射(Caching in the ORM)

现实中的每个应用都不同,一些应用的模型数据经常改变而另一些模型的数据几乎不同。访问数据库在很多时候对我们应用的来说 是个瓶颈。这是由于我们每次访问应用时都会和数据库数据通信,和数据库进行通信的代价是很大的。因此在必要时我们可以通过增加 缓存层来获取更高的性能。 本章内容的重点即是探讨实施缓存来提高性能的可行性。Phalcon框架给我们提供了灵活的缓存技术来实现我们的应用缓存。

缓存结果集(Caching Resultsets)

一个非常可行的方案是我们可以为那些不经常改变且经常访问的数据库数据进行缓存,比如把他们放入内存,这样可以加快程序的执行速度。

Phalcon\Mvc\Model 需要使用缓存数据的服务时Model可以直接从DI中取得此缓存服务modelsCache(惯例名).

Phalcon提供了一个组件(服务)可以用来 缓存 任何种类的数据,下面我们会解释如何在model使用它。第一步我们要在启动文件注册 这个服务:

<?php

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

// 设置模型缓存服务
$di->set('modelsCache', function () {

    // 默认缓存时间为一天
    $frontCache = new FrontendData(
        array(
            "lifetime" => 86400
        )
    );

    // Memcached连接配置 这里使用的是Memcache适配器
    $cache = new BackendMemcache(
        $frontCache,
        array(
            "host" => "localhost",
            "port" => "11211"
        )
    );

    return $cache;
});

在注册缓存服务时我们可以按照我们的所需进行配置。一旦完成正确的缓存设置之后,我们可以按如下的方式缓存查询的结果了:

<?php

// 直接取Products模型里的数据(未缓存)
$products = Products::find();

// 缓存查询结果缓存时间为默认1天。
$products = Products::find(
    array(
        "cache" => array(
            "key" => "my-cache"
        )
    )
);

// 只在数据存在时缓存。
$products = Products::find(
    array(
        "cache" => array(
            "key" => "my-cache",
            "allowEmpty" => false,
        )
    )
);

// 缓存查询结果时间为300秒
$products = Products::find(
    array(
        "cache" => array(
            "key"      => "my-cache",
            "lifetime" => 300
        )
    )
);

// 使用自定义缓存服务
$products = Products::find(
    array(
        "cache" => array(
            "key"      => "my-cache",
            "service"  => "myModelsCache",
            "lifetime" => 300
        )
    )
);

这里我们也可以缓存关联表的数据:
<?php

// 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 方法(Overriding find/findFirst)

从上面的我们可以看到这两个方法是从 Phalcon\Mvc\Model继承而来:

<?php

use Phalcon\Mvc\Model;

class Robots extends Model
{
    public static function find($parameters = null)
    {
        return parent::find($parameters);
    }

    public static function findFirst($parameters = null)
    {
        return parent::findFirst($parameters);
    }
}

这样做会影响到所有此类的对象对这两个函数的调用,我们可以在其中添加一个缓存层,如果未有其它缓存的 话(比如modelsCache)。例如,一个基本的缓存实现是我们在此类中添加一个静态的变量以避免在同一请求中 多次查询数据库:

<?php

use Phalcon\Mvc\Model;

class Robots extends 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)
    {
        // ...
    }
}

访问数据要远比计算key值慢的多,我们在这里定义自己需要的key生成方式。注意好的键可以避免冲突,这样就可以依据不同的key值取得不同的缓存结果。

上面的例子中我们把缓存放在了内存中,这做为第一级的缓存。当然我们也可以在第一层缓存的基本上实现第二层的缓存比如使用 APC/XCache 或是使用NoSQL数据库(如MongoDB等):

<?php

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];
}

这样我们可以对可模型的缓存进行完全的控制,如果多个模型需要进行如此缓存可以建立一个基础类:

<?php

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
    }
}

然后把这个类作为其它缓存类的基类:

<?php

class Robots extends CacheableModel
{

}

强制缓存(Forcing Cache)

前面的例子中我们在 Phalcon\Mvc\Model 中使用框架内建的缓存组件。为实现强制缓存我们传递了cache作为参数:

<?php

// 缓存查询结果5分钟
$products = Products::find(
    array(
        "cache" => array(
            "key"      => "my-cache",
            "lifetime" => 300
        )
    )
);

为了自由的对特定的查询结果进行缓存我们,比如我们想对模型中的所有查询结果进行缓存我们可以重写find/findFirst方法:

<?php

use Phalcon\Mvc\Model;

class Robots extends 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 查询(Caching PHQL Queries)

ORM中的所有查询,不管多么高级的查询方法内部使用使用PHQL进行实现的。这个语言可以让我们非常自由的创建各种查询,当然这些查询也可以被缓存:

<?php

$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'
    )
);

如果不想使用隐式的缓存尽管使用你想用的缓存方式:

<?php

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

$cars = $this->modelsManager->executeQuery(
    $phql,
    array(
        'name' => 'Audi'
    )
);

apc_store('my-cars', $cars);

基于条件的缓存(Caching based on Conditions)

此例中,我依据当的条件实施缓存:

类型 缓存
1 - 10000 mongo1
10000 - 20000 mongo2
> 20000 mongo3

最简单的方式即是为模型类添加一个静态的方法,此方法中我们指定要使用的缓存:

<?php

use Phalcon\Mvc\Model;

class Robots extends 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来查询关联数据时此方法亦会失效:

<?php

$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'
    )
);

为了实现这个我们需要拦截中间语言解析,然后书写相关的代码以定制缓存: 首先我们需要创建自定义的创建器,然后我们可以使用它来创建守全自己定义的查询:

<?php

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;
    }
}

这里我们返回的是CustomQuery而不是不直接的返回 Phalcon\Mvc\Model\Query, 类定义如下所示:

<?php

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;
    }
}

这里我们实现了一个帮助类用以递归的的检查条件以查询字段用以识我们知了需要使用缓存的范围(即检查条件以确认实施查询缓存的范围):

<?php

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;
    }
}

最后,我们替换Robots模型中的查询方法以使用我们创建的自定义类:

<?php

use Phalcon\Mvc\Model;

class Robots extends 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 查询计划(Caching of PHQL planning)

像大多数现代的操作系统一样PHQL内部会缓存执行计划,如果同样的语句多次执行,PHQL会使用之前生成的查询计划以提升系统的性能, 对开发者来说只采用绑定参数的形式传递参数即可实现:

<?php

for ($i = 1; $i <= 10; $i++) {

    $phql   = "SELECT * FROM Store\Robots WHERE id = " . $i;
    $robots = $this->modelsManager->executeQuery($phql);

    // ...
}

上面的例子中,Phalcon产生了10个查询计划,这导致了应用的内存使用量增加。重写以上代码,我们使用绑定参数的这个优点可以减少系统和数据库的过多操作:

<?php

$phql = "SELECT * FROM Store\Robots WHERE id = ?0";

for ($i = 1; $i <= 10; $i++) {

    $robots = $this->modelsManager->executeQuery($phql, array($i));

    // ...
}

得用PHQL查询亦可以提供查询性能:

<?php

$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注入 的影响。