前言
最近开始看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
使用方法:
- 在model中加入strait:
use Model\EagerLoadingTrait;
- 这样调用:
$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
1 Comments
test#217 Reply
good