Избавление Kohana ORM от лишних запросов к БД

от автора

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         ) 	... 

На скриншоте видно последний запрос к БД (кликабельно)
image

Причем 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();
Код самого класса — под спойлером

смотреть код класса Optimize

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 в распечатанном объекте модели.
image

Другие найденные рещения — 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *