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’

Обратите внимание, что таблица называется «user s », но соответствующий ей класс называется «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

EnglishRussianUkrainian