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

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

缓存结果集(Caching Resultsets)

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

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

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

  1. <?php
  2.  
  3. use Phalcon\Cache\Frontend\Data as FrontendData;
  4. use Phalcon\Cache\Backend\Memcache as BackendMemcache;
  5.  
  6. // 设置模型缓存服务
  7. $di->set('modelsCache', function () {
  8.  
  9. // 默认缓存时间为一天
  10. $frontCache = new FrontendData(
  11. array(
  12. "lifetime" => 86400
  13. )
  14. );
  15.  
  16. // Memcached连接配置 这里使用的是Memcache适配器
  17. $cache = new BackendMemcache(
  18. $frontCache,
  19. array(
  20. "host" => "localhost",
  21. "port" => "11211"
  22. )
  23. );
  24.  
  25. return $cache;
  26. });

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

  1. <?php
  2.  
  3. // 直接取Products模型里的数据(未缓存)
  4. $products = Products::find();
  5.  
  6. // 缓存查询结果缓存时间为默认1天。
  7. $products = Products::find(
  8. array(
  9. "cache" => array(
  10. "key" => "my-cache"
  11. )
  12. )
  13. );
  14.  
  15. // 只在数据存在时缓存。
  16. $products = Products::find(
  17. array(
  18. "cache" => array(
  19. "key" => "my-cache",
  20. "allowEmpty" => false,
  21. )
  22. )
  23. );
  24.  
  25. // 缓存查询结果时间为300秒
  26. $products = Products::find(
  27. array(
  28. "cache" => array(
  29. "key" => "my-cache",
  30. "lifetime" => 300
  31. )
  32. )
  33. );
  34.  
  35. // 使用自定义缓存服务
  36. $products = Products::find(
  37. array(
  38. "cache" => array(
  39. "key" => "my-cache",
  40. "service" => "myModelsCache",
  41. "lifetime" => 300
  42. )
  43. )
  44. );
  45.  
  46. 这里我们也可以缓存关联表的数据:
  1. <?php
  2.  
  3. // Query some post
  4. $post = Post::findFirst();
  5.  
  6. // Get comments related to a post, also cache it
  7. $comments = $post->getComments(
  8. array(
  9. "cache" => array(
  10. "key" => "my-key"
  11. )
  12. )
  13. );
  14.  
  15. // Get comments related to a post, setting lifetime
  16. $comments = $post->getComments(
  17. array(
  18. "cache" => array(
  19. "key" => "my-key",
  20. "lifetime" => 3600
  21. )
  22. )
  23. );

如果想删除已经缓存的结果,则只需要使用前面指定的缓存的键值进行删除即可。

注意并不是所有的结果都必须缓存下来。那些经常改变的数据就不应该被缓存,这样做只会影响应用的性能。另外对于那些特别大的不易变的数据集,开发者应用根据实际情况进行选择是否进行缓存。

