Шпаргалка по работе с DBIx::Class
28 марта 2012
Хорошо продуманный ORM может существенно упростить жизнь программисту. Но если это так, то откуда берутся крики, что «ORM — это антипаттерн»? Думается, дело в том, что не все ORM одинаково хороши (ORM для C++ из этой хабрастатьи просто ужасны). В этой заметке речь пойдет о DBIx::Class — хорошо продуманном и являющимся де-факто стандартным ORM для Perl.
Наши ожидания от ORM
Нормальный ORM должен обладать примерно такими свойствами:
- Гибкость, то есть мы по-прежнему можем писать SELECT, INSERT, UPDATE и DELETE запросы, делать JOIN, GROUP BY, ORDER BY и LIMIT, а также использовать транзакции и подзапросы — все как в SQL, просто другими словами;
- Совместимость с различными СУБД, в число которых как минимум должны входить MySQL, PostgreSQL, Oracle, Microsoft SQL Server, SQLite;
- Переносимость между поддерживаемыми СУБД — если приложение нормально работает с SQLite, то должно работать и с PostgreSQL без изменений в коде (на практике все же требуется тестирование совместимости);
- Производительность, сравнимая с SQL — там, где можно обойтись одним запросом, не должно использоваться десять (да, мы не можем использовать запросы типа «INSERT INTO … ON DUPLICATE KEY …», но это цена за переносимость, а не использование ORM);
- Более читаемый и лаконичный код, а такие функции, как createUser или updateUserInfo вообще должны быть сгенерированы автоматически;
- Проверка запросов на этапе компиляции (в Perl «компиляция» есть прогон модульных тестов);
- Одинаковые запросы генерируются одинаково с точностью до байта, что обеспечивает более эффективное кэширование со стороны СУБД;
- Различные оптимизации, например, соединение с СУБД не устанавливается, пока не будет выполнен первый запрос, и ни один запрос не выполняется до обращения к результатам его выполнения;
- Всякие плюшки типа кэширования, профилирования запросов, секционирования данных, поддержки плагинов, возможности восстановления соединения после разрыва и тп;
DBIx::Class вполне соответствует приведенному описанию. Да, использование ORM сопряжено с появлением некоторого оверхеда. Как и в случае с высокоуровневыми языками программирования. Но это же не мешает нам писать на Perl, Python или Java? Достаточно один раз поработать с DBIx::Class, и становится ясно, что оверхед того стоит.
Примеры использования DBIx::Class
Чтобы работать с базой данных, DBIx::Class’у требуется схема базы данных. Имеется в виду не UML диаграмма, а набор классов, описывающий БД. Проще всего создать схему с помощью утилиты dbicdump (см DBIx::Class::Schema::Loader):
'dbi:mysql:blojek:localhost:3306' user qwerty
В коде пишем:
# ...
my $schema = My::Namespace::Schema->connect(
$connect_string, $db_user, $db_pass, {
quote_names => 1,
mysql_enable_utf8 => 1,
});
Подробнее о mysql_enable_utf8 и аналогах для других СУБД можно прочитать здесь. На практике, чтобы не плодить соединения с БД, используется класс-одиночка, а параметры для соединения с БД читаются из конфига, например, с помощью DBIx::Class::Schema::Config.
Теперь попробуем написать какой-нибудь простой INSERT-запрос:
login => $login,
pass => $hash,
});
Не правда ли, это удобнее, чем писать свою функцию createUser?
Следует проверять генерируемые запросы, дабы испытывать уверенность в их эффективности. Чтобы увидеть SQL запрос, сгенерированный DBIx::Class, воспользуемся переменной окружения DBIC_TRACE:
Увидим следующее:
'test', 'd8578edf8458ce06'
Обратите внимание, что таблица называется «users», но соответствующий ей класс называется «User», в единственном числе.
SELECT-запросы пишутся следующим образом:
author_id => $author_id # WHERE aythor_id = ?
created => { '>', time() - 60*60*24 } # AND created > ?
status => [ $foo, $bar ] # AND (status = ? OR status = ?)
});
# запрос не будет выполнен до вызова ->next
while(my $book = $book_rs->next) {
print $book->title."\n";
print $book->status."\n";
}
Чтобы выполнить запрос «SELECT * FROM table» достаточно сделать вызов метода search() без аргументов. Также этот метод может быть вызван с двумя аргументами:
author_id => $author_id,
}, {
order_by => { -desc => 'created' }, # ORDER BY created DESC
rows => 10, page => 2, # LIMIT X, Y
columns => [qw/book_id title/], # SELECT book_id, title FROM ...
});
Первый аргумент может быть равен undef:
my $last_conf_id = $schema->resultset('Config')->search(undef, {
order_by => { -desc => 'version' },
rows => 1,
columns => [qw/conf_id/],
})->single->id;
Метод single() класса DBIx::Class::ResultSet похож на next(), но возвращает только первую найденную строку, которой соответствует класс DBIx::Class::Row. В свою очередь метод id() класса DBIx::Class::Row возвращает значение первичного ключа, в нашем примере — conf_id.
В следующем примере в список @list будут помещены указатели на хэши, ключи и значения которых соответсвуют именам и значениям столбцов таблицы:
{ order_by => { -desc => 'tstamp' }, rows => 100 });
$log_rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
my @list = $log_rs->all();
Метод all() возвращает все найденные строки. Метод result_class() — это аксессор к классу, который используется для создания объектов, соответствующих строкам таблицы. В данном случае вместо объектов возвращаются обычные хэши.
Для поиска строки по первичному или уникальному ключу вместо search() удобнее использовать find():
print $user->login."\n";
Важно понимать, когда следует использовать find(), когда single(), а когда search(). Например, find({ login => $login, pass => $hash }) будет искать строку только по уникальному ключу, то есть login. Подумайте, к какому нежелательному последствию это может привести. Вместо find() в данном случае следует использовать single().
UPDATE-запросы пишутся так:
$user->birthday($new_birthday);
$user->update;
или, например, так:
# важно: в update() - не строка, а указатель на строку
$post_rs->search({ uid => $uid })->update({ karma => \'karma - 1' });
Аналогично с DELETE-запросами:
$post_rs->search({ uid => $uid })->delete;
В DBIx::Class предусмотрены методы find_or_new(), find_or_create(), update_or_new() и update_or_create(), которые иногда бывают очень удобны:
my $domain_id = $schema->resultset('Domain')
->find_or_create({ domain => $domain },{ columns => ['domain_id']})
->id;
Напоследок — пример использования транзакций:
#...
$schema->storage->txn_begin();
try {
for my $id (@id_list) {
# тут какие-то запросы
}
$schema->storage->txn_commit();
} catch {
my $err = $_;
$schema->storage->txn_rollback();
# ...
};
Но по возможности лучше использовать txn_do() или txn_scope_guard().
Приведенное описание DBIx::Class никоим образом не претендуют на полноту. За кадром остались JOIN’ы, подзапросы, профайлинг и многое другое. Но я надеюсь, что вы уловили основы и при желании сможете разобраться в остальном самостоятельно. Во время изучения DBIx::Class я рекомендую держать открытой документацию к классу DBIx::Class::ResultSet. В ней содержатся ответы на большинство вопросов, которые могут у вас возникнуть. Также обратите внимание на DBIx::Class::Manual::Features.
Некоторые альтернативы DBIx::Class
Среди возможных альтернатив DBIx::Class хотелось бы отметить следующие:
- SQL::Abstract — генератор SQL запросов, переносимых между различными СУБД. Не поддерживает JOIN’ы. Сильно упрощает написание INSERT-запросов. Используется в DBIx::Class.
- SQL::Abstract::More — то же самое, только с поддержкой JOIN’ов.
- SQL::Maker — еще один генератор SQL запросов.
- DBIx::Custom — результат скрещивания генератора запросов с DBI.
- ORLite — согласно описанию, «экстремально легкий ORM, заточенный под SQLite». Оказывается, бывает и такое. Как по мне, довольно странная идея, ибо теряем по крайней мере одно из преимуществ ORM.
- Class::DBI — полноценный ORM. Давно не обновлялся. В некоторой степени на нем основан DBIx::Class.
- Class::DBI::Lite — ORM, родившийся из недовольства его автора модулем Class::DBI. Как и DBIx::Class, использует SQL::Abstract. Часто обновляется.
Существуют и другие ORM для Perl, например, Fey::ORM и Rose::DB. Впрочем, я вполне доволен DBIx::Class, в связи с чем не вижу острой необходимости в знакомстве с альтернативами.
Вопросы читателям
Пользовались ли вы когда-нибудь ORM и используете ли их в настоящее время? Если нет, то планируете ли попробовать ORM в будущем? Если да, то какой ORM и для какого языка вы используете/использовали и каковы ваши впечатления от него? Много ли общего c DBIx::Class у используемого/использованного вами ORM? Какие дополнительные аргументы вы могли бы привести за использование ORM или против него?
Дополнение: Особая благодарность выражается Peter Rabbitson за указания на неточности и дополнения к заметке.
Дополнение: Знакомьтесь — ORM для Scala под названием Slick
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.