Проблема
При создании объекта модели с помощью 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("/(class\sModel_)(\w+)?(\sextends)/", $file_text, $match); $model_name = preg_replace("/(class\sModel_)(.*?)(\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, кто не жадный — может дать инвайт.
ссылка на оригинал статьи http://habrahabr.ru/post/154865/
Добавить комментарий