Слайд 1Rose::DB
Perl
ORM
Быстрый страт
Слайд 2Создаём класс с коннектом к базе
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;
Слайд 3При этом, учитываем, что в 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
Слайд 4Выливаем классы таблиц из базы
#!/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» символов.
Слайд 5Допустим у вас есть класс My::DB::MlPerson для получения объекта строки,
с
person_id достаточно дать:
$p = My::DB::MlPerson->new(person_id=>74952020)->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=>74952020,password=>'pass');
$p->save;
В данном контексте (создание записи), в конструктор совсем необязательно передавать авто-инкрементируемый ключ.
Удаление записей абсолютно бесхитростно:
$p = My::DB::MlPerson->new(person_id=>74952020);
$p->delete;
Как видно, load в данном случае не требуется.
Слайд 6Теперь перейдём к вопросам более сложных запросов в базу
Когда вы выливали
структуру таблицы в классы, то для каждой таблицы
были созданы 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;
}
Слайд 7Но вот когда нам требуется обработать набор строк, вступает в свою
силу 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(...);
}
Ахтунг на лицо. И всё было бы совсем плохо, НО …
Слайд 8НО эти методы умеют принимать аргументы для уточнения результатов поиска и
возврата соответствующей ссылки на массив или итератор:
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
Слайд 9Далее,
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 }
Слайд 10Вы всегда можете получить объект 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.
Слайд 11Пусть у нас будут описаны в 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;
Рассмотрим зависимости.
Если у вас в базе они не были проставлены, то при генерации классов-таблиц, вы их то же не увидите.
Прелесть в том, что можно прописать их ручками.
Слайд 12Отредактируем 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=>74952020)->load;
$p->crm->any_field
где, any_field - это любое поле из таблицы crm_bridge.
Слайд 13В общем-то, в данном случае можно было бы и опустить 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, если вам так удобнее.
Слайд 14Что сразу не понравилось:
$p = My::DB::MlPerson->new(person_id=>74952020)->load;
$bor = $p->group;
ref $bor eq
'ARRAY';
другими словами – при обращении к отношению, вы получаете ссылку на массив или массив (в зависимости от контекста). На лицо явная избыточность. По документации не нашёл, как получить возможность обратиться к отношению с возможностью уточнения запроса (типа выбрать только конкретные группы для персоны) без выгребания всех записей из ml_group. И уж тем более, в продакшн всегда интереснее получить итератор, а тут #@я!
И, более того, по дефолту (а это очевидно настраивается), повторное обращение:
$p->group;
уже не стало лезть в базу (а ведь со стороны кто-то мог поменять значение в строках таблицы), а просто отдало значение из Кеша (!).
В общем, в данном вопросе, имхо, рулит DBIx::Class.
Что можно назвать удобным, так это удаление групп, в которых состоит персона:
$p = My::DB::MlPerson->new(person_id=>74952020)->load;
$p->group([ ]);
$p->save;
А вот пример тотально удаления всех связанных строк:
$p->delete(cascade => 1);
Для углубления вопроса по тому как составлять квери, рекомендую посмотреть Rose::DB::Object::Manager, где к сожалению не нашёл примера по вызову хранимок.
Тему сисек считаю не раскрытой, но для быстрого старта – вполне достаточно.