ORM, несомненно, мощная и удобная вещь, но запросы генерируются не только не всегда оптимальные, но еще и лишние. При создании объекта модели ORM должен знать информацию о всех полях соответствующей таблицы БД. Что приводит к нежелательным запросам к БД.
Проблема
При создании объекта модели с помощью ORM выполняется запрос SHOW FULL COLUMNS FROM `tablename` и заполняется поле объекта protected $_table_columns массивом с данными о полях.
protected _table_columns => array(8) (
"id" => array(4) (
"type" => string(3) "int"
"is_nullable" => bool FALSE
)
"email" => array(4) (
"type" => string(6) "string"
"is_nullable" => bool FALSE
)
...
На скриншоте видно последний запрос к БД (кликабельно)
Причем ORM::factory() каждый раз создает новый экземпляр объекта и, следовательно, вызов подряд нескольких методов с помощью конструкции
ORM::factory('model_name')->method_1()
ORM::factory('model_name')->method_2()
генерирует 2 одинаковых запроса SHOW FULL COLUMNS FROM (даже если $_table_columns вообще не понадобятся в конкретном случае). Также загрузка связанных моделей генерирует запросы для каждой из моделей — вызов ORM::factory('user')->with('profile')->with('photo'). Выходит каждый второй запрос к БД в проекте (активно использующем ORM) — SHOW FULL COLUMNS FROM.
Одно из решений
Решение проблемы очень простое, но почему-то нигде не описанное — заполнить вручную этот массив в каждой модели (естественно в конце разработки проекта). Попытка заполнить его в ручную для нескольких десятков больших таблиц подобна выстрелу себе в ногу. Поэтому за несколько часов было найдено универсальное решение — написать класс Optimize, который рекурсивно проходится по папке с моделями, выбирает содержащие запись extends ORM и не содержащие запись protected $_table_columns и генерирует для модели этот массив используя ORM::factory('model')->list_columns() и немного переделанный «родной» метод Debug::vars();
Код самого класса — под спойлером
class Optimize{
private static $files = array();
/**
* Returns database tables columns list
*
* @uses find_models()
* @uses _dump_simple()
*/
public static function list_columns()
{
$dir = APPPATH . "classes/model";
self::find_models($dir);
foreach (self::$files as $model) {
$file_text = file_get_contents($model);
if(preg_match('/extends +ORM/i', $file_text) && !preg_match('/_table_columns/i', $file_text)){
preg_match("/(classsModel_)(w+)?(sextends)/", $file_text, $match);
$model_name = preg_replace("/(classsModel_)(.*?)(sextends)/", "$2", $match[0]);
echo '<h3>Model_'.ucfirst($model_name).'</h3>';
$columns[] = ORM::factory(strtolower($model_name))->list_columns();
$output = array();
foreach ($columns as $var) {
$output[] = self::_dump_simple($var, 1024);
}
echo '<pre>protected $_table_columns = ' . substr(implode("n", $output), 0, -1) . ';</pre>';
echo '========================================================';
}
}
}
public static function find_models($in_dir)
{
if (preg_match("/_vti[.]*/i", $in_dir)) {
return;
}
if ($dir_handle = @opendir($in_dir)) {
while ($file = readdir($dir_handle)) {
$path = $in_dir . "/" . $file;
if ($file != ".." && $file != "." && is_dir($path) && $file != '.svn') {
self::find_models($path);
}
if (is_file($path) && $file != ".." && $file != "." && strtolower(substr(strrchr($path, '.'), 1))=='php') {
self::$files[] = $path;
}
}
}
}
protected static function _dump_simple(& $var, $length = 128, $limit = 10, $level = 0)
{
if ($var === NULL) {
return 'NULL,';
}
elseif (is_bool($var)) {
return ($var ? 'TRUE' : 'FALSE') . ',';
}
elseif (is_float($var)) {
return $var . ',';
}
elseif (is_string($var)) {
return "'" . $var . "',";
}
elseif (is_array($var)) {
$output = array();
$space = str_repeat($s = ' ', $level);
static $marker;
if ($marker === NULL) {
$marker = uniqid("x00");
}
if ($level < $limit) {
$output[] = "array(";
$var[$marker] = TRUE;
foreach ($var as $key => & $val) {
if ($level == 1 && !in_array($key, array('type', 'is_nullable')))
continue;
if ($key === $marker)
continue;
if (!is_int($key)) {
$key = "'" . htmlspecialchars($key, ENT_NOQUOTES, Kohana::$charset) . "'";
}
$output[] = "$space$s$key => " . self::_dump_simple($val, $length, $limit, $level + 1);
}
unset($var[$marker]);
$output[] = "$space),";
}
return implode("n", $output);
}
else {
return htmlspecialchars(print_r($var, TRUE), ENT_NOQUOTES, Kohana::$charset) . ',';
}
}
} // End Optimize
Запись массива с полями автоматически в код класса модели было решено не делать — все равно с форматированием кода не угадаешь. Поэтому все выводится на экран в виде:
Model_Option
protected $_table_columns = array(
'id' => array(
'type' => 'int',
'is_nullable' => FALSE,
),
'name' => array(
'type' => 'string',
'is_nullable' => FALSE,
),
Сам класс (размещать в /application/classes/optimize.php ). Вызов метода из любого места:
echo Optimize::list_columns();
Доказательство работы метода — отсутвие last_query в распечатанном объекте модели.
Другие найденные рещения — blogocms.ru/2011/05/kohana-uskoryaem-orm/ — кеширование структуры таблиц. Более просто решение, но и менее оптимальное по скорости.
Профилирование и тесты
Была произведена попытка измерения скорости (на точность измерений не претендую). Напишем небольшой синтетический тест
$token = Profiler::start('Model', 'User');
ORM::factory('user')->with('profile')->with('profile:file');
Profiler::stop($token);
echo View::factory('profiler/stats');
И запустим его 10 раз. Получаем что без заполения массивов $_table_columns в среднем на всю работу фреймворка уходит 0.15 сек из них 0.005 сек. на запросы SHOW FULL COLUMNS FROM.
С заполненным $_table_columns — в среднем 0.145 сек. Прирост 3.3%
Напишем более реальный тест c выборкой нескольких записей и использованием связанных моделей.
$token = Profiler::start('Model', 'User');
for ($index = 0; $index < 10; $index++) {
ORM::factory('user')->with('profile')->with('profile:file')->get_user(array(rand(1,100), rand(1,100)));
}
Profiler::stop($token);
echo View::factory('profiler/stats');
Без заполения массивов $_table_columns в среднем на всю работу фреймворка уходит 0.18 сек из них 0.015 сек. на запросы к БД на заполение массивов полями таблиц. Следовательно прирост поменьше — 2.8%
Конечно в реальном проекте цифры будут сильно зависеть от самого кода и работы с ORM. Ожидаемое уменьшение количества запросов к БД — 1.5 — 3 раза в проекте использующем ORM, что очень разгрузит сервер MySQL. Но запросы повторяются одинаковые и кешируются MySQL — поэтому конкретный прирост скорости будет в районе 2-3%.
Явный минус решения один — на проекте, который работает на лайв сервере и параллельно активно разрабатывается — нужно дописывать каждое новое поле в массив $_table_columns вручную, а для новых таблиц генерировать весь массив.
P.S. Соавтор статьи — unix44, кто не жадный — может дать инвайт.
Автор: Skull