Выпуск 20. Октябрь 2014
← Работа с API GitHub в Perl | Содержание | Обзор CPAN за сентябрь 2014 г. →Введение в Rose::DB::Object
Рассмотрено использование простейших возможностей ORM-пакета Rose::DB::Object
Автор пишет, что Rose::DB::Object
— “object-relational mapper (ORM)”. Это на 50% верно. Мы можем получить доступ к базе, но не можем создать объект Perl и создать в базе структуру под него. То есть это удобная замена SQL-запросам, не более.
Что нам даст Rose::DB::Object
Мы получаем возможность простые операции делать ещё проще, но при этом сложные операции становятся очень запутанными и нетривиальными. Реально можно использовать выборку с не очень сложными условиями, создание (INSERT) — удобно, обновление (UPDATE) — очень удобно, удаление (DELETE) — не сильно удобнее чем с DBI
.
Удобно использовать Rose::DB::Object
в операциях, затрагивающих один объект. В этом случае задействуется вся мощь внешних ключей, автоматического преобразования дат в объекты DateTime
, обнаружение некоторых неправильно передаваемых данных ещё до обращения к базе данных (используется ранее созданная модель базы). Использовать в операциях с большим числом объектов уже надо очень осторожно. При этом Rose::DB::Object
, по сообщениям автора, в 20 раз производительней единственного прямого конкурента — DBIx::Class
. Мой опыт — надо совмещать чистый SQL и Rose.
Текст ниже описывает основные операции. Для того, чтобы протестировать все примеры, обращаётесь к репозиторию https://github.com/Pilat66/RoseDBObject. Примеры из этой статьи находятся в файле example_1.pl
Структура тестовой базы данных
База данных у нас будет из двух таблиц.
my $sql_create = <<END;
CREATE TABLE authors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT
);
CREATE TABLE books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
editor_id INTEGER,
author_id_1 INTEGER,
author_id_2 INTEGER,
FOREIGN KEY (editor_id) REFERENCES authors (id) ,
FOREIGN KEY (author_id_2) REFERENCES authors (id) ,
FOREIGN KEY (author_id_1) REFERENCES authors (id)
);
END
В таблице books
нетривиальный момент то, что есть два почти одинаковых столбца author_id_1
и author_id_2
— они нужны для демонстрации дописывания генератора модулей в разделе ’Создание классов Rose
и ORM::ConventionManager
.
В стиле DBI
Сначала короткий пример работы с нашей базы стандартными средствами. Я создаю тестовую базу данных и заполняю её тестовыми данными. Этот пример занимает одну страницу текста, следующий пример выполняет то же самое в стиле Rose::DB::Object
, но занимает в несколько раз больше строк текста :)
my $dbh = DBI->connect("dbi:SQLite:dbname=$dbname_dbi", "", "");
$dbh->{AutoCommit} = 0;
$dbh->do('PRAGMA foreign_keys = ON');
foreach (1 .. 3) {
$dbh->do("INSERT INTO authors(name) VALUES(?)", {}, "Author $_");
}
foreach (1 .. 3) {
$dbh->do( <<END, {}, "Book $_", 1, $_);
INSERT INTO books(name, author_id_1, author_id_2)
VALUES(
?,
(SELECT id FROM authors WHERE name like '%'||?),
(SELECT id FROM authors WHERE name like '%'||?)
)
END
}
$dbh->commit;
В стиле Rose::DB::Object
В отличии от предыдущего примера, перед работой с ORM Rose::DB::Object
надо провести дополнительную работу:
- подключиться к базе;
- сформировать несколько служебных классов, корректирующих поведение ORM;
- превратить существующую структуру базы данных в объектную модель;
- загрузить эту модель.
Довольно сложный комплекс действий. Собственно использование ORM начинается с раздела “Основные операции с базой”.
Главное, что надо сразу понять о Rose::DB::Object
— он (или оно) построен на системе статических классов (модулей Perl). Каждой схеме в базе соответствует ( в один момент времени свой набор модулей, который нельзя использовать для манипуляции другой такой же схемой или обращаться к той же схеме с другим именем пользователя.
Связано это с тем, что ради простоты использования, каждый модуль (отражающий структуру таблицы базы) имеет свой $dbh
для доступа к базе (точнее $dbh
прячется в объекте $db
, унаследованном от Rose::DB
), причём этот $dbh
используется неявно в конструкциях типа:
$p = Product->new(name => 'Sled');
$p->save;
Это конструкция создания новой строки в базе. Мы нигде не указываем о какой базе идёт речь — это указывается во время инициализации системы классов. Правда, указать всё же можно, но тогда нарушается гармония:
$p = Product->new(db => $db, name => 'Sled');
так как этот $db
ещё надо как-то хранить, создавать, и вообще заботиться о нём. Потом об этом напишу подробнее.
Второе, что надо знать. Я пишу исключительно с использованием Rose::DB::Object::Loader
— то есть сначала создаём базу данных, потом Loader
строит множество модулей, потом мы их инициализируем и используем. Теоретически можно модули (классы) создавать и самостоятельно; но для больших баз, а с маленькими Rose нет смысла использовать, это слишком сложно. Проще сделать скрипт, создающий дерево классов, и выполнять его при каждом изменении базы.
Время от времени задаются в форумах вопросы типа “можно ли обратить процесс, то есть создавать структуру базы данных по Rose
-модели”. Ответ на это — можно, так как модель имеет всё необходимое для этого, но Rose
это не делает. И делать это не нужно, всё таки ORM в Perl это совсем не то же что Hibernate в Java.
Настройка Rose::DB::Object
Дополнительные классы
Дополнительные классы позволят изменить метод подключения к базе данных и правила автосоздания классов Rose по существующей базе данных.
ORM::DB
, ORM::DB_Object
, ORM::ConventionManager
в настоящем проекте надо описывать в отдельных файлах. Я их сделаю прямо тут. ORM — это префикс дерева классов, его можно выбрать любым или вообще не использовать, но тогда может возникнуть путаница.
ORM::DB
ORM::DB
содержит описание подключения к нашей базе.
В данном случае это псевдоподключение, так как будет использоваться метод, отличный от стандартного. То, что необходимо — сообщить Rose
тип базы данных:
package ORM::DB;
use strict;
use base qw(Rose::DB);
__PACKAGE__->use_private_registry;
__PACKAGE__->register_db(driver => 'sqlite',);
1;
ORM::DB_Object
ORM::DB_Object
— это тот класс, который rose будет использовать вместо Rose::DB::Object
. Нам нужно только переопределить метод init_db()
.
package ORM::DB_Object;
use strict;
use base qw(Rose::DB::Object);
our $DB;
sub init_db {
my $self = shift;
return $DB;
}
1;
ORM::ConventionManager
ORM::ConventionManager
переопределяет некоторые алгоритмы создания классов Rose::DB
по существующей структуре базы данных с помощью Rose::DB::Object::Loader
.
auto_foreign_key_name()
позволяет изменить стандартные имена для ссылки на внешние таблицы, table_to_class()
позволяет определить имена классов, соответствующих таблицам в модели. Например, для таблицы authors можно выбрать класс ORM::Authors
, ORM::Author
, ORM::authors
, ORM::author
или любой другой. По-умолчанию, Rose::DB::Object::Loader
применяет хитрые алгоритмы создания имён, настолько хитрые, что это приносит больше вреда чем пользы и создаёт проблемы. Для создания имён есть встроенные флаги, но проще всего иметь имена классов, совпадающих с именами таблиц в нижнем регистре. Но это всё на любителя. Но на практике имена таблиц бывают очень разные.
package ORM::ConventionManager;
use base qw(Rose::DB::Object::ConventionManager);
sub auto_foreign_key_name {
my ($self, $f_class, $current_name, $key_columns, $used_names) = @_;
my $name;
if ($f_class eq 'ORM::authors') {
$name = 'author_primary' if ($key_columns->{author_id_1});
$name = 'author_secondary' if ($key_columns->{author_id_2});
}
else {
$name = Rose::DB::Object::ConventionManager::auto_foreign_key_name(@_);
}
return $name;
}
sub table_to_class {
my ($self, $table, $prefix, $plural) = @_;
$table = lc $table;
return ($prefix || '') . $table;
}
1;
Подключение к базе данных, создание объекта Rose::DB
$dbh
my $dbh = DBI->connect("dbi:SQLite:dbname=$dbname_rose", "", "");
$dbh->{AutoCommit} = 0;
private_pid
и private_tid
Rose::DB
проверяет параметры private_pid
и private_tid
, поэтому надо их создать.
$dbh->{'private_pid'} = $$;
$dbh->{'private_tid'} = threads->tid if ($INC{'threads.pm'});
$schema
$schema
содержит имя схемы базы данных.
У нас может быть несколько схем с одинаковой структурой, и мы можем указать, с какой схемой нужно работать. $db->schema($schema)
можно вызывать в любой момент. SQLite3 не имеет схем, Так что в этом примере $schema=undef
.
my $schema = undef;
my $db = ORM::DB->new();
$ORM::DB_Object::DB = $db;
$db->dbh($dbh);
$db->schema($schema) if $schema;
my $module_dir = './ormlib_auto';
В директории ormlib_auto
будут лежать сгенерированные модули описания базы данных. Перед созданием модулей надо либо очистить эту директорию, либо обеспечить её отсутствие в @INC
.
#system("rm -Rf $module_dir"); # надо сделать перед созданием модулей
system("mkdir -p $module_dir");
my $loader = Rose::DB::Object::Loader->new(
db => $db,
db_schema => $schema,
module_dir => $module_dir,
class_prefix => 'ORM',
db_class => 'ORM::DB',
base_classes => ['ORM::DB_Object'],
convention_manager => 'ORM::ConventionManager',
# include_views можно и установить в 1, но будут глюки.
# Да и Rose любит иметь приватные ключи, роль которых будет исполнять
# столбец из view, выбранный случайным образом.
include_views => 0,
# include_tables можно не указать, тогда будут использоваться все таблицы
# ( если require_primary_key==1, то только те таблицы,
# у которых есть primary key)
include_tables => [qw{ books authors }],
# exclude_tables содержит список таблиц, для которых
# не нужно создавать классы
exclude_tables => [],
# require_primary_key позволяет указать, создавать ли классы
# для таблиц без private key. Таблицы без primary key пораждают глюки,
# поэтому лучше их не загружать. Ну или аккуратно следить за тем,
# чтобы их использовать только в ::Manager-> запросах.
require_primary_key => 1,
# warn_on_missing_pk порождает сообщения о потенциальных проблемных таблицах
#warn_on_missing_pk => 1,
);
Создание классов Rose
Классы создать можно двумя способами: вызвать $loader->make_modules
или $loader->make_classes()
. Первый метод создаст файлы с модулями Perl в указанной module_dir директории, второй только создаст и загрузит сами классы.
Есть соблазн всегда пользоваться только make_classes()
и не замусоривать файловую систему. Делать это не надо по двум причинам:
- изучение созданных модулей крайне полезно для отладки;
- создание классов на базах данных с большим числом таблиц крайне ресурсоёмкое занятие. Создание классов может занимать 20 секунд, а их загрузка из файлов одну секунду.
Дополнительно о фрагменте:
post_init_hook => sub {
my $meta = shift;
$meta->{allow_inline_column_values} = 1;
},
Очень хорошая особенность Rose
— он позволяет вмешиваться в работу его компонентов на разных стадиях, например в данном случае после создания метаинформации для генерации объекта, соответствующего таблице, можно эту метаинформацию немного подправить. Конкретно allow_inline_column_values
в данном случае не нужно ни для чего, это только демонстрация. Вообще очень полезная опция, Rose может сам установить значения по-умолчанию для некоторых столбцов, для которых в баые указан параметр default
, а может позволить выполнить это самой СУБД. Причина простая — некоторые default
значения в базе на самом деле не константы, а функции (now()
в PostgreSQL, SYSDATE в Oracle), и Rose далеко не всегда справляется сам с опознанием таких объектов.
Rose::DB::Object::Loader
имеет кучу флагов, рекомендую почитать о них. Так же полезно посмотреть исходники — они небольшие по размеру, и позволяют понять некоторые нетривиальные вещи.
my @classes = $loader->make_modules(
db => $db,
post_init_hook => sub {
my $meta = shift;
$meta->{allow_inline_column_values} = 1;
},
);
ddx @classes;
# "ORM::authors",
# "ORM::authors::Manager",
# "ORM::books",
# "ORM::books::Manager",
push @INC, 'ormlib_auto';
Подключение классов ORM::
В данном примере загружать классы не нужно, так как Loader
их уже загрузил. Но в реальности Loader
запускается один раз и сохраняет классы в файлах, потом классы только загружаются без вызова Loader
.
Ещё надо не забывать загружать ORM::DB
, ORM::DB_Object
, ORM::ConventionManager
. Мы их уже загрузили, да и вообще их нет в файловой системе, так что это не делаем, но помним что в реальном проекте сделать надо, например
foreach(@classes){
load($_);
}
foreach (1 .. 3) {
my $author = ORM::authors->new(name => "Author $_");
$author->save();
}
ddx $dbh->selectall_arrayref("SELECT * FROM authors", {Slice => {}});
# { id => 1, name => "Author 1" },
# { id => 2, name => "Author 2" },
# { id => 3, name => "Author 3" },
#my $authors = ORM::authors::Manager->get_authors();
#ddx $authors;
foreach (1 .. 3) {
ORM::books->new(
name => "Book $_",
editor => ORM::authors->new(id => 1)->load,
author_primary => ORM::authors->new(id => 1)->load,
author_secondary => ORM::authors->new(id => $_)->load,
)->save();
}
ddx $dbh->selectall_arrayref("SELECT * FROM books", {Slice => {}});
# { author_id_1 => 1, author_id_2 => 1, id => 1, name => "Book 1" },
# { author_id_1 => 1, author_id_2 => 2, id => 2, name => "Book 2" },
# { author_id_1 => 1, author_id_2 => 3, id => 3, name => "Book 3" },
Основные операции с базой
Теперь быстренько пройдёмся по Rose::DB::Object::Tutorial
. У нас есть таблицы authors
и books
. Над ними и поработаем.
Select — выборка данных
Загрузим автора, которого только что создали, и проверим, есть ли такой. ORM::author->new(id => 1)
только создаёт объект в памяти, чтобы наполнить его данными из базы, надо вызвать функцию load()
. Чтобы записать в базу — save()
. Если объекта в базе нет, вызывается исключение. Параметр speculative => 1
как раз предотвращает это.
my $author = ORM::authors->new(id => 1);
unless ($author->load(speculative=>1)) {
warn("Нет такого автора");
}
Но это загрузка только одного объекта, по его уникальному идентификатору. Можно загрузить несколько объектов, выполнив для них SQL-запрос. Подробно язык запросов описан в Rose::DB::Object::Manager
.
Загрузка нескольких объектов выполняется двумя способами — аналоги функций DBI $dbh->selectall_arrayref()
и $sth->fetchrow_hashref()
. Первый способ подходит для небольших выборок, но он должен быть существенно быстрее второго, второй не загружает сразу все объекты в память, поэтому позволяет обработать любые объёмы информации. Сначала первый способ:
my $authors = ORM::authors::Manager->get_authors(
query => [
or => [
name => {
'like' => '%1%'
},
name => {
'like' => '%2%'
},
],
],
select => ['name'],
limit => 2,
offset => 0,
debug => 1
);
На экран выводится текст запроса, который пойдёт в базу, и подставляемые в него параметры:
SELECT
t1.name
FROM
authors t1
WHERE
(
t1.name LIKE ? OR
t1.name LIKE ?
)
LIMIT 2 OFFSET 0 (%1%, %2%)
Опции:
debug => 1
заставит Rose вывести на консоль текст сформированного запроса;limit => 1
— вывести одну строку, начиная с offset строк;offset => 0
— пропустить ноль строк;select => ['name']
— выбрать только столбецname
, если этото параметр не указывать, выбираются все столбцы.
В $authors
будет содержаться массив объектов, соответствующих выбранным строкам таблицы.
Теперь второй способ, создадим итератор: разница только в функции get_authors_iterator()
для выборки объектов.
my $authors = ORM::authors::Manager->get_authors_iterator(
query => [or => [name => {'like' => '%1%'}, name => {'like' => '%2%'},],],
select => ['name'],
limit => 2,
offset => 0,
debug => 1
);
while (my $author = $authors->next) {
ddx [$author->id, $author->name];
}
ddx "Total row(s):" => $authors->total;
Вне зависимости от метода, которым мы выбираем записи, мы получаем объекты, унаследованные от Rose::DB::Object
— один объект, или массив объектов.
get_authors
и get_authors_iterator
— функции, их имена определяются в ConventionManager классе, и схему их образования можно изменить.
Получение и изменение значений столбцов
Для каждого столбца таблицы создаётся функция (getter
/setter
) с именем, по умолчанию совпадающим с именем столбца (можно изменить в ConventionManager
).
Не забываем, что мало вызвать $author->name('Author 1 New Name')
, надо после этого сделать $author->save()
и, возможно, $dbh->commit()
(в нашем случае точно надо, так как базу мы открывали с опцией AutoCommit => 0
).
my $author = ORM::authors->new(id => 1);
$author->load();
$author->name('Author 1 New Name');
$author->save();
Insert — добавление (создание) данных
my $new_author = ORM::authors->new(name => "Author 4");
$new_author->save();
Разница между получением данных и обновлением
Несмотря на то, что в обоиих случаях используется метод new()
, при получении данных мы должны указать уникальный идентификатор строки — первичный ключ или значение поля с уникальным индексом, после чего вызвать метод load()
. Для создания данных первичный ключ можно не указывать, нужно указать значения для обязательных (not null
) столбцов и вызвать метод save()
.
ОЧЕНЬ ВАЖНЫЙ МОМЕНТ!
При создании строк в таблице надо как-то задать значение для PRIMARY KEY
столбцов. Можно сделать это в явном виде? указав значение столбца. Rose
может сделать это автоматически, при соблюдении некоторых условий. Например, для PostgreSQL первичные ключи задаются с типом serial, который СУБД разворачивает в тип Integer (или BigInt), и создаёт последовательность с именем в стандартном формате. У MySQL есть встроенный тип AUTO_INCREMENT
. У Oracle нет ничего, поэтому Rose
предполагает, что для первичных ключей есть последовательность с именем:
my $name = join('_', $table, $column, 'seq');
Имя последовательности определяется в ConventionManager
. Если Ваши имена последовательностей строятся иначе, можно в ORM::ConventionManager
переназначить функции auto_primary_key_column_sequence_name()
и auto_column_sequence_name()
из Rose::DB::Object::ConventionManager
.
Update — Обновление
Обновление так же просто — загружаем, обновляем, сохраняем.
my $author = ORM::authors->new(id => 1);
$author->load();
$author->name('Author 1 New New Name');
$author->save();
$author->load();
Если что-то хотим сделать потом с объектом, иногда надо повторно прочитать его из базы — при сохранении он мог быть изменён триггером, или ещё по каким-то причинам измениться.
Если надо обновить несколько объектов, используем подкласс ::Manager
:
my $num_rows_updated = ORM::authors::Manager->update_authors(
set => {
name => {
sql => "name || ' update'"
},
},
where => [id => 1],
debug => 1
);
Delete — Удаление
Удалять можно один объект или группу объектов. Сначала удалим один объект:
my $author = ORM::authors->new(id => 4);
$author->delete();
Теперь удалим несколько объектов.
my $num_rows_deleted
= ORM::authors::Manager->delete_authors(where => [id => {ge => '5'}],);
Более сложные операции — задействуем связи между объектами
Предположим, что наши книги имеют по паре авторов и одного редактора editor.
Таблица authors:
{ authorid1 => 1, authorid2 => 1, id => 1, name => "Book 1" },
{ authorid1 => 1, authorid2 => 2, id => 2, name => "Book 2" },
{ authorid1 => 1, authorid2 => 3, id => 3, name => "Book 3" },
Таблица books
{ author_id_1 => 1, author_id_2 => 1, editor_id => 1, id => 1, name => "Book 1" },
{ author_id_1 => 1, author_id_2 => 2, editor_id => 1, id => 2, name => "Book 2" },
{ author_id_1 => 1, author_id_2 => 3, editor_id => 1, id => 3, name => "Book 3" },
Загрузим книгу:
my $book = ORM::books->new(id => 2)->load;
Теперь мы можем получить данные авторов, используя внешние ключи — просто вызвав:
$book->author_primary->name;
и
$book->author_secondary->name
Мы можем выбирать объекты, на которые ссылаемся, автоматически, по мере потребности. Побочный эффект от этого — дополнительные SQL запросы, то есть злоупотреблять этим нельзя. Правда, есть кэширование, но оно не панацея. Установим Rose::DB::Object::Debug
в 1 и посмотрим как это выглядит:
$Rose::DB::Object::Debug = 1;
my $book = ORM::books->new(id => 2)->load;
#SELECT author_id_2, editor_id, author_id_1, name, id
#FROM books WHERE id = ? - bind params: 2
print $book->editor->name;
#SELECT name, id FROM authors WHERE id = ? - bind params: 1
$Rose::DB::Object::Debug = 0;
Мы видим, что создался дополнительный SQL запрос. Затратно для большого количества книг, но при выводе информации об одной книге очень удобно.
Заключение
Рассмотрены базовые возможности. Вообще не описаны операции с связями типа many-to-many (многие-ко-многим), так как они для практического использования сложноваты. Не рассмотрены возможности выборок с связанными таблицами (имитация JOIN) — это тема для отдельной статьи. Например, из документации:
$products =
Product::Manager->get_products(
query =>
[
name => { like => 'Kite%' },
id => { gt => 15 },
],
require_objects => [ 'vendor' ],
with_objects => [ 'colors' ],
sort_by => 'name');
Получим SQL запрос
SELECT
t1.id,
t1.name,
t1.vendor_id,
t3.code,
t3.name,
t4.id,
t4.name,
t4.region_id
FROM
products t1
JOIN vendors t4 ON (t1.vendor_id = t4.id)
LEFT OUTER JOIN product_colors t2 ON (t2.product_id = t1.id)
LEFT OUTER JOIN colors t3 ON (t2.color_code = t3.code)
WHERE
t1.id > 15 AND
t1.name LIKE 'Kite%'
Мне кажется, что такого рода запросы проще делать руками. Но, используя знания о внешних ключах и связях между таблицами, такие запросы строятся просто. Сложно потом искать ошибки.
Сразу о замеченных тёмных местах в Rose
Огромная проблема — таблицы и столбцы с именами в смешанном регистре. Таблицу
CREATE TABLE "SmartTablE" ("Id": serial PRIMARY KEY, "NaMe": character varying);
использовать практически (почти) невозможно в существующей реализации.
Поля и таблицы, совпадающие с именами внутренних переменных Rose, придётся переименовать в ConventionManager
.
← Работа с API GitHub в Perl | Содержание | Обзор CPAN за сентябрь 2014 г. →