Rose::DB Perl ORM Быстрый страт
Создаём класс с коннектом к базе package My::DB; use warnings; use strict; use base qw(Rose::DB); __PACKAGE__->use_private_registry; __PACKAGE__->register_db( driver => 'mysql', type => 'main', database => 'databasename', host => '*****.***.***', username => 'user', password => '*****', connect_options => { AutoCommit => 1, RaiseError => 1, } ); 1;
При этом, учитываем, что в Rose::DB существует 2 параметра подключения к базе: type и domain. Т.о. вы можете варьировать между девелоперской и продакшн базой с одинаковыми структурами данных. Для это можно описать несколько вызовов метода register_db. В документации приводится даже пример: use My::DB; if($ENV{'MYCORP_PRODUCTION_SERVER'}) { My::DB->default_domain('production'); } else { My::DB->default_domain('development'); } Аналогично можно указывать и default_type для коннекта. Rose::DB::Cache – тут описывается вариант работы под mod_perl с/без использования Apache::DBI
Выливаем классы таблиц из базы #!/usr/bin/perl use strict; use warnings; use lib '/home/****/'; use My::DB; use Rose::DB::Object::Loader; my $loader = Rose::DB::Object::Loader->new( db => My::DB->new('main'), class_prefix => 'My::DB', with_foreign_keys => 1, with_relationships => 1 ); $loader->make_modules(module_dir=>'/home/****/',exclude_tables=>'Tmp'); exit; В данном случае передаётся доп параметр 'Tmp', чтобы исключить таблицы /Tmp/ (regexp) из итогового набора таблиц. При этом учитываем, что если у вас уже есть классы с описанием таблиц (со своими кастомными добавками), то они просто перезатрутся. Так же нужно учитывать, что если у вас есть поля с типом char(х), то это будет отмечено в классах, и когда вы будете забирать значения из из объекта строки для данной таблицы, значение будет дополнено пробелами до «x» символов.
Допустим у вас есть класс My::DB::MlPerson для получения объекта строки, с person_id достаточно дать: $p = My::DB::MlPerson->new(person_id=> )->load В конструктор передаётся первичный или уникальный ключ. «Прогрузка» данных произойдёт только в момент load. Может так случиться, что в базе нет строки с указанным значением ключа, и тогда вам вернётся warn. Поскольку иметь такие отбивки не интересно, да и не наглядно, рекомендуется использовать конструкцию с использованием параметра speculative: unless($p->load(speculative => 1)) { warn "not found"; } Получим и изменим значение поля password для нашей персоны: $p->password; $p->password('new_pass'); При установке значения в поле, проверяется его валидность на соответствие в описании класса. Сохраним изменённый объект строки в базе (update): $p->save; Создадим новую запись в базе: $p = My::DB::MlPerson->new(person_id=> ,password=>'pass'); $p->save; В данном контексте (создание записи), в конструктор совсем необязательно передавать авто-инкрементируемый ключ. Удаление записей абсолютно бесхитростно: $p = My::DB::MlPerson->new(person_id=> ); $p->delete; Как видно, load в данном случае не требуется.
Теперь перейдём к вопросам более сложных запросов в базу Когда вы выливали структуру таблицы в классы, то для каждой таблицы были созданы 2 модуля (на примере MlPerson): My::DB::MlPerson My::DB::MlPerson::Manager Идеология состоит в том, что если вам требуется создать некий дополнительный метод для таблицы, который бы возвращал обработанное значение имеющихся полей, то это удобно делать в My::DB::MlPerson. Примером является случай, когда у вас в поле хранится стерилизованная структура данных. Скажем, стерилизованная через Storable. Пример для такого поля stor_field: package My::DB::MlPerson; …. use Storable qw(thaw freeze); $Storable::interwork_56_64bit = 1; sub alias_to_stor_field{ my ($row_obj,$new_val) if ($new_val and ref $new_val) { # ставим новое значение $row_obj->stor_field(freeze($new_val)); return 1; } elsif ($row_obj->stor_field) { # десериализуем имеющееся return thaw($row_obj->stor_field); }; return undef; }
Но вот когда нам требуется обработать набор строк, вступает в свою силу My::DB::MlPerson::Manager. В самом модуле вы увидите: __PACKAGE__->make_manager_methods('НазваниеТаблицыВБазе'); Этот метод делает доступными следующие методы для класса My::DB::MlPerson::Manager: get_НазваниеТаблицыВБазе get_НазваниеТаблицыВБазе_iterator get_НазваниеТаблицыВБазе_count delete_НазваниеТаблицыВБазе update_НазваниеТаблицыВБазе Итак, для нашей таблицы ml_person, это будут: My::DB::MlPerson::Manager->get_ml_person возвращает ссылку на массив всех строк в таблице (что удобно в очень редких случаях, когда обращаешься по индексу, а не разыменовываешь массив. И, всё равно, не удобно.) My::DB::MlPerson::Manager->get_ml_person_iterator возвращает итератор для обхода всей таблицы. my $i = My::DB::MlPerson::Manager->get_ml_person_iterator; while( my $p = $i->next ) { print $p->name; $i->finish if(...); } Ахтунг на лицо. И всё было бы совсем плохо, НО …
НО эти методы умеют принимать аргументы для уточнения результатов поиска и возврата соответствующей ссылки на массив или итератор: my $i = My::DB::MlPerson::Manager->get_ml_person_iterator( query => [ name => {like => '%Hat'}, person_id => {ge => 7}, or => [ age => 15, age => {lt => 10}, ], sort_by => 'name', limit => 10, offset => 50 ); while( my $p = $i->next ) { print $p->name; $i->finish if(...); } Эти аргументы эквивалентны к вере: SELECT person_id, name, age,…,password FROM ml_person WHERE name LIKE '%Hat' AND person_id >= 7 AND (age = 15 OR age < 10.00) ORDER BY name LIMIT 10 OFFSET 50
Далее, My::DB::MlPerson::Manager->get_ml_person_count возвращает число записей в таблице. Так же умеет принимать аргументы, чтобы конкретизировать where в запросе на подсчёт. My::DB::MlPerson::Manager->delete_ml_person удаляет записи в соответствии с переданными аргументами. My::DB::MlPerson::Manager->update_ml_person обновляет записи в соответствии с переданными аргументами. Здесь стоит отметить, как передаётся аргумент set в верю: My::DB::MlPerson::Manager->update_ml_person( set => { age => 25, }, where => [ age => 24, person_id => { gt => 100 }, ] ); В любом из классов-описаний таблиц, вы можете выбрать стиль подключения, если перегрузите метод: Собственное подключение к базе: sub init_db { My::DB->new } Использовать уже созданное или новое: sub init_db { My::DB->new_or_cached }
Вы всегда можете получить объект My::DB $p = My::DB::MlPerson->new(...); $db = $p->db; И, организовать транзакционный механизм (если авто комет отключен): $p = My::DB::MlPerson->new(...); $db = $p->db; $db->begin_work; # Начало транзакции # Используем наш $db для каждого создаваемого объекта-строки $p1 = My::DB::MlPerson->new(name => 'Bike', db => $db); $p1->save; $p2 = My::DB::MlPerson->new(name => 'Sled', db => $db); $p2->save; $p3 = My::DB::MlPerson->new(name => 'Kite', db => $db); $p3->save; if(...) # Применяем изменения или откатываемся { $db->commit; } else { $db->rollback; }; А теперь вернёмся к My::DB и возможности регистрировать несколько коннектов к базам, играя параметрами type и domain.
Пусть у нас будут описаны в My::DB два коннекта для баз с одинаковыми структурами. Одна с domain=>production и вторая с domain=>archive. Назначение баз понятно. $production_db = My::DB->new('production'); $archive_db = My::DB->new('archive'); # Загрузим с продакшн базы данные по конкретной персоне $p = My::DB::MlPerson->new(person_id => 'John', db => $production_db); $p->load; # Скопируем данные по персоне в архив $p->db($archive_db); $p->save(insert => 1); # аргумент insert позволит либо записать строку, либо обновить значения # Удалим обозначенную персону из продакшн базы $p->db($production_db); $p->delete; Рассмотрим зависимости. Если у вас в базе они не были проставлены, то при генерации классов-таблиц, вы их то же не увидите. Прелесть в том, что можно прописать их ручками.
Отредактируем My::DB::MlPerson и добавим в метод setup вот такой аргумет: foreign_keys => [ crm => { class => 'My::DB::CrmBridge', key_columns => { person_id => 'f_person_id' }, relationship_type => 'one to one', }, ] Итак, что у нас здесь: создана связь между классами (не таблицами): My::DB::MlPerson и My::DB::CrmBridge. следующим образом: ml_person.person_id является внешним ключом для crm_bridge.f_person_id. Соответствие определено как один-к-одному. Обращение к отноению идёт по методу с именем crm. relationship_type может так же быть записан как rel_type (синоним) И, теперь мы можем сделать вот такую штуку: $p = My::DB::MlPerson->new(person_id=> )->load; $p->crm->any_field где, any_field - это любое поле из таблицы crm_bridge.
В общем-то, в данном случае можно было бы и опустить rel_type. Да и к тому же, можено вообще вынести этот аргумент в виде вызова метода (но очь важно, чтобы это было определено до вызова setup): package My::DB::MlPerson; __PACKAGE__->meta->foreign_keys( 'crm' => { class => 'My::DB::CrmBridge', key_columns => { person_id => 'f_person_id' } } ); __PACKAGE__->meta->setup(…………) А вот, интересный случай когда персона входит в несколько групп (в том же классе My::DB::MlPerson до setup): __PACKAGE__->meta->relationships ( group => { type => 'one to many', class => 'My::DB::MlGroup', column_map => { person_id => 'f_person_id' }, }, ); Здесь обозначено отношение один-ко-многим (одна персона из ml_persons может иметь несколько записей в ml_group). Как уже понятно, можно обозначит это отношение и в виде аргумента в методе setup, если вам так удобнее.
Что сразу не понравилось: $p = My::DB::MlPerson->new(person_id=> )->load; $bor = $p->group; ref $bor eq 'ARRAY'; другими словами – при обращении к отношению, вы получаете ссылку на массив или массив (в зависимости от контекста). На лицо явная избыточность. По документации не нашёл, как получить возможность обратиться к отношению с возможностью уточнения запроса (типа выбрать только конкретные группы для персоны) без выгребания всех записей из ml_group. И уж тем более, в продакшн всегда интереснее получить итератор, а тут И, более того, по дефолту (а это очевидно настраивается), повторное обращение: $p->group; уже не стало лезть в базу (а ведь со стороны кто-то мог поменять значение в строках таблицы), а просто отдало значение из Кеша (!). В общем, в данном вопросе, имхо, рулит DBIx::Class. Что можно назвать удобным, так это удаление групп, в которых состоит персона: $p = My::DB::MlPerson->new(person_id=> )->load; $p->group([ ]); $p->save; А вот пример тотально удаления всех связанных строк: $p->delete(cascade => 1); Для углубления вопроса по тому как составлять квери, рекомендую посмотреть Rose::DB::Object::Manager, где к сожалению не нашёл примера по вызову хранимок. Тему сисек считаю не раскрытой, но для быстрого старта – вполне достаточно.