重写 find 与 findFirst 方法(Overriding find/findFirst)

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

  1. <?php
  2.  
  3. use Phalcon\Mvc\Model;
  4.  
  5. class Robots extends Model
  6. {
  7. public static function find($parameters = null)
  8. {
  9. return parent::find($parameters);
  10. }
  11.  
  12. public static function findFirst($parameters = null)
  13. {
  14. return parent::findFirst($parameters);
  15. }
  16. }

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

  1. <?php
  2.  
  3. use Phalcon\Mvc\Model;
  4.  
  5. class Robots extends Model
  6. {
  7. protected static $_cache = array();
  8.  
  9. /**
  10. * Implement a method that returns a string key based
  11. * on the query parameters
  12. */
  13. protected static function _createKey($parameters)
  14. {
  15. $uniqueKey = array();
  16.  
  17. foreach ($parameters as $key => $value) {
  18. if (is_scalar($value)) {
  19. $uniqueKey[] = $key . ':' . $value;
  20. } else {
  21. if (is_array($value)) {
  22. $uniqueKey[] = $key . ':[' . self::_createKey($value) .']';
  23. }
  24. }
  25. }
  26.  
  27. return join(',', $uniqueKey);
  28. }
  29.  
  30. public static function find($parameters = null)
  31. {
  32. // Create an unique key based on the parameters
  33. $key = self::_createKey($parameters);
  34.  
  35. if (!isset(self::$_cache[$key])) {
  36. // Store the result in the memory cache
  37. self::$_cache[$key] = parent::find($parameters);
  38. }
  39.  
  40. // Return the result in the cache
  41. return self::$_cache[$key];
  42. }
  43.  
  44. public static function findFirst($parameters = null)
  45. {
  46. // ...
  47. }
  48. }

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

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

  1. <?php
  2.  
  3. public static function find($parameters = null)
  4. {
  5. // Create an unique key based on the parameters
  6. $key = self::_createKey($parameters);
  7.  
  8. if (!isset(self::$_cache[$key])) {
  9.  
  10. // We're using APC as second cache
  11. if (apc_exists($key)) {
  12.  
  13. $data = apc_fetch($key);
  14.  
  15. // Store the result in the memory cache
  16. self::$_cache[$key] = $data;
  17.  
  18. return $data;
  19. }
  20.  
  21. // There are no memory or apc cache
  22. $data = parent::find($parameters);
  23.  
  24. // Store the result in the memory cache
  25. self::$_cache[$key] = $data;
  26.  
  27. // Store the result in APC
  28. apc_store($key, $data);
  29.  
  30. return $data;
  31. }
  32.  
  33. // Return the result in the cache
  34. return self::$_cache[$key];
  35. }

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

  1. <?php
  2.  
  3. use Phalcon\Mvc\Model;
  4.  
  5. class CacheableModel extends Model
  6. {
  7. protected static function _createKey($parameters)
  8. {
  9. // ... Create a cache key based on the parameters
  10. }
  11.  
  12. public static function find($parameters = null)
  13. {
  14. // ... Custom caching strategy
  15. }
  16.  
  17. public static function findFirst($parameters = null)
  18. {
  19. // ... Custom caching strategy
  20. }
  21. }

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

  1. <?php
  2.  
  3. class Robots extends CacheableModel
  4. {
  5.  
  6. }

强制缓存(Forcing Cache)

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

  1. <?php
  2.  
  3. // 缓存查询结果5分钟
  4. $products = Products::find(
  5. array(
  6. "cache" => array(
  7. "key" => "my-cache",
  8. "lifetime" => 300
  9. )
  10. )
  11. );

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

  1. <?php
  2.  
  3. use Phalcon\Mvc\Model;
  4.  
  5. class Robots extends Model
  6. {
  7. protected static function _createKey($parameters)
  8. {
  9. // ... Create a cache key based on the parameters
  10. }
  11.  
  12. public static function find($parameters = null)
  13. {
  14. // Convert the parameters to an array
  15. if (!is_array($parameters)) {
  16. $parameters = array($parameters);
  17. }
  18.  
  19. // Check if a cache key wasn't passed
  20. // and create the cache parameters
  21. if (!isset($parameters['cache'])) {
  22. $parameters['cache'] = array(
  23. "key" => self::_createKey($parameters),
  24. "lifetime" => 300
  25. );
  26. }
  27.  
  28. return parent::find($parameters);
  29. }
  30.  
  31. public static function findFirst($parameters = null)
  32. {
  33. // ...
  34. }
  35.  
  36. }

缓存 PHQL 查询(Caching PHQL Queries)

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

  1. <?php
  2.  
  3. $phql = "SELECT * FROM Cars WHERE name = :name:";
  4.  
  5. $query = $this->modelsManager->createQuery($phql);
  6.  
  7. $query->cache(
  8. array(
  9. "key" => "cars-by-name",
  10. "lifetime" => 300
  11. )
  12. );
  13.  
  14. $cars = $query->execute(
  15. array(
  16. 'name' => 'Audi'
  17. )
  18. );

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

  1. <?php
  2.  
  3. $phql = "SELECT * FROM Cars WHERE name = :name:";
  4.  
  5. $cars = $this->modelsManager->executeQuery(
  6. $phql,
  7. array(
  8. 'name' => 'Audi'
  9. )
  10. );
  11.  
  12. apc_store('my-cars', $cars);

一些模型有关联的数据表我们直接使用关联的数据:

  1. <?php
  2.  
  3. // Get some invoice
  4. $invoice = Invoices::findFirst();
  5.  
  6. // Get the customer related to the invoice
  7. $customer = $invoice->customer;
  8.  
  9. // Print his/her name
  10. echo $customer->name, "\n";

