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, наверняка понравится.

perl, ORM, Rose::DB
26 октября, 1:04
1730

Комментарии

А чего его искать? redhat.com

Оставьте свой комментарий