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