Perl + ORM: Rose::DB::Object
Всем привет.
Наверняка, постоянные читатели нашего сообщества помнят месячник Perl 6, в рамках которого я писал небольшие заметки про шестой перл.
Что ни говори, а формат подобных серий статей мне нравится своей незамутненностью: не нужно мучительно придумывать темы, нужно лишь углубиться в ту область, которой сам сейчас увлекаешься.
Итак, мой новый гик — ORM. За последнее время на CPAN стали появляться они в том самом количестве, при котором можно уже задуматься о некоей тенденции.
Я не обещаю глубоких и глобальных обзоров, но рассказать о наиболее интересных сейчас реализациях ORM для Perl я все же попробую.
Сегодня поговорим не о DBIx::Class, как многие могли подумать, а о не менее, а для кого и более, интересном классе — Rose::DB::Object.
Как обычно, чтобы начать, нужно выполнить простую команду в терминале:
$ sudo cpan Rose::DB::Object
После чего все преимущества ORM вам станут доступны совершенно бесплатно =). Итак, что касается документации, то тут имеется небольшой туториал, который вполне способен дать довольно четкое представление о том, с чем нам придется столкнуться.
К особенностям рассматриваемого ORM можно отнести весьма недурную инкапсуляцию методов самого класса. Rose::DB::Object имеется в нескольких вариантах. В самом простом — это возможность работать с одной записью, как с объектом. Можно загружать объект, изменять его свойства, добавлять новые объекты в базу и так далее. Простейший CRUD.
На уровне повыше находится менеджер объектов, который, при желании, может существовать для каждой таблицы отдельно или для всей базы в целом. Менеджер содержит методы выборки массивов данных, используя сложную логику запросов и так далее.
Еще с помощью Rose::DBx::Object::Renderer можно рисовать получаемые данные сразу в HTML, в виде таблицы, формы или меню.
И это примерная схема моего рассказа. Начнем по порядку, с простых методов. Для создания простых методов достаточно создать несколько классов. Начнем с самого основного, который наследует класс Rose::DB и отвечает за подключение нашего ORM к базе данных. Пусть это будет My::DB.
package My::DB;
use base qw/Rose::DB/;
__PACKAGE__->use_private_registry;
__PACKAGE__->register_db(
driver => 'SQLite',
database => '/home/shootnix/bases/products_db',
);
Пытливый читатель наверняка догадался, что использовать в качестве примера мы будем SQLite. Кроме sqlite, Rose::DB может быть использована и с другими СУБД, например, MySQL или Pg.
Следующий класс, который может быть несказанно полезен и который нужно будет создать — My::DB::Object. Он наследует класс Rose::DB::Object. Нужен он для того, чтобы писать в нем новые методы нашей системы, которые потом бы использовались внутри будущих объектов.
package My::DB::Object;
use My::DB;
use base qw/Rose::DB::Object/;
sub init_db { My::DB->new() }
Все, больше пока ничего не нужно. Для начала нужно создать базу данных. Ничего, конечно, сложного: продукты и поставщики. Она будет выглядеть примерно так. Есть две таблицы, связанные друг с другом полем vendor_id, т.е. ID поставщика товара.
CREATE TABLE products (
id SERIAL NOT NULL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
vendor_id INT REFERENCES vendors (id),
UNIQUE(name)
);
CREATE TABLE vendors (
id SERIAL NOT NULL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
UNIQUE(name)
);
Каждая таблица в ORM — как бы отдельный класс. То есть, каждую таблицу в базе нужно описать в отдельном классе. Процесс этот достаточно нудный, чтобы впасть в уныние при виде своей сложной, многотабличной базы. Специально для тех, кто боится рутинной и бессмысленной работы, сделали класс Rose::DB::Object::Loader, который все классы создает автоматически. Но мне пока не сложно, у меня всего две таблицы:
package Vendor;
use base 'My::DB::Object';
__PACKAGE__->meta->setup(
table => 'vendors',
columns => [ qw/id name/ ],
pk_columns => 'id',
unique_key => 'name',
);
Это простой синтаксис описания схемы таблицы. Есть еще более изощренный способ, при котором подробно описывается каждый столбец. Рассмотрим этот способ на примере создания следующего класса Product:
package Product;
use base 'My::DB::Object';
__PACKAGE__->meta->setup(
table => 'products',
columns => [
id => { type => 'serial', not_null => 1 },
name => { type => 'varchar', length => 255, not_null => 1 },
price => { type => 'numeric', default => '0.00',
not_null => 1, precision => 2, scale => 10 },
vendor_id => { type => 'integer' },
],
primary_key_columns => [ 'id' ],
unique_keys => [ 'name' ],
foreign_keys => [
vendor => {
class => 'Vendor',
key_columns => { vendor_id => 'id' },
},
],
);
Обратите внимание: в этой схеме не только описывается структура таблицы, но и отношение этой таблицы к другой, 'vendors', с помощью ключа foreign_keys.
Так, теперь, кажется, все готово для того, чтобы попробовать сделать что-то совершенно простое (для начала). Записи в таблице — это всегда объект в понимании Rose::DB, поэтому, чтобы использовать эти данные, нужно взять класс (в нашем случае это таблица) и создать его объект (это и будет записью):
use Product;
my $p = Product->new(id => 1);
В конструктор передается первичный ключ таблицы, для того, чтобы метод возвратил единичную запись из базы. Но пока еще ничего не произошло, просто создался объект. Никаких запросов к базе не было, и переменная $p пока еще ничего не содержит. Для загрузки данных из базы используем метод load:
$p->load;
Можно сразу создать объект и загрузить в него данные:
$p = Product->new(id => 1)->load;
Для выборки другой записи, снова создаем объект:
$p2 = Product->new(id => 2)->load;
Каждое поле таблицы является акцессором/мутатором соответствующего объекта. Скажем, загрузил я запись в объект. Чтобы вывести название продукта, выполняю соответственно, метод-акцессор:
print $p->name;
А если я хочу изменить название продукта? Просто: этот же метод является мутатором, то есть, просто передав в него новый параметр, я изменю это поле на новое:
$p->name('Apple');
print $p->name # Apple
Что произошло? Я изменил свойство объекта. А что в базе? А в базе никаких изменений, т.к. запросов к базе не выполнялось. Чтобы отобразить все внесенные изменения в объекте на соответствующую запись в базе данных, нужно использовать метод save:
$p->name('Apple');
$p->save;
Таким образом работает update. Совершенно схожим же образом работает insert: просто не нужно указывать первичный ключ существующей записи. Укажите другой и сохраните:
$p = Product->new( id => 2, name => 'iPod', price => '199', vendor_id => 1 );
$p->save;
Ну и самое последнее здесь, это удаление. Тоже просто, обычный метод delete.
$p->delete;
При этом, можно удалить запись и недавно добавленную, отредактированную и так далее. То есть, любой объект можно удалить, если он существует.
Что делать со связанной таблицей, спросите вы. Ах да, у нас же есть таблица поставщиков товаров. Что ж, это просто, даже не надо учить ничего нового:
$p = Priduct->new(id => 1)->load;
print $p->name; # iPod
print $p->vendor->name; # Apple
На этом заканчивается часть «для новичков» и начинается настоящий экспириенс. Авторы и разработчики Rose::DB::Object решили вынести более сложную структуру запросов и, соответственно, методы их организующие, в отдельный класс Rose::DB::Object::Manager. Плохо это или хорошо, судить может каждый по-своему. Однако, это так.
Для того, чтобы расширить возможности класса Product, нужно создать еще один класс, например, Product::Manager:
package Product::Manager;
use base qw/Rose::DB::Object::Manager/;
__PACKAGE__->make_manager_methods('products');
Обратите внимание на последнюю строчку, в ней выполняется метод make_manager_methods, который создаст новые виртуальные методы:
get_products
get_products_iterator
get_products_count
delete_products
update_products
Да, в середине этих методов имеется ключевое слово 'products', которое мы указали сами. Красота, я считаю =).
Все, что теперь нужно, научиться пользоваться этими методами. Начнем с простого: получить из базы все продукты:
my $products = Product::Manager->get_products;
for my $p ( @$products ) {
print $p->name, "\n";
}
Не будем забывать про связанную таблицу:
my $products = Product::Manager->get_products;
for my $p ( @$products ) {
print $p->name, " - ", $p->vendor->name "\n";
}
Еще один стандартный пример: что, если объем данных настолько большой, что может нехватить оперативной памяти, или вы просто боитесь того, что время от времени ваше приложение, использующее Rose::DB::Object, будет использовать слишком много ресурсов сервера? Для того, чтобы избежать подобных страхов, в Rose::DB::Object создали итератор:
my $i = Product::Manager->get_products_iterator;
while( my $p = $i->next ) {
print $p->name;
}
Так или иначе, это все до сих пор очень просто, настоящий путь джедая Rose::DB::Object лежит именно в применении условий и дополнительных параметров в запросах. Например, так, как показано в примере:
$products = Product::Manager->get_products(
query => [
name => { like => '%Hat' },
id => { ge => 7 },
or => [
price => 15.00,
price => { lt => 10.00 },
],
],
sort_by => 'name',
limit => 10,
offset => 50
);
Этот метод будет соответствовать такому SQL-коду:
SELECT id, name, price FROM products WHERE
name LIKE '%Hat' AND
id >= 7 AND
(price = 15.00 OR price < 10.00)
ORDER BY name
LIMIT 10 OFFSET 50
Собственно, на этом общее введение заканчивается. Остальное я предлагаю для самостоятельного изучения, а изучать там еще есть чего, поверьте. Я же подведу небольшой итог.
Что касается удобства использования этого ORM, то я усматриваю в его интерфейсе прямую отсылку к одному из девизов самого Perl: сделать язык так, чтобы простые вещи можно было выполнить как можно проще, а сложные можно было выполнить в принципе =). Именно это и прослеживается в Rose::DB::Object: простые вещи выполняются очень просто, а сложные… по крайней мере, выполняются.
Еще одним несомненным плюсом я усмотрел обилие надстроек над этим классом, в частности, Rose::DBx::Object::Renderer, о котором я упоминал в начале статьи, или Rose::DB::Object::Loader — вообще незаменимая штука в процессе создания схемы БД. Ну или Rose::DBx::Object::Cached::CHI для кеширования и много чего еще.
И, конечно, немаловажным моментом является интеграция Rose::DB::Object в Catalyst и общая «зрелость» проекта. Лицам, постигшим дао ORM, наверняка понравится.

shootnix
Комментарии
РедХат ищем?