这个例子非常简单,依据查询到的订单信息取得用户信息之后再取得用户名。下面的情景也是如何:我们查询了一些订单的信息,然后取得这些订单相关联用户的信息,之后取得用户名:

  1. <?php
  2.  
  3. // Get a set of invoices
  4. // SELECT * FROM invoices;
  5. foreach (Invoices::find() as $invoice) {
  6.  
  7. // Get the customer related to the invoice
  8. // SELECT * FROM customers WHERE id = ?;
  9. $customer = $invoice->customer;
  10.  
  11. // Print his/her name
  12. echo $customer->name, "\n";
  13. }

每个客户可能会有一个或多个帐单,这就意味着客户对象没必须取多次。为了避免一次次的重复取客户信息,我们这里设置关系为reusable为true,这样ORM即知可以重复使用客户信息:

  1. <?php
  2.  
  3. use Phalcon\Mvc\Model;
  4.  
  5. class Invoices extends Model
  6. {
  7. public function initialize()
  8. {
  9. $this->belongsTo(
  10. "customers_id",
  11. "Customer",
  12. "id",
  13. array(
  14. 'reusable' => true
  15. )
  16. );
  17. }
  18. }

此Cache存在于内存中,这意味着当请示结束时缓存数据即被释放。我们也可以通过重写模型管理器的方式实现更加复杂的缓存:

  1. <?php
  2.  
  3. use Phalcon\Mvc\Model\Manager as ModelManager;
  4.  
  5. class CustomModelsManager extends ModelManager
  6. {
  7. /**
  8. * Returns a reusable object from the cache
  9. *
  10. * @param string $modelName
  11. * @param string $key
  12. * @return object
  13. */
  14. public function getReusableRecords($modelName, $key)
  15. {
  16. // If the model is Products use the APC cache
  17. if ($modelName == 'Products') {
  18. return apc_fetch($key);
  19. }
  20.  
  21. // For the rest, use the memory cache
  22. return parent::getReusableRecords($modelName, $key);
  23. }
  24.  
  25. /**
  26. * Stores a reusable record in the cache
  27. *
  28. * @param string $modelName
  29. * @param string $key
  30. * @param mixed $records
  31. */
  32. public function setReusableRecords($modelName, $key, $records)
  33. {
  34. // If the model is Products use the APC cache
  35. if ($modelName == 'Products') {
  36. apc_store($key, $records);
  37. return;
  38. }
  39.  
  40. // For the rest, use the memory cache
  41. parent::setReusableRecords($modelName, $key, $records);
  42. }
  43. }

别忘记注册模型管理器到DI中:

  1. <?php
  2.  
  3. $di->setShared('modelsManager', function () {
  4. return new CustomModelsManager();
  5. });

当使用find或findFirst查询关联数据时,ORM内部会自动的依据以下规则创建查询条件于:

类型 描述 隐含方法
Belongs-To 直接的返回模型相关的记录 findFirst
Has-One 直接的返回模型相关的记录 findFirst
Has-Many 返回模型相关的记录集合 find

这意味着当我们取得关联记录时,我们需要解析如何如何取得数据的方法:

  1. <?php
  2.  
  3. // Get some invoice
  4. $invoice = Invoices::findFirst();
  5.  
  6. // Get the customer related to the invoice
  7. $customer = $invoice->customer; // Invoices::findFirst('...');
  8.  
  9. // Same as above
  10. $customer = $invoice->getCustomer(); // Invoices::findFirst('...');

因此,我们可以替换掉Invoices模型中的findFirst方法然后实现我们使用适合的方法

  1. <?php
  2.  
  3. use Phalcon\Mvc\Model;
  4.  
  5. class Invoices extends Model
  6. {
  7. public static function findFirst($parameters = null)
  8. {
  9. // .. custom caching strategy
  10. }
  11. }

