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

в 17:26, , рубрики: Kohana, orm, php, метки: ,

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("/(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 в распечатанном объекте модели.
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, кто не жадный — может дать инвайт.

Автор: Skull

Поделиться

* - обязательные к заполнению поля