Давайте заполнять книгу
MVC
Здесь будут собраны материалы, посвяшенные работе с базами данных из perl
subj
В настоящий момент представить сложное приложение, взаимодействующее с базами данных и при этом не использующее транзакции достаточно сложно. Большое количество вычислений, преобразований и прочих действий сопряжены с возникновением ошибок. Для некоторых приложений корректная обработка ошибок является обязательной.
Представьте гипотетическую ситуацию, когда приложение в одном SQL запросе изменяет баланс пользователя, а в другом должно на эти самые деньги купить какой-нибудь товар. И в от этот самый второй запрос и дает сбой – не столь важно по какой причине, важно, что деньги уже списаны. Конечно, если рассуждать с точки зрения владельца системы, то он в какой-то степени в выигрыше:), но вот мы, как простые пользователи, будем в лучшем случае просто расстроены(в худшем начнем искать способы, чтобы вторая фаза выполнялась успешно, в первая не работала:)).
Грамотный разработчик будет использовать транзакции и обработку ошибок. Поймав вторую ошибку, он сделает откат первого изменения и выдаст информацию пользователю, что в по той или иной причине его запрос не может быть обработан.
Так это выглядит в теории.
Подавляющее число разработчиков на perl используют DBI для работы с базами данных. Он достаточно прост и удобен в использовании, хотя в управлении транзакциями он слабоват. DBI предоставляет только две функции – commit и rollback.
Функция commit вызывается, когда необходимо зафиксировать(сохранить изменения в базу и сделать их видимыми для других процессов) транзакции, rollback приводит к откату изменений.
Подавляющее большинство драйверов под DBI работают со включенным режимом AutoCommit. Это значит, что любое изменение, вызываемое командами DML будет зафиксировано. Например, у Вас идет десяток insert-ов и на 7-ом происходит сбой. Включенный режим автофиксации приведет к тому, что в базе окажутся первые 6 записей. Плохо это или хорошо зависит только от логики приложения.
Итак, давайте рассмотрим пару примеров. Для начала создадим таблицу:
create table com_test(id serial, name varchar(30));
Напишем небольшую тестовую программу:
#!/usr/bin/perl
use DBI;
my $dbh = DBI->connect("DBI:Pg:dbname=test","rimas", "", {RaiseError => 1, PrintError=>0});
#Отключим автофиксацию
$dbh->{AutoCommit} = 0;
#Попытаемся записать строку
$dbh->do("insert into com_test(name) values('test1')");
#Выходим без принудительной фиксации
$dbh->disconnect();
Переходим в консоль и проверяем, что же находится у нас в таблице:
select * from com_test;
id | name
----+------
(0 rows)
Как и ожидалось, там не сильно много:)
Теперь изменим код таким образом, чтобы фиксировать транзакцию:
#Попытаемся записать строку
$dbh->do("insert into com_test(name) values('test1')");
#Фиксируем транзакцию
$dbh->commit();
В результате выполнения скрипта в таблицу будет добавлена одна запись.
select * from com_test;
id | name
----+-------
2 | test1
(1 запись)
Обратите внимание на ID – он равен 2. В независимости от того, был сделан откат транзакции или нет, последовательность изменила свое значение.
А что будет, если кто-то будет читать данные из параллельного потока? По хорошему, существуют несколько видов транзакций с разным уровнем изоляции – например, когда из внешнего мира не видно совсем ничего из того, что делает приложение или, например, эти самые изменения видны, но существует возможность сделать откат.
Модифицируем скрипт следующим образом:
$dbh->do("insert into com_test(name) values('test1')");
#Ожидаем ввода пользователя или просто останавливаем выполнение программы
<STDIN>;
$dbh->rollback();
$dbh->disconnect();
Когда программа была остановлена, проверим из консоли, что же находится в таблице. В моем случае новых записей не появилось. Это говорит о том, что приложение выполняется с уровнем полной изоляции.
Меня всегда интересовал вопрос – а как будут вести себя подпрограммы на стороне сервера, если я выключил автофиксацию. Для тестов был написал следующий скрипт на plpgsql:
create or replace function insert_into_coms() returns void as '
declare
begin
insert into com_test(name) values(''from plgpsql'');
end;
' language plpgsql;
который был потом загружен в базу:
test=# \i demo.sql
CREATE FUNCTION
Скрипт переписан для вызова функции:
$dbh->do("select insert_into_coms()");
#Выходим без фиксации
$dbh->disconnect();
В результате новых записей обнаружено не было. Думаю, излишне будет говорить, что при добавлении $dbh->commit изменения были записаны в таблицу.
Следует отметить, что выключение автофиксации скажется на производительности сервера баз данных. Но это уже работа администраторов баз данных, как правильно настроить сегменты, распределить tablespace-ы и назначить контрольные точки. Если логика приложения требует атомарных операций – выключайте автофиксацию и работайте на уровне commit/rollback. Потери производительности ничто по сравнению с потерями данных.
Проверено:)
Информация общего характера
Раздел, посвященный обработке текста в Perl
Adobe PDF является стандартом для обмена текстовыми документами. Большинство офисных пакетов могут экспортировать данные из внутренних форматов в pdf, однако функция эта зачастую не обладает широким набором возможностей и с желаниями более продвинутых пользователей может не справиться. Например, попробуйте скомбинировать несколько pdf файлов в один или создать навигационную панель на основе закладок. Представьте себе, что стоит задача поддерживать документ, содержащий тематическую выборку статей с perl.com, при этом навигационная панель так же должна обновляться. Для решения данной проблемы можно было бы воспользоваться утилитами типа HTMLDOC, но вот если потребуется добавить статью номер 51, то нужно будет предварительно закачать первые 50. Скорее всего, даже пройдя через все мучения, Вы не будете удовлетворены результатом. Данная статья рассказывает, как можно воспользоваться модулем PDF::Reuse(автор Lars Lundberg) для создания комбинированных документов и навигационных панелей.
Не стоит рассчитывать на что, что, используя PDF::Reuse, Вы сможете создавать потрясающие по красоте и наполнению документы. Возможность данного модуля достаточно ограничены, так что для полноценной работы рекомендую ознакомиться с такими пакетами, как PDF::API2 или Text::PDF. Однако, даже базовые возможности позволяют создать простенький документ, который мы будем использовать в дальнейших примерах.
# file: examples/create-pdfs.pl
use strict;
use PDF::Reuse;
mkdir "out" if (!-e "out") ;
foreach my $x (1..4) {
prFile("out/file-$x.pdf");
foreach my $y (1..10) {
prText(35,800,"File: file-$x.pdf");
prText(510,800,"Page: $y");
foreach my $z (1..15) {
prText(35,700-$z*16,"Line $z");
}
# add graphics with the prAdd function
# stroke color
prAdd("0.1 0.1 0.9 RG\n");
# fill color
prAdd("0.9 0.1 0.1 rg\n");
my $pos = 750 - ($y * 40);
prAdd("540 $pos 10 40 re\n");
prAdd("B\n");
if ($y < 10) {
prPage();
}
}
prEnd();
}
Как видно из данного примера, для открытия файла используется функция prFile($filename), а для закрытия – prEnd(). В промежутке между этими двумя вызовами мы добавили некоторый текст с использованием функции prText. Так же Вы можете работать с графикой посредством функции prAdd, используя стандартный синтаксис pdf для работы. По умолчанию вся работа ведется на текущей странице. Для того, чтобы перейти на другую или создать новую страницу, вызовите prPage(). Функция prFile автоматически создает одну пустую страницу, так что каждую новую нужно создавать вручную.
В примере мы создали красный прямоугольник с синими границами с использованием функции prAdd. Как Вы поняли, это низкоуровневый вызов, использующий нотацию самого pdf. Если у Вас появится желание и большое количество свободного времени, можно более детально изучить информацию в разделе PDF reference manual. Для более комфортной работы с графикой желательно воспользоваться PDF::API2 или Text::PDF, которые предоставляют высокоуровневые абстракции для манипуляций с графикой.
Комбинируем документы
Основное предназначение PDF::Reuse – обработка уже существующих документов. Давайте создадим один большой файл на основе полученных ранее pdf-ов.
# file: examples/combine-pdfs.pl
use strict;
use PDF::Reuse;
prFile("out/resultat.pdf");
prDoc('out/file-1.pdf',1,4);
prDoc('out/file-2.pdf',2,9);
prDoc('out/file-3.pdf',8);
prDoc('out/file-4.pdf');
prEnd();
Как и ранее, prFile открывает новый файл. Затем, используя prDoc($filename, $firstPage, $lastPage), мы добавляем в новый файл различное количество страниц из исходных документов. Например, из file-1.pdf мы взяли первые 4, из file-3.pdf – только восьмую страницу. Аргументы $firstPage и $lastPage являются опциональными. Их полное отсутствие подразумевает, что будет использоваться весь документ.
К достаточно полезным функциям модуля PDF::Reuse можно отнести создание документов на основе pdf-шаблонов. Например, у нас есть файл customers.txt, который содержит список имен пользователей, которым нужно будет отправить именные письма. У Вашей компании есть свои собственные шаблоны таких писем, созданные в том же OpenOffice , но нет желания готовить документы для всей 1000 клиентов вручную. Используя perl, очень легко разобрать файл с именами и создать набор документов:
# file: examples/reuse-letter.pl
use PDF::Reuse;
use Date::Formatter;
use strict;
my $date = Date::Formatter->now();
$date->createDateFormatter("(DD).(MM). (YYYY)");
my $n = 1;
my $incr = 14;
my $infile = 'examples/customer.txt';
prFile("examples/sample-letters.pdf");
prCompress(1);
prFont('Arial');
prForm("examples/sample-letter.pdf");
open (my $fh, "<$infile") || die "Couldn't open $infile, $!\n aborts!\n";
while (my $line = <$fh>) {
my $x = 60;
my $y = 760;
my ($first, $last, $street, $zipCode, $city, $country) = split(/,/, $line);
last unless $country;
prPage() if $n++ > 1 ;
prText($x, $y, "$first $last");
$y -= $incr;
prText($x, $y, $street);
$y -= $incr;
prText($x, $y, $zipCode);
prText(($x + 40), $y, $city);
$y -= $incr;
prText($x, $y, $country);
prText(60, 600, "Dear $first $last,");
prText(400, 630, "Berlin, $date");
}
prEnd();
close $fh;
Сразу после открытия файлы с помощь prFile, мы включаем компрессию данных, вызвав prCompress(1). Устанавливаем шрифт по умолчанию, используя prFont. Стандартными значениями являются Times-Roman, Times-Bold, Times-Italic, Times-BoldItalic, Courier, Courier-Bold, Courier-Oblique, Courier-BoldOblique, Helvetica, Helvetica-Bold, Helvetica-Oblique и Helvetica-BoldOblique. Все последующие действия представляют собой элементарные итерации через данные, хранимые в файле с их последующей печатью через prText.
Иногда бывает необходимо внести лишь небольшие изменения в документ, например, пронумеровать страницы:
# file: examples/sample-numbers.pl
use PDF::Reuse;
use strict;
my $n = 1;
prFile('examples/sample-numbers.pdf');
while (1) {
prText(550, 40, $n++);
last unless prSinglePage('sample-letters.pdf');
}
prEnd();
prSinglePage извлекает следующую страницу открытого документа и возвращает количество оставшихся страниц.
Если по счастливой случайности или в результате работы Вы познакомились с синтаксисом pdf, PDF::Reuse даст возможность воспользоваться этими знаниями. Команда prAdd позволит вводить команды непосредственно в тело документа, однако следует быть осторожным, так как модуль не проводит проверки на корректность синтаксиса. Приведем небольшой пример, показывающий, как нарисовать небольшие залитые цветом прямоугольники:
use PDF::Reuse;
use strict;
prFile('examples/sample-rectangle.pdf');
my $x = 40;
my $y = 50;
my @colors;
foreach my $r (0..5) {
foreach my $g (0..5) {
foreach my $b (0..5) {
push @colors,
sprintf("%1.1f %1.1f %1.1f rg\n",
$r * 0.2, $g * 0.2, $b * 0.2);
}
}
}
while (1) {
if ($x > 500) {
$x = 40; $y += 40;
last unless @colors;
}
# a rectangle
my $string = "$x $y 30 30 re\n";
$string .= shift @colors;
# fill and stroke
$string .= "b\n";
prAdd($string);
$x += 40;
}
prEnd();
Одним из неоспоримых преимуществ формата pdf является наличие в нем механизма ссылок, который позволяет создавать оглавления. Очень многие приложение не умеет создавать оглавления или делают это некорректно, в результате чего документ становиться практически бесполезным. PDF::Reuse позволяет решить эту проблему путем использования функции prBookmark($reference).
Переменная $reference представляет собой ссылку на массив или хеш, который может иметь следующую структуру:
{ text => 'Document-Text',
act => 'this.pageNum = 0; this.scroll(40, 500);',
kids => [ { text => 'Chapter 1',
act => '1, 40, 600'
},
{ text => 'Chapter 2',
act => '10, 40, 600'
}
]
}
где act является кодом на JavaScript, который отрабатывается каждый раз после того, как пользователь нажал на ссылку. Насколько мне известно, в настоящий момент существует только одно приложение, корректно отрабатывающее данный callback(приложение делает компания Adobe), поэтому мы покажем в дальнейшем, как это исправить.
Другие, достаточно интересные примеры, такие как вставка изображений, можно найти в документе PDF::Reuse::Tutorial.
Для того, чтобы избавиться от утомительной и достаточно неблагодарной задачи каждый раз редактировать исходные тексты, задавая имена файлов и номера страниц, я написал небольшой скрипт, который будет выполнять обработку pdf-ов, принимая всю необходимую информацию через параметры командной строки. Чтобы сделать приложение более модульным, я вынес основной код в новый модуль, получивший название CombinePDFs. Это даст нам возможность применять его не только в консольном варианте программы, но и в более дружественных к пользователю, использующих GUI(такой как Perl/Tk).
Данная диаграмма показывает связи между модулем, примерами и приложениями:

Приложение app-combine-console-pdfs.pl не использует модуль PDF::Reuse напрямую, оно лишь обрабатывает параметры командной строки с помощью модуля Getopt::Long. Это своего рода стандарт de facto для данных задач в мире perl. Мы разбираем и сохраняем список файлов и количество страниц в два массива одинаковой длинны. Пользователь так же может указать имя выходного документа и файл, содержащий описание ссылок. Главная функция приложения, которая обрабатывает входные параметры и вызывает CombinePDFs::createPDF:
sub main {
GetOptions("infile=s" => \@infiles,
"outfile=s" => \$outfile,
"pages=s", => \@pages,
'overwrite' => \$overwrite,
'bookmarks:s' => \$bookmarks,
'help' => \&help);
help unless ((@infiles and $outfile and @pages) and @pages == @infiles);
checkPages();
checkFiles();
checkBookmarks();
CombinePDFs::createPDF(\@infiles, \@pages, $outfile, $bookmarks);
}
В случае, если пользователь передал некорректное количество входных аргументов, неправильные имена файлов или неверные диапазоны страниц, приложение распечатает небольшую справку по использованию. Любое хорошее консольное приложение должно быть написано в таком стиле.
Основная задача приложения состоит в проверке корректности ввода данных. Если все введено правильно, вызывается функция CombinePDFs::createPDF, которой в качестве аргументов передаются массив файлов, массив, содержащий диапазон страниц и опциональный параметр, описывающий ссылки.
Диапазон страниц может быть представлен как в формате с запятой в качестве разделителя – (1-10, 14-22, 44-46), так и в виде отдельных одиночных страниц или же с помощью специального тега all.
Далее происходит проверка файлов – права на чтение, тест, является ли файл документом pdf. Последняя проверка достаточно наивна, так как использует всего-лишь чтение первой строки с ее последующим сравнением с шаблоном %PDF-1.[0-9].
Модуль PDF::Reuse не является объектно-ориентированным, так что CreatePDFs не сильно отличается от прародителя.
Передача большого набора параметров, которыми является описание закладок – достаточно трудоемкая задача, так что я решил воспользоваться для этого отдельным текстовым файлом следующего формата:
<level> "bookmarks text" <page>
Отсчет уровня начинается с 0. К корневому уровню с номером 0 примыкают листья с последующими номерами 1, 2 и 3. Если подряд указывается несколько одинаковых уровней, то это значит, что первый из них является корневым, а каждый последующий – вложенным. Всего допускается уровень вложенности, равный 3.
0 "Folder File 1 - Page 1" 1
1 "File 1 - Page 2" 2
1 "Subfolder File 1 - Page 3" 3
2 "File 1 - Page 4" 4
0 "Folder File 2 - Page 7 " 7
1 "File 2 - Page 7" 7
1 "File 2 - Page 9" 9
Функция обработки файла закладок в называется CombinePDFs::addBookmarks($filname). Ее логика не представляет из себя ничего особо сложного. Закладки являются массивом хешей. Хеш включает в себя следующие ключи: text – название ссылки, act – действие, которое должно быть выполнено, когда пользователь нажал на закладку, в нашем случае это номер страницы для перехода. kids содержит список вложенных ссылок.
После того, как модуль обработает все ссылки из файла описания, они будут добавлены в общий документ с помощью функции prBookmarks($reference).
Приведем пример запуска приложения с опциями командной строки:
perl bin/app-combine-pdfs.pl \
--infile out/file-1.pdf --pages 1-6 \
--infile out/file-2.pdf --pages 1-4,7,9-10 \
--bookmarks out/bookmarks.cnt \
--outfile file-all.pdf –overwrite
К сожалению, PDF::Reuse не позволяет управлять настройками отображения документа после первоначальной загрузки(полный экран или вид с разделителями), так что пользователю нужно будет вручную открыть панель навигации, где будут расположены закладки. В следующем релизе PDF::Reuse данная функция должна появиться, не исключено,что это уже сделано на момент написания данной статьи.
Как уже говорилось, обрабатывать встроенный JavaScript пока может только Acrobat Reader. Для того, чтобы остальные программы могли работать с закладками, нужно заменить ключ act на page(во вложениях Вы можете найти измененную версию PDF::Reuse, включающую данные модификации):
$bookmarks = { text => 'Document',
page => '0,40,50;',
kids => [ { text => 'Chapter 1',
page => '1, 40, 600'
},
{ text => 'Chapter 2',
page => '10, 40, 600'
}
]
}
Этот код может быть вставлен в документ с использованием стандартной prBookmark.
Конечно, консольные приложения – вещь сильная, однако воспользоваться ими могут достаточно продвинутые пользователи. Было бы неплохо предоставить разным людям возможность управлять параметрами создания документов из графической среды. Я решил воспользоваться для этого модулем Perl/Tk. В приложении app-combine-tk-pdfs.pl можно в графическом режиме выбирать файлы pdf, сортировать эти файлы в Tk::Tree и задавать диапазоны страниц. Дополнительно приложение может сохранять параметры генерации в специальных файлах сессий для их последующей загрузки в будущем. В дополнение app-combine-tk-pdfs.pl позволяет редактировать закладки и сохранять изменения в отдельном файле.
PDF::Reuse представляет собой неплохой модуль с подробной документацией, который позволит существенно упростить процесс создания, комбинирования и редактирования pdf файлов. Для демонстрации возможностей данного модуля было разработано два приложения. Следует помнить две вещи – PDF::Reuse не позволяет работать с уже созданными закладками, а так же может нарушить работу уже существующих в документе ссылок. В прилагаемых архивах Вы можете найти примеры, использованные в данной статье и модифицированный код PDF::Reuse, который позволит использовать page вместо act (решение проблемы с JavaScript)
Как, чем и зачем:)
Данный материал представляет собой своего рода путевые заметки человека, только постигающего всю эзотерическую глубину тестирования. По мере накопления информации и критики я буду пополнять данный раздел. Все размышления носят печать глубокого IMHO и не претендуют на истинность.
По мере роста приложения возникает необходимость в тестировании. Конечно, желательно строить процесс разработки таким образом, чтобы тестирование предшествовало выходу релизов, а не последние выступали в роли тестовой платформы(sic бывает очень часто:)). Сразу оговорюсь, речь идет об автоматическом тестировании на основе планов, а не о так называемом UAT – User Acceptance Testing. Это отдельная тема разговора, такое тестирование проводят люди с железными нервами, которым, как я слышал, в одной из отечественных контор даже разрешается пить пиво на работе.
Автоматические тесты предназначены более для выявления дефектов в запросах, поиска просчетов в алгоритмах(бейте граничными значениями по входам – узнаете много нового о своих функциях), нахождении мелких неточностей и упущений. Что представляет собой тест? Это последовательность действий, которая включает в себя проверку загрузки модулей в память, их инициализацию и выполнение некоторых действий с последующим сравнением результатов. Теоретически, подобными тестами можно покрыть все приложение, однако делать это смысла нет, так как даже тотальное авто-тестирование не дает 100% гарантии, что приложение и в жизни будет работать столь же успешно, как и на тестах.
Как мне кажется, одно из основных предназначений тестирования состоит в том, что дает возможность разработчику проверить, а будет ли работать данный метода/класс после внесения некоторых изменений и как эти изменения повлияют на работу других частей приложения, которые используют модифицированный код. Буду судить по себе – я достаточно часто возвращаюсь к уже написанным модулям, когда меня посещает очередная ИДЕЯ. Мне нравится делать рефакторинг уже созданного кода, но далеко не всегда изменения носят сугубо косметический характер и потом превращаются в головную боль. Когда же тесты готовы заранее, можно относительно безопасно работать с кодом. Так что – тесты поощряют разработчика заниматься рефакторингом.
Еще одним полезным моментом является то, что тестировать лучше маленькие порции кода, что уже подразумевает наличие хотя бы какого-то дизайна у приложения:)
Итак, с чего начать? Как же проводить тестирование? Как ни странно, начинается все с бумаги. Перед тем, как что-либо писать, нужно разработать тестовый план. Если необходимо тестировать бизнес-логику – не слезайте с заказчика, пока он не предоставит его, в случае же низкоуровневого взаимодействия полагайтесь только на свою фантазию, будьте максимально пессимистичны относительно входных данных, что почти соответствует тому, что придет в приложение, когда за дело возьмутся реальные пользователи. Давайте представим модуль который подлежит тестированию, как черный ящик, своего рода передаточную функцию, у которой есть входные и выходные сигналы. Хороший тестовый план должен включать в себя описание входных параметров и соответствующих им выходных значений. Так же неплохо включать в тестовый план ошибочные данные, чтобы проверить работу функции в таких вот ситуациях.
Наконец, план разработан, был выписан набор параметров и просчитаны выходные значения. Давайте подумаем, каким образом данные будут попадать на вход тестируемой функции. Хорошим тоном будет загружать тестовый сценарий из файла, нежели жестко кодировать граничные условия. Таким образом тест получается более гибкий и адаптируемый. Думаю, у каждого есть свои собственные излюбленные методы загрузки данных. Лично мне очень понравилось использовать для этого YAML(мы вернемся к этой теме чуть позже), ввиду простоты и формата файла, который можно редактировать вручную.
Идем дальше. Предположим, приложение активно использует какой-то источник данных. Довольно часто в его роли выступает СУБД. В принципе, можно сделать копию реальной рабочей базы с продакшн-сервера и гонять на ней тесты, но этот вариант имеет ряд недостатков. Во-первых, нет детерминированности данных – вы не знаете достоверно, что сейчас находится в таблице/таблицах. Во-вторых, тесты могут менять данные, так что в следующий раз придется иметь дело не только с настоящими данными, но и с синтетическими, попавшими туда в результате тестов(которые, кстати, не все могут закончиться успешно). Как мне кажется, достаточно удобно иметь заранее подготовленный дамп тестовой базы данных, который можно загружать в начале процедуры. Такой дамп можно сделать из реальной базы, удалив 80% информации, оставив лишь пару-тройку записей. Так же неплохо иметь описание этих данных – сколько и что содержится в таблицах, так что если запрос с INNER JOIN вернет 10 строк вместо содержащихся 2 в таблице – это повод серьезно призадуматься.
Подведем краткий итог. Для проведения тестирования нужно:
To be continue