Выпуск 20. Октябрь 2014
Другие выпуски и форматы журнала всегда можно загрузить с pragmaticperl.com. С вопросами и предложениями пишите на почту .
Комментарии к каждой статье есть в html-версии. Подписаться на новые выпуски можно по ссылке pragmaticperl.com/subscribe.
Авторы статей: Вячеслав Коваль, Владимир Леттиев, Константин Токар
Обложка: Марко Иванык
Корректор: Андрей Шитов
Выпускающий редактор: Вячеслав Тихановский
Ревизия: 2014-11-29 16:24
© «Pragmatic Perl»
- От редактора
- Событийно-ориентированное программирование. Введение
- Локальная установка и использование Perl-модулей
- Работа с API GitHub в Perl
- Введение в Rose::DB::Object
- Что нам даст
Rose::DB::Object
- Структура тестовой базы данных
- В стиле
DBI
- В стиле
Rose::DB::Object
- Настройка
Rose::DB::Object
- Основные операции с базой
- Select — выборка данных
- Insert — добавление (создание) данных
- Update — Обновление
- Более сложные операции — задействуем связи между объектами
- Заключение
- Сразу о замеченных тёмных местах в
Rose
- Что нам даст
- Обзор CPAN за сентябрь 2014 г.
- Интервью с Леоном Тиммермансом
От редактора
В этом месяце выпуск несколько задержался. Но это зависело не совсем от нас и будет обязательно исправлено.
На прошлой неделе прошел австрийский Perl-воркшоп в Зальцбурге, на котором побывал Ларри Уолл, с ним можно почитать небольшое интервью.
В наших интервью мы обычно задаем вопросы от читателей. Через наш twitter сообщаем с кем будет интервью в следующем номере. Так можно задавать свои вопросы.
Друзья, журнал нуждается в новых авторах. Не упускайте такой возможности! Если у вас есть идеи или желание помочь, пожалуйста, с нами.
Приятного чтения.
Событийно-ориентированное программирование. Введение
Первая статья из цикла по событийно-ориентированному программированию
Данная статья будет первой в цикле статей по СОП (событийно-ориентированному программированию). Конечной целью данного цикла является понимание того, как работают EV и AnyEvent изнутри и как их применять более эффективно в реальных приложениях.
Первая часть цикла будет использовать C, чтобы показать, как работают:
- неблокирующий ввод-вывод (
open
/fcntl
+O_NONBLOCK
и ошибкаEWOULDBLOCK
(для сокетов)); - мультиплексирование ввода-вывода (
select
,poll
,epoll
(Linux 2.6+),kqueue
(BSD),eventfd
(Linux 2.6.27+), Input/output completion port (IOCP) (Windows NT 3.5+)); - как устроены библиотеки
libev
,libuv
иlibevent
и как с ними работать; - как устроен биндинг EV к libev;
Вторая часть цикла будет использовать Perl для того, чтобы описать работу модулей EV и AnyEvent, и, возможно, новый модуль UV.
Согласно Википедии, серверное приложение при использовании СОП (в нашем случае это AnyEvent+EV) реализуется на системном вызове, получающем события одновременно от многих дескрипторов (мультиплексирование). При обработке событий используются исключительно неблокирующие операции ввода-вывода, чтобы ни один дескриптор не препятствовал обработке событий от других дескрипторов. Далее рассмотрим мультиплексирование ввода-вывода.
Мультиплексирование ввода-вывода
Как сказано в книге Роберта Лава «Linux: системное программирование» (2-е издание, стр. 82), мультиплексированный ввод-вывод позволяет приложениям параллельно блокировать несколько файловых дескрипторов и получать уведомления, как только любой из них будет готов к чтению или записи без блокировки. По умолчанию файловые дескприторы (например, сокеты, каналы) блокируют выполнение процесса. Это означает, что когда мы вызываем на файловом дескрипторе функцию, которая не может выполниться немедленно, наш процесс переходит в “спящее” состояние и ждет, когда будет выполнено определенное условие. Для того, чтобы использовать неблокируемый ввод-вывод, необходимо использовать флаг O_NONBLOCK
для функций open или fcntl. Пример неблокирующего ввода-вывода мы рассмотрим в следующей статье, когда будем рассматривать сокеты и клиент-серверные приложения.
Для того, чтобы понять, как работают функции для мультиплексирования ввода-вывода, рассмотрим простой пример — программа блокируется, дожидаясь поступления ввода на стандартный ввод (stdin), блокировка может продолжаться вплоть до 5 секунд. Эта программа отслеживает всего один файловый дескриптор, поэтому здесь отсутствует мультиплексный ввод-вывод как таковой. Однако данный пример должен прояснить использование этих системных вызовов:
Функция select()
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define TIMEOUT 5 // Установка таймаута в секундах
#define BUF_LEN 1024 // Длина буфера считывания в байтах
int main() {
// Объявляем структуру для определения продолжительности времени ожидания
struct timeval tv;
// Ожидаем не дольше 5 секунд
tv.tv_sec = TIMEOUT;
tv.tv_usec = 0;
// Объявляем набор дескрипторов для чтения
fd_set readfds;
// Дожидаемся ввода на stdin.
FD_ZERO(&readfds); // Сбрасываем все биты в наборе readfds
FD_SET(STDIN_FILENO, &readfds); // Устанавливаем бит для стандартного ввода (STDIN)
/**
* Блокируем процесс, пока не поступят данные на STDIN, либо пока не
* истечет время TIMEOUT
* В качестве первого параметра передаем максимальный номер дескриптора
* + 1 */
int ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &tv);
if (ret == -1) {
perror("select");
return 1;
} else if (!ret) {
printf("%d seconds elapsed.\n", TIMEOUT);
return 0;
}
/**
* После возврата из функции select с помощью функции FD_ISSET проверяем,
* какие биты в наборе остались установленными
*/
if (FD_ISSET(STDIN_FILENO, &readfds)) {
char buf[BUF_LEN+1];
// Блокировка гарантированно отсутствует
int len = read(STDIN_FILENO, buf, BUF_LEN);
if (len == -1) {
perror("read");
return 1;
} else if (len) {
buf[len] = '\0';
printf("read: %s\n", buf);
}
return 0;
}
fprintf(stderr, "Этого быть не должно!\n");
return 1;
}
Функция poll()
#include <stdio.h>
#include <unistd.h>
#include <poll.h>
#define TIMEOUT 5 // Установка таймаута в секундах
#define BUF_LEN 1024 // Длина буфера считывания в байтах
int main() {
struct pollfd fds;
// Отслеживаем ввод на stdin
fds.fd = STDIN_FILENO;
fds.events = POLLIN;
// Выполняем блокирование
int ret = poll(&fds, 1, TIMEOUT * 1000);
if (ret == -1) {
perror("poll");
return 1;
}
if (!ret) {
printf("%d seconds elapsed.\n", TIMEOUT);
return 0;
}
if (fds.revents & POLLIN) {
char buf[BUF_LEN+1];
// Блокировка гарантированно отсутствует
int len = read(STDIN_FILENO, buf, BUF_LEN);
if (len == -1) {
perror("read");
return 1;
} else if (len) {
buf[len] = '\0';
printf("read: %s\n", buf);
}
}
return 0;
}
Функция epoll()
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#define TIMEOUT 5 // Установка таймаута в секундах
#define BUF_LEN 1024 // Длина буфера считывания в байтах
#define MAX_EVENTS 64
int main() {
/**
* Задаем контекст опроса событий
* На выходе получаем файловый дескриптор, ассоциированный с новым
* экземпляром epoll, который создается в функции epoll_create1().
* Данный файловый дескриптор не имеет отношения к реальному файлу; это
* просто указатель, который должен применяться с последующими вызовами,
* задействующими возможность опроса событий.
*/
int epfd = epoll_create1(0);
if (epfd < 0) {
perror("epoll_create1");
return 1;
}
/**
* Добавляем файловый дескриптор STDIN_FILENO в контекст опроса epfd
*/
struct epoll_event event;
event.data.fd = STDIN_FILENO;
event.events = EPOLLIN;
int epctl = epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event);
if (epctl) {
perror("epoll_ctl");
return 1;
}
/**
* Ожидаем событие чтения из STDIN_FILENO, ассоциипрованному с экземпляром
* опроса событий epfd
*/
struct epoll_event *events = malloc(sizeof(struct epoll_event) * MAX_EVENTS);
if (!events) {
perror("malloc");
return 1;
}
int nr_events = epoll_wait(epfd, events, MAX_EVENTS, TIMEOUT * 1000);
if (nr_events < 0) {
perror("epoll_wait");
free(events);
return 1;
}
if (!nr_events) {
printf("%d seconds elapsed.\n", TIMEOUT);
return 0;
}
for (int i = 0; i < nr_events; ++i) {
if (events[i].events & EPOLLIN) {
char buf[BUF_LEN+1];
// Блокировка гарантированно отсутствует
int len = read(STDIN_FILENO, buf, BUF_LEN);
if (len == -1) {
perror("read");
return 1;
} else if (len) {
buf[len] = '\0';
printf("read: %s\n", buf);
}
}
}
free(events);
return 0;
}
Функция kqueue()
#include <sys/types.h>
#include <sys/event.h>
#include <sys/time.h>
#include <unistd.h>
#include <stdio.h>
#define TIMEOUT 5
#define BUF_LEN 1024
int main() {
// registration event
struct kevent change; // events we want to monitor
struct kevent event; // event that were triggered
// create event queue
int kq = kqueue();
if (kq == -1) {
perror("kqueue");
return 1;
}
EV_SET(&change, STDIN_FILENO, EVFILT_READ, EV_ADD, 0, 0, NULL);
struct timespec ts;
ts.tv_sec = TIMEOUT;
ts.tv_nsec = 0;
while(1) {
int nev = kevent(kq, &change, 1, &event, 1, &ts);
if (new < 0) {
perror("kevent");
return 1;
}
if (nev == 0) {
printf("%d seconds elapsed.\n", TIMEOUT);
close(kq);
return 0;
}
if (event.ident == STDIN_FILENO) {
char buf[BUF_LEN + 1];
int len = read(STDIN_FILENO, buf, BUF_LEN);
if (len == -1) {
perror("read");
close(kq);
return 1;
} else if (len) {
buf[len] = '\0';
printf("read: %s\n", buf);
close(kq);
return 0;
}
}
}
close(kq);
return 0;
}
Я не буду расписывать, как работают функции для каждого примера, думаю по комментариям в коде должно быть все ясно. Лишь вкратце опишу, как работает epoll и kqueue, т.к. они описаны в книгах и статьях меньше всего.
Итак, в примере с epoll мы видим аналогию с AnyEvent — регистрация наблюдателя откреплятеся от самого акта наблюдения. Один системный вызов инициализирует контекст опроса событий (для epoll — вызов функции epoll_create1(), для AnyEvent - создание переменной состояния (condvar)), другой добавляет в контекст наблюдаемые файловые дескрипторы или удаляет их оттуда (для epoll — системный вызов epoll_ctl, для AnyEvent — создание наблюдателей для определенных событий (io, timer и т.д.)). Вызов метода send() в нашем случае означает выполнение события, что влечет за собой выход из колбэка и вызов метода recv()), третий выполняет само ожидание события (для epoll — системный вызов epoll_wait(), для AnyEvent — вызов метода recv() для переменной состояния).
Аналогичный пример на AnyEvent можете посмотреть в статье «Всё, что вы хотели знать об AnyEvent, но боялись спросить» из выпуска 1 данного журнала.
Системный вызов kqueue() аналогичен вызову epoll — сначала регистрируем фильтр событий с помощью функции kqueue(), затем с помощью макроса EV_SET
настраиваем структуру change для отслеживания ввода на stdin, затем с помощью kevent отслеживаем события.
Помимо приведенных примеров создания таймера для блокирующего ожидания с помощью мультиплексирования ввода-вывода имеется еще два способа, как это сделать:
- использовать вызов функции alarm, которая генерирует сигнал
SIGALRM
, когда истекает заданное время; - использование новых параметров сокета —
SO_RCVTIMEO
иSO_SNDTIMEO
(специфична для сокетов).
Функции eventfd
и IOCP
, а также приведенные выше способы создания таймера будут рассмотрены в следующих статьях. В следующей статье будет создано приложение с использованием мультиплексирования ввода-вывода и неблокирующего ввода-вывода на нескольких дескрипторах с использованием сокетов.
Ссылки, где можно почитать про мультиплексирование ввода-вывода:
select/poll:
- книга “Роберт Лав — Linux. Системное программирование, 2-е издание, Питер, 2014” — глава 2. Файловый ввод-вывод, Раздел “Мультиплексный ввод-вывод”, стр. 81;
- книга “Стивенс У. Р. — UNIX: разработка сетевых приложений, 3-е изд., Питер, 2007” — глава 6. “Мультиплексирование ввода-вывода: функции select и poll”, стр. 185;
- книга “Стивенс Р. — UNIX. Профессиональное программирование, 2-е изд., Символ-Плюс, 2007” — глава 14. Расширенные операции ввода-вывода, раздел 14.5 Мультиплексирование ввода-вывода, стр. 558
- книга “Michael Kerrisk — The Linux Programming Interface, 2010” — глава 63. Alternative I/O Models, раздел 63.2 I/O Multiplexing, стр. 1374
epoll:
- книга “Роберт Лав — Linux. Системное программирование, 2-е издание, Питер, 2014” — глава 4. Расширенный файловый ввод-вывод, Раздел “Опрос событий”, стр. 131
- книга “Michael Kerrisk — The Linux Programming Interface, 2010” — глава 63. Alternative I/O Models, раздел 63.4 The epoll API, стр. 1399
- Epoll
kqueue:
- книга “Стивенс У. Р. — UNIX: разработка сетевых приложений, 3-е изд., Питер, 2007” — глава 14. “Дополнительные функции ввода-вывода”, раздел 14.9. Расширенный опрос, стр. 436
- kqueue tutorial
- ман-страница в интеренете
- События ядра в FreeBSD
- Краткое введение в kqueue/kevent
Локальная установка и использование Perl-модулей
Рассмотрены способы установки Perl-модулей в локальную директории с помощью cpanm
, использование local::lib
для работы с ними, и carton
для автоматизации процесса.
Часто требуется изолировать установленные модули для каждого проекта отдельно. Это может быть связано с разными версиями, необходимостью локализации или, например, с нежеланием устанавливать модули в систему, если вы просто тестируете какое-то новое Perl-приложение. Для примера возьмем простейшую серверную программу на Plack
, которая при наличии параметра name
приветствует посетителя:
#!/usr/bin/env perl
use strict;
use warnings;
use Plack::Request;
sub {
my $env = shift;
my $req = Plack::Request->new($env);
my $name = $req->param('name') || 'anonymous';
return [200, [], [qq{Hello, $name!}]];
}
Наш проект на текущий момент имеет следующую структуру:
app.psgi
cpanm
Далее с помощью cpanfile
(подробнее о формате cpanfile
читайте в статьей Что такое cpanfile?) укажем зависимости:
requires 'Plack';
Теперь наш проект выглядит так:
app.psgi
cpanfile
Установим зависимости в локальную директорию. Пусть, например, это будет third-party
. Для того, чтобы все модули устанавливались несмотря на их присутствие в локальной системе, воспользуемся ключом -L
у cpanm
:
$ cpanm -L third-party --installdeps .
Установка займет некоторое время и на выходе мы получим следующую структуру:
app.psgi
cpanfile
third-party/
bin/
...
plackup
lib/
perl5/
x86_64-linux-gnu-thread-multi
...
man/
...
Как видно, модули установились в third-party/lib/perl5
, исполняемые файлы в third-party/bin
и man
-страницы в third-party/man
. В директории perl5
присутствует также x86_64-linux-gnu-thread-multi
, куда обычно устанавливаются модули, требующие компиляции. К сожалению, до сих пор Plack
требует компилятор для установки, однако ведутся работы по разделению дистрибутива на несколько пакетов, где необходимые модули для запуска приложения не будут требовать компиляции.
Работа cpanm
на этом закончена. Мы установили необходимые модули в локальную директорию. Теперь необходимо запустить наше приложение.
local::lib
Если мы попробуем запустить следующим образом:
$ perl third-party/bin/plackup app.psgi
То получим вполне ожидаемую ошибку:
Can't locate Plack/Runner.pm in @INC
Мы, конечно, можем указать perl
с помощью ключа -I
где искать модули, однако это довольно утомительно. Более того, часто можно забыть подключить директорию с компилированными модулями, и это не так просто автоматизировать, потому как название директории меняется в зависимости от операционной системы. Именно для подключения локальных модулей нам поможет local::lib
.
$ perl -Mlocal::lib=third-party third-party/bin/plackup app.psgi
HTTP::Server::PSGI: Accepting connections at http://0:5000/
Ключ -M
интерпретатора подключает модули до запуска скрипта. А с помощью =
можно передать модулю параметры. local::lib
получает их в методе import
и подключает необходимую директорию.
Использование cpanm
и local::lib
таким образом позволяет быстро установить модули рядом с проектом и запустить его. Однако, для больших приложений, которые часто приходится устанавливать и запускать на разных системах, это не слишком удобно.
Carton
Carton
объединяет в себе возможности cpanm
и local::lib
(на самом деле, внутри он использует эти два модуля), а также избавляет от необходимости ручного указания путей, позволяет контролировать зависимости и упрощает процесс поставки приложения.
Для работы carton
достаточно cpanfile
, который у нас уже есть. Вернемся к структуре проекта до установки зависимостей и инициализируем carton
:
$ carton install
Installing modules using cpanfile
Successfully installed ...
23 distributions installed
Complete! Modules were installed into local
Модули установились в директорию local
, но это не так важно, потому как carton
сам позаботится о добавлении нужных путей для запуска perl
. Кроме того, carton
создал файл cpanfile.snapshot
, где указаны установленные зависимости с их версиями. Это файл стоит добавить в систему контроля версий. При запуске carton install --deployment
и при наличии cpanfile.snapshot
будут установлены указанные там версии. Таким образом, на машине другого разработчика или сервере будет такое же окружение.
Запускаем приложение:
$ carton exec -- plackup app.psgi
HTTP::Server::PSGI: Accepting connections at http://0:5000/
carton exec
запускает приложение в виртуальном окружении, где все модули подгружаются из директории local
. Так, например, можно проверять все ли модули указаны как зависимости.
carton
также позволяет собирать дистрибутив с зависимостями:
$ carton bundle
Bundling modules using cpanfile
Copying K/KA/KAZEBURO/Apache-LogFormat-Compiler-0.32.tar.gz
Copying ...
/tmp/h3lGuLzMVc/carton.pre.pl syntax OK
Bundling vendor/bin/carton
Complete! Modules were bundled into vendor/cache
Теперь в директории vendor/cache
находятся тарболы модулей. Просто скопировав их на другую машину и запустив там:
$ carton install --cached
модули будут устанавливаться из тарболов без обращения к CPAN. Так можно разворачивать приложения на серверах без доступа к внешним сетям.
Другие решения
Другим подходом для контроля зависимостей является создание своего приватного CPAN. Это позволяют сделать модули CPAN::Mini
и подобные ему. Отдельно стоит приложение Pinto
(читайте подробнее в статье Pinto — собственный CPAN из коробки), которое автоматизирует процесс установки, контроля и фиксирования модулей.
Работа с API GitHub в Perl
Github один из самых популярных сервисов для создания, распространения и совместной работы с программным обеспечением. GitHub обладает превосходным API, позволяя автоматизировать задачи администрирования и тестирования, а также создавать уникальные сервисы, приносящие радость разработчикам. И, как всегда, CPAN предлагает отличный выбор библиотек для работы с GitHub API в Perl.
GitHub API
Перед обзором средств для работы с GitHub API необходимо ознакомиться с тем, что оно из себя представляет. Любое обращение к API делается с помощью http-запросов к сервису api.github.com, поэтому начать изучение можно имея под рукой curl
или аналогичную утилиту. Сервис возвращает ответ в формате JSON, причём немаловажное значение имеют и http-заголовки ответа. Например, информацию о пользователе можно получить, выполнив GET-запрос по URI /users/:user
:
$ curl -i https://api.github.com/users/octocat
HTTP/1.1 200 OK
Server: GitHub.com
Date: Mon, 06 Oct 2014 06:18:13 GMT
Content-Type: application/json; charset=utf-8
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1412579893
X-GitHub-Media-Type: github.v3
{
"login": "octocat",
"id": 583231,
"avatar_url": "https://avatars.githubusercontent.com/u/583231?v=2",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
...
}
Здесь для простоты удалены некоторые заголовки и часть ответа. Как и ожидалось, тип содержимого ответа — это application/json
. Заголовки, начинающиеся с X-
не относятся к спецификации HTTP и в данном случае несут информацию, относящуюся к API сервиса:
X-GitHub-Media-Type
сообщает, что используется третья версия API,X-RateLimit-...
информирует о том, каков ваш лимит на обращения к данному API.
Итак, как уже удалось выяснить, GitHub API предоставляет возможность выполнять некоторые запросы анонимно, но ограничивает количество таких обращений до 60 за час. Для того, чтобы увеличить данный лимит, требуется выполнять авторизацию. Для авторизованного клиента лимит увеличивается до 5000 обращений за час. На данный момент GitHub предоставляет несколько способов авторизации:
Basic-авторизация — каждый запрос сопровождается указанием имени зарегистрированного пользователя и пароля;
OAuth-авторизация — запрос сопровождается указанием ключа доступа (access token), который даёт определённый набор прав.
Если первый вариант авторизации может быть приемлем на этапе знакомства с API, то для реального применения он мало походит ввиду своей небезопасности (нужно передавать пароль в приложение, которое получает максимальный набор прав), а также в случае использования двухфакторной авторизации, когда помимо пароля на каждую операцию требуется одноразовый код. Поэтому в основном применяется авторизация с использованием ключей доступа, и на их видах и способах получения надо остановиться подробно.
Ключи доступа
Ключи доступа позволяют авторизовать ваше приложение на выполнение ограниченного набора действий (scopes), как то чтение/запись репозиториев, создание/удаление заметок gist и т.п. Ключи доступа можно отзывать в случае компрометации. Всё это делает их довольно безопасным средством для работы с API. Существуют два подхода к созданию ключей.
Персональные ключи доступа
Это самый простой способ получения ключа доступа для вашего приложения. С помощью веб-интерфейса настроек приложений создаётся ключ доступа и указываются те права, которые получает приложение. С использованием подобных ключей вы можете решать задачи по администрированию или тестированию ваших репозиториев, получения публичной информации о других репозиториях и пользователях.
Регистрация приложения
Регистрация приложения — это способ, который позволяет получать ключи доступа в соответствии со спецификацией OAuth2. Вы регистрируете приложение на GitHub. Пользователь GitHub, который хочет воспользоваться сервисом вашего приложения, предоставляет требуемые разрешения, и GitHub генерирует соответствующий ключ доступа для вашего приложения. В основном такой подход используется для веб-приложений, в случае если требуется аутентифицировать GitHub-пользователя или приложение должно действовать от имени пользователя (как например, Travis-CI).
Модули GitHub API на CPAN
На данный момент наиболее популярны два модуля для работы с GitHub API: Pithub и Net::GitHub. Оба модуля обладают практически полным набором функций и похожи как братья близнецы, можно отметить лишь два заметных отличия:
Pithub
больше ориентирован на ООП-подход в программировании. Например, результат обращения к API — это объект классаPithub::Result
, который, в частности, поддерживает методcount
, чтобы получить число элементов, и методnext
, чтобы получить следующий элемент. В то время какNet::GitHub
возвращает голый результат, например, в виде хеша.Net::GitHub
, наряду с ключами доступа, поддерживает и basic-авторизацию, что может быть полезно, чтобы сгенерировать новый ключ доступа.Pithub
работает только с ключами доступа.
Возможно, это дело вкуса, но Pithub
показался мне более удобным и приятным в использовании, поэтому остановимся подробно именно на нём.
Структура модулей Pithub
API GitHub можно разделить на несколько основных разделов, каждый из которых представляется соответствующим модулем:
Pithub::Events
— API событий, позволяющая отслеживать ту или иную активность на сайте GitHub;Pithub::Gists
— API выжимок (gist) для получения и манипуляций с выжимками;Pithub::GitData
— API данных для работы с сущностями git-репозиториев: блобы, деревья, коммиты, теги и ссылки;Pithub::Issues
— API баг-трекера (ошибки, вопросы и проблемы);Pithub::Orgs
— API организаций для получения и изменения информации организаций, а также управления командами и участниками;Pithub::PullRequests
— API запросов на слияние, включая и работу с комментариями к запросам;Pithub::Repos
— API репозиториев, содержащее обширное число операций по работе с репозиториями;Pithub::Users
— API пользователей для получения и изменения информации о пользователях GitHub.Pithub::Search
— API поиска по GitHub.
Как видно из списка, в Pithub
пока отсутствуют некоторые новые разделы GitHub API, такие как Активность, включающая несколько подразделов: уведомлений, ATOM-фидов и других. Также отсутствуют Markdown API, Meta API, RateLimit API и некоторые другие. Кроме того Pithub::Search
использует устаревший legacy-интерфейс поиска. Тем не менее, как будет показано позже, с помощью модуля Pithub::Base
вы можете компенсировать отсутствие готового нужного раздела API и напрямую делать вызовы GitHub API.
“Hello, GitHub!”
Итак, вы успешно зарегистрировали нового пользователя на GitHub (увы, некоторые просчёты могут привести к блокировке учётной записи, так что лучше перестраховаться) и создали первый персональный ключ доступа. Можно приступать к созданию своего первого приложения.
use DDP;
use Pithub;
# Ключ доступа берем из переменной окружения
my $p = Pithub->new(
token => $ENV{GITHUB_TOKEN}
);
# Исчерпывающая информация о пользователе octocat
my $result = $p->users->get( user => 'octocat' );
# Вывод декодированной структуры ответа
p $result->content;
# Вывод количества оставшихся запросов к API
p $result->ratelimit_remaining;
Перед запуском скрипта необходимо экспортировать переменную окружения GITHUB_TOKEN
, в которую нужно поместить значение ключа доступа:
$ export GITHUB_TOKEN=deadbeeffacecafe
$ perl hello_github.pl
\ {
avatar_url "https://avatars.githubusercontent.com/u/583231?v=2",
bio undef,
blog "http://www.github.com/blog",
company "GitHub",
created_at "2011-01-25T18:44:36Z",
email "octocat@github.com",
...
}
4999
В данном примере мы создавали базовый объект Pithub
$p
, а затем получали доступ к методам объекта Pithub::Users
с помощью метода users
. Точно такой же результат можно было получить, создав Pithub::Users
напрямую:
my $u = Pithub::Users->new(
token => $ENV{GITHUB_TOKEN}
);
my $result = $u->get( user => 'octocat' );
Такой же подход можно использовать и для всех других субмодулей Pithub
.
Работа со списками
Некоторые вызовы API возвращают не один объект, а список JSON-объектов. Как, например, запрос списка репозиториев пользователя или организации:
use DDP;
use Pithub;
my $r = Pithub::Repos->new( token => $ENV{GITHUB_TOKEN} );
my $result = $r->list( org => 'PadreIDE' );
p $result->count;
while ( my $row = $result->next ) {
p $row->{name};
}
В данном примере запрашивается список репозиториев организации PadreIDE. Запрос возвращает список из 30 репозиториев, но самих репозиториев на самом деле 100. Связано это с тем, что GitHub по умолчанию устанавливает лимит на 30 объектов в рамках одного запроса, и для того, чтобы получить весь список объектов, можно запрашивать их постранично:
my $result = $r->list( org => 'PadreIDE' );
while ( $result && $result->success ) {
while ( my $row = $result->next ) {
p $row->{name};
}
$result = $result->next_page;
}
В данном случае метод next_page
делает новый запрос к API и возвращает новую порцию данных (или undef
, если данных больше нет).
Тоже самое можно получить, установив флаг auto_pagination
в истинное значение, тогда метод next
будет автоматически вызывать next_page
при завершении списка. Кроме того, можно регулировать и количество объектов в одном ответе с помощью свойства per_page
:
# включить автоматический запрос последующих страниц
$r->auto_pagination(1);
# 50 объектов на странице
$r->per_page(50);
my $result = $r->list( org => 'PadreIDE' );
while ( my $row = $result->next ) {
p $row->{name};
}
Запросы к API, которые не реализованы в Pithub
GitHub API активно развивается, добавляются новые разделы, которые не были реализованы в Pithub
. Поскольку все обращения к API выполняются через http-запросы, в Pithub
существует возможность указывать нужный метод и URI запроса с помощью метода request
. Рассмотрим на примере Ratelimit API, которое позволяет GET-запросом по URI /rate_limit
получить информацию об актуальных значениях лимитов на API-запросы для вашего приложения.
use DDP;
use Pithub;
my $p = Pithub->new(
token => $ENV{GITHUB_TOKEN}
);
my $result = $p->request(
method => 'GET',
path => '/rate_limit',
);
p $result->content;
В данном примере мы указываем метод и путь запроса к API, и в результате получаем объект класса Pithub::Result
с информацией.
\ {
rate {
limit 5000,
remaining 5000,
reset 1412552655
},
resources {
core {
limit 5000,
remaining 5000,
reset 1412552655
},
search {
limit 30,
remaining 30,
reset 1412549115
}
}
}
Другой пример это Markdown API. GitHub позволяет преобразовывать данные, отправленные в формате Markdown, в html-формат.
use DDP;
use Pithub;
my $p = Pithub->new(
token => $ENV{GITHUB_TOKEN}
);
my $result = $p->request(
method => 'POST',
path => '/markdown',
data => {
text => '**Hello, World!**',
mode => 'markdown',
}
);
p $result->raw_content;
В данном примере выполняется POST-запрос, тело запроса в формате JSON формируется из структуры data
. Обратите внимание, что для извлечения результата используется метод raw_content
, поскольку получаемые данные приходят в формате text/html
, а не application/json
и не требуют декодирования.
<p><strong>Hello, World!</strong></p>
Практический пример: строим свой CI-сервер
Одно из интересных применений GitHub API — это возможность создания своих собственных сервисов и в частности сервера непрерывной интеграции.
В GitHub существует возможность уведомлять внешние сервисы о тех или иных событиях, происходящих в ваших репозиториях. Например, отправка коммитов в репозиторий (push). В настройках каждого репозитория в разделе WebHooks можно указать URL сервиса, который будет принимать POST-запросы с информацией о произошедшем событии.
API GitHub поддерживает установку статусов для каждого коммита. Статус — это некоторая метка, информация о состоянии репозитория и может использоваться CI-системами для обозначения того, успешно ли собирается проект или того, что данный коммит приводит к ошибкам.
Рассмотрим простое веб-приложение на веб-фреймворке Dancer
, которое может выступить в роли CI-сервера:
use Dancer;
use Pithub;
use JSON ();
post '/event_handler' => sub {
my $payload = JSON::decode_json(request->body);
my $user = $payload->{repository}->{owner}->{name};
my $repo = $payload->{repository}->{name};
# Статус сборки
my $s = Pithub::Repos::Statuses->new(
token => $ENV{GITHUB_TOKEN},
user => $user,
repo => $repo,
);
# Статус сборки - в обработке (pending)
$s->create(
sha => $payload->{after},
data => {
state => 'pending',
description => 'starting build',
context => 'ci/perl',
}
);
# Запустить сборку
run_ci(
commit => $payload->{after},
url => $payload->{repository}->{clone_url},
name => $payload->{repository}->{name},
user => $payload->{repository}->{owner}->{name},
cb => sub {
my ( $state, $url ) = @_;
# Обновляем статус (failure или success)
$s->create(
sha => $payload->{after},
data => {
state => $state,
description => "build $state!",
context => 'ci/perl',
target_url => $url
}
);
},
);
"ok";
};
dance;
Итак, запускается веб-сервер, который ожидает POST-запрос по URI /event_handler
, которое было указано в WebHooks репозитория. GitHub на каждый push в репозиторий будет отправлять данные на наше веб-приложение. Данные приходят в формате JSON, которые декодируются в строке 6. Из полученной структуры извлекается информация о коммите, пользователе и названии репозитория.
В строке 19 мы используем API статусов и устанавливаем для полученного коммита статус «pending», т.е. коммит находится в процессе проверки. Далее следует некая асинхронная процедура run_ci
, которая клонирует репозиторий, выполняет проверку, например, сборку. И по результатам выполняет функцию обратного вызова с результатами проверки: это может быть «failure»/«error» или «success». Этот результат мы используем для обновления статуса коммита. В данном примере мы также устанавливаем target_url
— это url по которому можно получить, например, логи сборки репозитория.
В веб-интерфейсе GitHub информацию о статусе можно увидеть в списке веток репозитория, где показывается последний статус для каждой ветки в виде ссылки, которую вы указали в target_url
.
Таким же образом можно проверять сборку репозитория при получении запросов на слияние (pull request). В этом случае надо проверять получаемый заголовок X-Github-Event
, который будет иметь значение pull_request
.
Заключение
GitHub API — это уникальный сервис, открывающий поистине огромные возможности для создания полезных и практичных приложений. В статье было рассмотрено лишь несколько примеров по работе с API с помощью модуля Pithub
, которые дают представление о способе его работы и интерфейсе. Более подробная информация содержится в POD-документации модуля, а информация об API GitHub — на сайте для разработчиков. Смотрите и изучайте, надеюсь, его больше никогда не заблокируют.
Введение в 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
.
Обзор CPAN за сентябрь 2014 г.
Рубрика с обзором интересных новинок CPAN за прошедший месяц.
Статистика
- Новых дистрибутивов — 237
- Новых выпусков — 1044
Новые модули
Проект Clownfish разрабатывается в рамках поискового движка Lucy, и представляет собой реализацию объектной системы для языка C. Одноимённый Perl-модуль Clownfish является симбиотическим байндингом, связывая объектную системы языка Perl с Clownfish, позволяя получать доступ к объектам Clownfish как к обычным объектам Perl, наследовать, расширять и затем использовать в Clownfish.
Утилита mssh
позволяет выполнять команды через ssh-соединения одновременно (или последовательно) на нескольких хостах.
Ещё один представитель модулей для генерации SQL-запросов. Интерфейс модуля оправдывает своё название, позволяя достаточно гибко строить весьма сложные запросы. Документация модуля содержит большое число примеров.
Модуль Scalar::Watcher
позволяет отслеживать изменение скалярной переменной, вызывая указанную пользователем функцию:
my $a = 123;
when_modified $a, sub { print "catch $_[0]\n" };
$a = 456; # напечатает 'catch 456'
Фреймворк для автоматизации задач администрирования Rex
обзавёлся веб-интерфейсом Rex::JobControl
. Веб-интерфейс создан на основе фреймворка Mojolicious и сервера задач Minion.
Object::Util
— набор полезных функций для работы с объектами. Идея интерфейса заимствована из Safe::Isa
, но набор функций значительно шире и интереснее.
$foo->$_isa("Class");
В данном примере метод $_isa
аналогичен стандартному isa
: он проверяет, является ли $foo
объектом класса Class
, с тем лишь отличием, что если $foo не является объектом, то вместо исключения возвращается undef
.
Утилита cpanmw
является обёрткой для cpanm
и делает симпатичную подсветку вывода, если терминал поддерживает цвета. Подсветка работает в том числе и на платформе Windows.
Net::DNS::Native
неблокирующийся ДНС-резолвер, который использует вызов getaddrinfo
вашей системной библиотеки. Разрешение имени происходит в выделенной нити, позволяя основной нити программы выполнять свою работу.
Mesos
— это обвязка к разделяемой библиотеке Mesos проекта Apache для управления кластером серверов. Модуль позволяет создавать распределённые приложения (фреймворки), запускающие задачи на динамическом пуле серверов, управляемом Apache Mesos с поддержкой изоляции и выделения ресурсов.
Модуль Plugin::Loader
позволяет просто и безопасно решать задачу по поиску и загрузке модулей-плагинов. Данная задача, наверно, один из самых распространённых случаев использования строкового eval
неискушёнными разработчиками.
Обновлённые модули
- Mango 1.14
Новый релиз неблокирующегося драйвера MongoDB Mango
по всей видимости стал последним. Себастьян Ридель объявил, что в связи с тем, что разработка актуального драйвера требует высоких трудозатрат, а каждый релиз MongoDB ломает обратную совместимость, разработка Mango
прекращена. Почти сразу появился форк проекта, но пока неясно, станет ли он преемником.
- HTTP::Tiny 0.50
В новом релизе лёгкого веб-клиента HTTP::Tiny
исправлена работа keep_alive
при создании нового процесса/нити в программе.
- DBIx::Class 0.082800
Новый релиз DBIx::Class
со множеством исправлений ошибок.
- MaxMind::DB::Reader 1.000000
Вышел первый мажорный релиз MaxMind::DB::Reader
для работы с базами данных геолокации IP-адресов MaxMind. Параллельно выпущена и XS-версия модуля, работающая в 100 раз быстрее.
- Template::Tooolkit 2.26
Новый релиз популярного шаблонизатора Template::Tooolkit
теперь поддерживает контурные теги (outline tags %%
), позволяя избавиться от нагромождения скобок:
%% IF some.list.size
<ul>
%% FOREACH item IN some.list
<li>[% item.html %]</li>
%% END
</ul>
%% END
- Data::Dumper 2.154
Обновление для модуля Data::Dumper
содержит исправление ошибки безопасности CVE-2014-4330, при разборе глубоко вложенной структуры происходили расходование всего пространства стека и крах приложения. Теперь модуль содержит переменную Maxrecurse
, которая по умолчанию ограничивает максимальный уровень рекурсии значением в 1000.
Интервью с Леоном Тиммермансом
Леон Тиммерманс (Leon Timmermans) — Perl-программист, биолог по образованию, автор и сопровождающий многих популярных модулей на CPAN.
Когда и как научился программировать?
Вообще-то первым языком, который я по-настоящему выучил, был Javascript, причем еще тогда, когда никто его толком не использовал ни для чего серьезного (мне кажется, в районе 1999 года). По иронии судьбы, его сейчас используют все, а я не прикасался к нему в течение многих лет. После этого я познакомился с C, Java и Perl, хотя я уже не помню, в каком это было порядке (это была непрерывная последовательность).
Какой редактор используешь?
Vim. Я начал им пользовался когда начал использовать Linux и никогда не пробовал использовать что либо другой (я даже использовал его на Windows).
Когда и как познакомился с Perl?
Думаю, что где-то в 2000-2001 году я наткнулся на Perl-книгу и начал играться с языком на своей Linux-машине (там был установлен 5.005004). Только в 2008 году я начал загружать свои модули на CPAN и через несколько месяцев позже посетил свой первый Perl-воркшоп. Так я и попал в сети Perl-сообщества.
С какими другими языками интересно работать?
Мне очень нравится С++, особенно когда вышел С++11 (и этим летом С++14). Он во многом похож на perl, у него такая репутация устаревшего языка, но если присмотреться, он живой и в активной разработке. И точно также как Perl, этот язык относится к тебе как к взрослому: он не говорит, как что-нибудь сделать, но предоставляет богатый инструментарий для достижения своей цели. Не поймите меня неправильно, C++ капризный, и каждый опытный C++ программист испытывает разного рода раздражение по отношению к нему (большее, чем к другим языкам), но как по мне, он вполне хорош.
Моим другим основным языком является C. Сегодня в большинстве случаев я использую его для работы над ядром perl и XS-модулями. Мне он все также нравится как и раньше, но С++ позволяет мне решать проблемы более эффективным способом.
Что, по-твоему, является самым большим преимуществом Perl?
Его выразительность. Он дает мне возможность написать вещи несколькими словами, которые бы заняли целые абзацы на других языках. Его желание дать мне всю возможную мощь вместо нескольких опций, как мне что-то сделать.
Что, по-твоему, является наиболее важной особенностью языков будущего?
В длительной перспективе — расширяемость. Возможность реализации вне ядра вещей, которые авторы не предусмотрели.
В короткой перспективе — хорошая поддержка распараллеливания. Мы живем в многоядерном мире, но только некоторые программы пользуются этим, потому как большинство языков программирования не позволяют этого сделать. Есть незаполненная ниша.
Как пришла в голову идея написать прагму experimental?
Я заметил, что включение экспериментального функционала выглядит несколько странным. И я думаю, что включение их по умолчанию с выводом предупреждения было бы более правильным подходом, чем добавлении двух строчек, чтобы сказать perl о том, что ты знаешь, что делаешь (это не perl-подход). Поэтому мне пришла в голову идея написать модуль, который бы говорил, что ты делаешь что-то рискованное, без попытки помешать тебе в этом.
Как объединяешь биологию и программирование?
У меня вообще биологическое образование, а не программисткое. Во время моего исследования я решил смешать мое хобби и работу.
Грубо говоря, биоинформатика может быть разделена на вычислительную сферу (как, например, 3D-моделирование белка) и на сферу больших данных (как, например, анализ генома), хотя на практике обычно проблема относится к обеим сферам. Я работал над второй. Многие инновации за последние два десятилетия ускорили процесс накопления данных быстрее, чем увеличиваются возможности компьютеров и устройств хранения данных, это все рождает многие интересные проблемы.
Что не так с File::Slurp?
Есть три проблемы.
Первая — это интерфейс. В современной работе с файловой системой кодировка файла важна настолько же, насколько и его имя. Это было инновацией perl 5.8, но File::Slurp был написан до этой версии. Откровенно говоря, вся парадигма сильно устарела. read_file($filename, binmode => ":encoding(utf-8)")
слишком длинно, что приводит к тому, что кодировку все игнорируют. В File::Slurper это выглядит как read_text($filename)
, что, по-моему, выглядит несколько лучше.
Во-вторых, реализация содержит ошибки. Так как модуль был написан до IO layers и пытается их переизобрести, и не очень успешно. В частности, он некорректно декодирует/кодирует не-utf-8 символы (как, например, UTF-16 и KOI-*). Также у него есть проблемы с правильной обработкой перевода строк. Все это из-за небольших оптимизаций для частных случаев (чтение бинарного файла в переменную), что сильно усложняет код. Я планирую добавить похожую, но правильную оптимизацию в ядро perl версии 5.22, чтобы закончить этот спор.
Третья проблема — это сопровождение. Дистрибутив не обновлялся в течение трех лет несмотря на ошибки, найденные более полутора лет назад. Он и до этого редко обновлялся. Я понимаю, что у всех программ есть ошибки, но я не хотел бы зависеть от авторов, которые настолько не заботятся об их исправлении.
Правильно ли использовать потоки в Perl?
Обычно нет.
Они не обязательно зло, но на практике никто толком не знает, как эффективно их использовать. Их модель ни для чего по-настоящему не годится, они не масштабируются на больших данных.
Я работал над модулем акторов, что лучше вписывается во внутренности perl, но доведение модуля до возможности использования другими программистами довольно сложно. Существующая на сегодняшний день непонятная ситуация со smart-matching, возможно, вынудит меня вообще пересмотреть весь интерфейс.
libperl++ это законченный проект? Каков его статус?
Законченный? К сожалению, нет. Должен признать, что это несколько заброшенный проект, хотя у меня есть планы по его воскрешению.
libperl++ был очень амбициозным проектом, который научил меня C++ и Perl API. По факту, это самый сложный проект, который я когда-либо писал. Настолько сложный, что я загнал себя в угол. Объединение шаблонов, множественного наследования и неявных преобразований сделало эту смесь взрывоопасной.
Возможно, мне стоит сделать версию 2.0 с более компактным и не таким магическим кодом.
Улучшилась ли Perl-документация с 2010 года?
Местами. Не настолько, насколько мне бы хотелось. Некоторые вещи были переписаны, как например, perlopentut, многие вещи остались неизменными. На это стоит направить поток мотивированных волонтеров.
Стоит ли советовать молодым программистам учить Perl сейчас?
Конечно. Но опять же, я призываю программистов учить множество языков, возможно, даже очень разных. Perl объединяет практичность и изобретательность, его хочется изучать и применять.
Вопросы от наших читателей
Будешь ли снова начинать писать блог?
Возможно. Пока не уверен. Мне обычно довольно сложно заканчивать свои статьи, несмотря на большое количество идей. Вообще, у кодирования больший приоритет, поэтому статьи остаются незаконченными.