在这种场景下我们假定我们每次取主记录时都会取模型的关联记录,如果我们此时保存这些记录可能会为为我们的系统带来一些性能上的提升:

  1. <?php
  2.  
  3. use Phalcon\Mvc\Model;
  4.  
  5. class Invoices extends Model
  6. {
  7. protected static function _createKey($parameters)
  8. {
  9. // ... Create a cache key based on the parameters
  10. }
  11.  
  12. protected static function _getCache($key)
  13. {
  14. // Returns data from a cache
  15. }
  16.  
  17. protected static function _setCache($key)
  18. {
  19. // Stores data in the cache
  20. }
  21.  
  22. public static function find($parameters = null)
  23. {
  24. // Create a unique key
  25. $key = self::_createKey($parameters);
  26.  
  27. // Check if there are data in the cache
  28. $results = self::_getCache($key);
  29.  
  30. // Valid data is an object
  31. if (is_object($results)) {
  32. return $results;
  33. }
  34.  
  35. $results = array();
  36.  
  37. $invoices = parent::find($parameters);
  38. foreach ($invoices as $invoice) {
  39.  
  40. // Query the related customer
  41. $customer = $invoice->customer;
  42.  
  43. // Assign it to the record
  44. $invoice->customer = $customer;
  45.  
  46. $results[] = $invoice;
  47. }
  48.  
  49. // Store the invoices in the cache + their customers
  50. self::_setCache($key, $results);
  51.  
  52. return $results;
  53. }
  54.  
  55. public function initialize()
  56. {
  57. // Add relations and initialize other stuff
  58. }
  59. }

从已经缓存的订单中取得用户信息,可以减少系统的负载。注意我们也可以使用PHQL来实现这个,下面使用了PHQL来实现:

  1. <?php
  2.  
  3. use Phalcon\Mvc\Model;
  4.  
  5. class Invoices extends Model
  6. {
  7. public function initialize()
  8. {
  9. // Add relations and initialize other stuff
  10. }
  11.  
  12. protected static function _createKey($conditions, $params)
  13. {
  14. // ... Create a cache key based on the parameters
  15. }
  16.  
  17. public function getInvoicesCustomers($conditions, $params = null)
  18. {
  19. $phql = "SELECT Invoices.*, Customers.*
  20. FROM Invoices JOIN Customers WHERE " . $conditions;
  21.  
  22. $query = $this->getModelsManager()->executeQuery($phql);
  23.  
  24. $query->cache(
  25. array(
  26. "key" => self::_createKey($conditions, $params),
  27. "lifetime" => 300
  28. )
  29. );
  30.  
  31. return $query->execute($params);
  32. }
  33.  
  34. }

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

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

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

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

  1. <?php
  2.  
  3. use Phalcon\Mvc\Model;
  4.  
  5. class Robots extends Model
  6. {
  7. public static function queryCache($initial, $final)
  8. {
  9. if ($initial >= 1 && $final < 10000) {
  10. return self::find(
  11. array(
  12. 'id >= ' . $initial . ' AND id <= '.$final,
  13. 'cache' => array(
  14. 'service' => 'mongo1'
  15. )
  16. )
  17. );
  18. }
  19.  
  20. if ($initial >= 10000 && $final <= 20000) {
  21. return self::find(
  22. array(
  23. 'id >= ' . $initial . ' AND id <= '.$final,
  24. 'cache' => array(
  25. 'service' => 'mongo2'
  26. )
  27. )
  28. );
  29. }
  30.  
  31. if ($initial > 20000) {
  32. return self::find(
  33. array(
  34. 'id >= ' . $initial,
  35. 'cache' => array(
  36. 'service' => 'mongo3'
  37. )
  38. )
  39. );
  40. }
  41. }
  42. }

