Шпаргалка по работе с 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):

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

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.