前言

最近开始看phalcon的官方文档,并开始用在我的博客上。使用下来发现phalcon的模型model层非常非常难用,并且有一些性能方面的问题需要注意。

这篇文章带来phalcon的关联关系relation和分页paginator等几个性能问题及解决方案。

本文内容会在不断深入的使用过程中不定期添加。

关联关系relation

常见的错误姿势

不要在多个地方执行relation方法,而是赋值。

错误使用方法

<a href="/category/{{ post.getCategories().id }}/{{ post.getCategories().title }}">
    {{ post.getCategories().title }}
</a>

使用这种方法会导致产生3个relation数据库sql查询

正确使用方法

{% set category = post.getCategories() %}
<a href="/category/{{ category.id }}/{{ category.title }}">
    {{ category.title }}
</a>

尽量不用relation

效率问题

虽然relation看起来很好用,但是phalcon的relation却有严重的效率问题。们通常会这样定义relation:

// Model Posts
public function initialize()
{
    $this->belongsTo('category_id', 'MyApp\Models\Categories', 'id', [
        'alias' => 'Categories',
    ]);
}

然后这样使用:

$posts = Posts::find();
foreach($posts as $post){
    var_dump($post->title, $post->categories->title);
}

们来看看使用了一些什么sql查询:

SELECT `posts`.`id`, `posts`.`title` FROM `posts`
SELECT `categories`.`id`, `categories`.`title` FROM `categories` WHERE `categories`.`id` = :APR0 LIMIT :APL0
SELECT `categories`.`id`, `categories`.`title` FROM `categories` WHERE `categories`.`id` = :APR0 LIMIT :APL0
SELECT `categories`.`id`, `categories`.`title` FROM `categories` WHERE `categories`.`id` = :APR0 LIMIT :APL0
SELECT `categories`.`id`, `categories`.`title` FROM `categories` WHERE `categories`.`id` = :APR0 LIMIT :APL0
-- N条categires表的查询

也就是说在你foreach每一个post元素的时候,就会产生一条查询categories的sql语句

我们可以使用reusable来缓存已经查询出来的关联关系

public function initialize()
{
    $this->belongsTo('category_id', 'MyApp\Models\Categories', 'id', [
        'alias' => 'Categories',
        'reusable' => true, // 如果这个categories id已经查找过,那么他将不会重复查询
    ]);
}

尽量使用queryBuilder join

的推荐做法是使用queryBuilder:

// createBuilder中一定要加columns
$post = new Posts();
$results = $post->getModelsManager()->createBuilder()
    ->columns(['p.*', 'c.*'])
    ->addFrom(Posts::class, 'p')
    ->join(Categories::class, 'p.category_id = c.id', 'c')
    ->getQuery()->execute();
foreach($results as $result){
    var_dump($result->p->title, $result->c->title);
}

这种方式的sql:

SELECT `p`.`id` AS `_p_id`, `p`.`title` AS `_p_title`, `c`.`id` AS `_c_id`, `c`.`title` AS `_c_title` FROM `posts` AS `p`  INNER JOIN `categories` AS `c` ON `p`.`category_id` = `c`.`id`

折中的relation方案

如果你对relation仍然情有独钟,那么可以通过incubator让一切看起来不那么糟糕。

https://github.com/phalcon/incubator/tree/master/Library/Phalcon/Mvc/Model

使用方法:

  1. 在model中加入strait:use Model\EagerLoadingTrait;
  2. 这样调用:
    $posts = Posts::with('categories');
    foreach ($posts as $post){
    var_dump($post->title, $post->categories->title);
    }

    来看一下他产生的sql:

    SELECT `posts`.`id`, `posts`.`title` FROM `posts`
    SELECT `categories`.`id`, `categories`.`title` WHERE `categories`.`id` IN (:AP0, :AP1, :AP2, :AP3, :AP4)

    它把多个categories的单条查询合并成了一个in条件语句。但这依旧不是一种推荐的方式。

相关讨论:

分页paginator

错误使用方法

use Phalcon\Paginator\Adapter\Model as Page;

$records = $model::find([
    'conditions' => $conditions,
    'bind' => $bind,
    'order' => $order,
]);
$paginate = (new Page([
    'data' => $records,
    'limit' => $limit,
    'page' => $page,
]))->getPaginate();

使用model find的方式会把model对应的表的所有数据都查询出来,再扔到paginator中进行分页处理。如果表的数据量很多,直接内存就爆了,表现在sql中是:

SELECT `posts`.`id` FROM `posts` ORDER BY `posts`.`id` DESC
-- 注意此处没有使用limit

正确使用方法

use Phalcon\Paginator\Adapter\QueryBuilder as Page;

$records = $model->getModelsManager()->createBuilder()
    ->from(get_class($model))
    ->where($conditions, $bind)
    ->orderBy($order);

$paginate = (new Page([
    'builder' => $records,
    'limit' => $limit,
    'page' => $page,
]))->getPaginate();

使用QueryBuilder的方式会在sql中加入limit,这才是分页的正确使用姿势,表现在sql中是:

SELECT `posts`.`id` FROM `posts` ORDER BY `posts`.`id` DESC LIMIT :APL0

元数据Metadata

默认情况下,每一个model的调用都会去数据库中查询Metadata,表现在sql中是:

SELECT IF(COUNT(*) > 0, 1, 0) FROM `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_NAME` = 'pages' AND `TABLE_SCHEMA` = DATABASE()
DESCRIBE `pages`
SELECT IF(COUNT(*) > 0, 1, 0) FROM `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_NAME` = 'categories' AND `TABLE_SCHEMA` = DATABASE()
DESCRIBE `categories`
-- N个Metadata信息获取sql

我们可以把这些Metadata缓存下来,官方支持非常多的缓存方式:https://docs.phalconphp.com/ar/3.2/db-models-metadata

以files方式为例:

use Phalcon\Mvc\Model\Metadata\Files as MetaDataAdapter;

$di->setShared('modelsMetadata', function () use ($config) {
    return new MetaDataAdapter([
        'lifetime' => 300,
        'metaDataDir' => __DIR__ . '/../cache/metadata/',
    ]);
});

动态更新Dynamic Update

默认情况下,如果你update了一个model的某个字段,在生成sql语句时会变成update所有字段,例如:

$post = Posts::findFirst([
    'conditions' => 'id = 1',
    'for_update' => true,
]);
$post->clicks += 1;
$post->save();

对应的sql:

UPDATE `posts` SET `category_id` = ?, `title` = ?, `slug` = ?, `content` = ?, `clicks` = ? WHERE `id` = ?

当你的content内容居多的时候,想想是不是很坑的赶脚?我明明只需要更新clicks好不好

我们可以使用动态更新,在model的初始化方法中加入useDynamicUpdate来避免这种情况:

public function initialize()
{
    $this->useDynamicUpdate(true);
}

对应的sql:

UPDATE `posts` SET `clicks` = ? WHERE `id` = ?

如果您觉得您在我这里学到了新姿势,博主支持转载,姿势本身就是用来相互学习的。同时,本站文章如未注明均为 hisune 原创 请尊重劳动成果 转载请注明 转自: 关于phalcon的模型(Model)的性能问题 - hisune.com