这个方法是可以解决问题,不过如果我们需要添加其它的参数比如排序或条件等我们还要创建更复杂的方法。另外当我们使用find/findFirst来查询关联数据时此方法亦会失效:

  1. <?php
  2.  
  3. $robots = Robots::find('id < 1000');
  4. $robots = Robots::find('id > 100 AND type = "A"');
  5. $robots = Robots::find('(id > 100 AND type = "A") AND id < 2000');
  6.  
  7. $robots = Robots::find(
  8. array(
  9. '(id > ?0 AND type = "A") AND id < ?1',
  10. 'bind' => array(100, 2000),
  11. 'order' => 'type'
  12. )
  13. );

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

  1. <?php
  2.  
  3. use Phalcon\Mvc\Model\Query\Builder as QueryBuilder;
  4.  
  5. class CustomQueryBuilder extends QueryBuilder
  6. {
  7. public function getQuery()
  8. {
  9. $query = new CustomQuery($this->getPhql());
  10. $query->setDI($this->getDI());
  11. return $query;
  12. }
  13. }

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

  1. <?php
  2.  
  3. use Phalcon\Mvc\Model\Query as ModelQuery;
  4.  
  5. class CustomQuery extends ModelQuery
  6. {
  7. /**
  8. * The execute method is overridden
  9. */
  10. public function execute($params = null, $types = null)
  11. {
  12. // Parse the intermediate representation for the SELECT
  13. $ir = $this->parse();
  14.  
  15. // Check if the query has conditions
  16. if (isset($ir['where'])) {
  17.  
  18. // The fields in the conditions can have any order
  19. // We need to recursively check the conditions tree
  20. // to find the info we're looking for
  21. $visitor = new CustomNodeVisitor();
  22.  
  23. // Recursively visits the nodes
  24. $visitor->visit($ir['where']);
  25.  
  26. $initial = $visitor->getInitial();
  27. $final = $visitor->getFinal();
  28.  
  29. // Select the cache according to the range
  30. // ...
  31.  
  32. // Check if the cache has data
  33. // ...
  34. }
  35.  
  36. // Execute the query
  37. $result = $this->_executeSelect($ir, $params, $types);
  38.  
  39. // Cache the result
  40. // ...
  41.  
  42. return $result;
  43. }
  44. }

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

  1. <?php
  2.  
  3. class CustomNodeVisitor
  4. {
  5. protected $_initial = 0;
  6.  
  7. protected $_final = 25000;
  8.  
  9. public function visit($node)
  10. {
  11. switch ($node['type']) {
  12.  
  13. case 'binary-op':
  14.  
  15. $left = $this->visit($node['left']);
  16. $right = $this->visit($node['right']);
  17. if (!$left || !$right) {
  18. return false;
  19. }
  20.  
  21. if ($left=='id') {
  22. if ($node['op'] == '>') {
  23. $this->_initial = $right;
  24. }
  25. if ($node['op'] == '=') {
  26. $this->_initial = $right;
  27. }
  28. if ($node['op'] == '>=') {
  29. $this->_initial = $right;
  30. }
  31. if ($node['op'] == '<') {
  32. $this->_final = $right;
  33. }
  34. if ($node['op'] == '<=') {
  35. $this->_final = $right;
  36. }
  37. }
  38. break;
  39.  
  40. case 'qualified':
  41. if ($node['name'] == 'id') {
  42. return 'id';
  43. }
  44. break;
  45.  
  46. case 'literal':
  47. return $node['value'];
  48.  
  49. default:
  50. return false;
  51. }
  52. }
  53.  
  54. public function getInitial()
  55. {
  56. return $this->_initial;
  57. }
  58.  
  59. public function getFinal()
  60. {
  61. return $this->_final;
  62. }
  63. }

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

  1. <?php
  2.  
  3. use Phalcon\Mvc\Model;
  4.  
  5. class Robots extends Model
  6. {
  7. public static function find($parameters = null)
  8. {
  9. if (!is_array($parameters)) {
  10. $parameters = array($parameters);
  11. }
  12.  
  13. $builder = new CustomQueryBuilder($parameters);
  14. $builder->from(get_called_class());
  15.  
  16. if (isset($parameters['bind'])) {
  17. return $builder->getQuery()->execute($parameters['bind']);
  18. } else {
  19. return $builder->getQuery()->execute();
  20. }
  21. }
  22. }

缓存 PHQL 查询计划(Caching of PHQL planning)

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

  1. <?php
  2.  
  3. for ($i = 1; $i <= 10; $i++) {
  4.  
  5. $phql = "SELECT * FROM Store\Robots WHERE id = " . $i;
  6. $robots = $this->modelsManager->executeQuery($phql);
  7.  
  8. // ...
  9. }

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

  1. <?php
  2.  
  3. $phql = "SELECT * FROM Store\Robots WHERE id = ?0";
  4.  
  5. for ($i = 1; $i <= 10; $i++) {
  6.  
  7. $robots = $this->modelsManager->executeQuery($phql, array($i));
  8.  
  9. // ...
  10. }

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

  1. <?php
  2.  
  3. $phql = "SELECT * FROM Store\Robots WHERE id = ?0";
  4. $query = $this->modelsManager->createQuery($phql);
  5.  
  6. for ($i = 1; $i <= 10; $i++) {
  7.  
  8. $robots = $query->execute($phql, array($i));
  9.  
  10. // ...
  11. }

预先准备的查询语句 的查询计划亦可以被大多数的数据库所缓存,这样可以减少执行的时间,也可以使用我们的系统免受 SQL注入 的影响。

原文: http://www.myleftstudio.com/reference/models-cache.html