← На главную

Шпаргалка по работе с DBIx::Class

Хорошо продуманный 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):

dbicdump -o dump_directory=./lib My::Namespace::Schema \ 'dbi:mysql:blojek:localhost:3306' user qwerty

В коде пишем:

use My::Namespace::Schema; # ... 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-запрос:

my $user_rs = $schema->resultset('User')->create({ login => $login, pass => $hash, });

Не правда ли, это удобнее, чем писать свою функцию createUser?

Следует проверять генерируемые запросы, дабы испытывать уверенность в их эффективности. Чтобы увидеть SQL запрос, сгенерированный DBIx::Class, воспользуемся переменной окружения DBIC_TRACE:

DBIC_TRACE=1 ./adduser.pl

Увидим следующее:

INSERT INTO `users` ( `login`, `pass`) VALUES ( ?, ? ): 'test', 'd8578edf8458ce06'

Обратите внимание, что таблица называется «users», но соответствующий ей класс называется «User», в единственном числе.

SELECT-запросы пишутся следующим образом:

my $book_rs = $schema->resultset('Book')->search({ 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() без аргументов. Также этот метод может быть вызван с двумя аргументами:

my $book_rs = $schema->resultset('Book')->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:

# SELECT conf_id FROM configs ORDER BY version DESC LIMIT 1 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 будут помещены указатели на хэши, ключи и значения которых соответсвуют именам и значениям столбцов таблицы:

my $log_rs = $schema->resultset('Log')->search(undef, { order_by => { -desc => 'tstamp' }, rows => 100 }); $log_rs->result_class('DBIx::Class::ResultClass::HashRefInflator'); my @list = $log_rs->all();

Метод all() возвращает все найденные строки. Метод result_class() – это аксессор к классу, который используется для создания объектов, соответствующих строкам таблицы. В данном случае вместо объектов возвращаются обычные хэши.

Для поиска строки по первичному или уникальному ключу вместо search() удобнее использовать find():

my $user = $schema->resultset('User')->find($user_id); print $user->login."\n";

Важно понимать, когда следует использовать find(), когда single(), а когда search(). Например, find({ login => $login, pass => $hash }) будет искать строку только по уникальному ключу, то есть login. Подумайте, к какому нежелательному последствию это может привести. Вместо find() в данном случае следует использовать single().

UPDATE-запросы пишутся так:

# UPDATRE users SET birthday = ? WHERE user_id = ? $user->birthday($new_birthday); $user->update;

или, например, так:

my $post_rs = $schema->resultset('Post'); # важно: в update() - не строка, а указатель на строку $post_rs->search({ uid => $uid })->update({ karma => \'karma - 1' });

Аналогично с DELETE-запросами:

$user->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;

Напоследок – пример использования транзакций:

use Try::Tiny; #... $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