Один из вопросов, которые я получил от поста на прошлой неделе, был о том, как работает локатор сервисов LedgerSMB. В прошлом я рассмотрел сторону проектирования базы данных, но не то, как работает локатор служб. Частично причина в том, что сервисный локатор все еще находится в стадии разработки, и со временем мы ожидаем получить что-то более полнофункциональное, чем то, что мы имеем сейчас, что просто, но работает.
В настоящее время мы используем API хранимых процедур, разделенный на верхнюю и нижнюю половину. Верхняя половина генерирует вызов API для нижней половины, который может вызываться независимо, когда требуется более точное управление.
Нижняя половина
Нижняя половина этого механизма — API вызова хранимой процедуры. Perl API использует следующий синтаксис:
@results = call_procedure( procname => $procname, args => $args );
@results — это массив хеш-ссылок, каждая из которых представляет возвращаемую строку. $ procname, естественно, является именем процедуры, а $ args — ссылкой на массив значений скалярных аргументов.
Например, если я хочу вызвать функцию с именем company__get, которая принимает один аргумент $ id, я мог бы:
($company) = call_procedure(
procname => 'company__get', args => [$id]
);
Затем эта функция генерирует запрос из аргументов:
SELECT * FROM company__get(?);
И запускает его, параметризованный, с $ id в качестве аргумента.
Полный исходный код функции в Perl:
sub call_procedure {
my $self = shift @_;
my %args = @_;
my $procname = $args{procname};
my $schema = $args{schema};
my @call_args;
my $dbh = $LedgerSMB::App_State::DBH;
if (!$dbh){
$dbh = $self->{dbh};
}
@call_args = @{ $args{args} } if defined $args{args};
my $order_by = $args{order_by};
my $query_rc;
my $argstr = "";
my @results;
if (!defined $procname){
$self->error('Undefined function in call_procedure.');
}
$procname = $dbh->quote_identifier($procname);
# Add the test for whether the schema is something useful.
$logger->trace("\$procname=$procname");
$schema = $schema || $LedgerSMB::Sysconfig::db_namespace;
$schema = $dbh->quote_identifier($schema);
for ( 1 .. scalar @call_args ) {
$argstr .= "?, ";
}
$argstr =~ s/\, $//;
my $query = "SELECT * FROM $schema.$procname()";
if ($order_by){
$query .= " ORDER BY $order_by";
}
$query =~ s/\(\)/($argstr)/;
my $sth = $dbh->prepare($query);
my $place = 1;
# API Change here to support byteas:
# If the argument is a hashref, allow it to define it's SQL type
# for example PG_BYTEA, and use that to bind. The API supports the old
# syntax (array of scalars and arrayrefs) but extends this so that hashrefs
# now have special meaning. I expect this to be somewhat recursive in the
# future if hashrefs to complex types are added, but we will have to put
# that off for another day. --CT
foreach my $carg (@call_args){
if (ref($carg) eq 'HASH'){
$sth->bind_param($place, $carg->{value},
{ pg_type => $carg->{type} });
} else {
$sth->bind_param($place, $carg);
}
++$place;
}
$query_rc = $sth->execute();
if (!$query_rc){
if ($args{continue_on_error} and # only for plpgsql exceptions
($dbh->state =~ /^P/)){
$@ = $dbh->errstr;
} else {
$self->dberror($dbh->errstr . ": " . $query);
}
}
my @types = @{$sth->{TYPE}};
my @names = @{$sth->{NAME_lc}};
while ( my $ref = $sth->fetchrow_hashref('NAME_lc') ) {
for (0 .. $#names){
# numeric float4/real
if ($types[$_] == 3 or $types[$_] == 2) {
$ref->{$names[$_]} = Math::BigFloat->new($ref->{$names[$_]});
}
}
push @results, $ref;
}
return @results;
}
В дополнение к частям, описанным выше, эта функция также выполняет некоторую базовую обработку ошибок, делегируя другую функцию, которая регистрирует полные ошибки и скрывает некоторые ошибки (особенно чувствительные к безопасности) за более общими сообщениями об ошибках, обращенными к пользователю.
Логика в отношении обработки типов и тому подобного немного скромнее, но это примерно для основной логики.
Будущие улучшения для нижней половины
В будущем я хотел бы добавить ряд функций, включая определения окон и агрегаты оконных функций, которые можно привязать к выводу функции. По сути, я хотел бы иметь возможность перейти от максимальной сложности чего-то вроде:
SELECT * FROM my_func(?) order by foo;
в
SELECT *, sum(amount)
over (partition by reference order by entry_id)
AS running_balance
FROM gl_report(?, ?, ?) order by transdate;
Подобные вещи сделали бы функции отчетности намного более гибкими.
Верхняя половина
Верхняя половина служит общим сервисом в отношении расположения хранимой процедуры. Функция находится в модуле DBObject и называется exec_method. Эта функция обеспечивает возможности определения местоположения службы при условии, что имена функций уникальны (это может измениться в будущих поколениях, когда мы будем экспериментировать с другими представлениями и интерфейсами).
Верхняя половина в настоящее время использует исключительно подход объект-свойство для сопоставления аргументов или подход перечислимого аргумента. Нет возможности смешивать их, что является текущим недостатком. Текущий код допускает подход с перечисленными аргументами, который я почти никогда не использую, поскольку он относительно хрупок.
Кроме того, API заказа в коде Perl действительно неоптимален и должен быть переделан в будущих версиях.
Код Perl:
sub exec_method {
my $self = shift @_;
my %args = (ref($_[0]) eq 'HASH')? %{$_[0]}: @_;
my $funcname = $args{funcname};
my $schema = $args{schema} || $LedgerSMB::Sysconfig::db_namespace;
$logger->debug("exec_method: \$funcname = $funcname");
my @in_args;
@in_args = @{ $args{args} } if $args{args};
my @call_args;
my $query = "
SELECT proname, pronargs, proargnames FROM pg_proc
WHERE proname = ?
AND pronamespace =
coalesce((SELECT oid FROM pg_namespace WHERE nspname = ?),
pronamespace)
";
my $sth = $self->{dbh}->prepare(
$query
);
my $ref;
$ref = $sth->fetchrow_hashref('NAME_lc');
my $pargs = $ref->{proargnames};
my @proc_args;
if ( !$ref->{proname} ) { # no such function
# If the function doesn't exist, $funcname gets zeroed?
$self->error( "No such function: $funcname");
# die;
}
$ref->{pronargs} = 0 unless defined $ref->{pronargs};
# If the user provided args..
if (!defined $args{args}) {
@proc_args = $self->_parse_array($pargs);
if (@proc_args) {
for my $arg (@proc_args) {
#print STDERR "User Provided Args: $arg\n";
if ( $arg =~ s/^in_// ) {
if ( defined $self->{$arg} )
{
$logger->debug("exec_method pushing $arg = $self->{$arg}");
}
else
{
$logger->debug("exec_method pushing \$arg defined $arg | \$self->{\$arg} is undefined");
#$self->{$arg} = undef; # Why was this being unset? --CT
}
push ( @call_args, $self->{$arg} );
}
}
}
for (@in_args) { push @call_args, $_ } ;
$self->{call_args} = \@call_args;
$logger->debug("exec_method: \$self = " . Data::Dumper::Dumper($self));
return $self->call_procedure( procname => $funcname,
args => \@call_args,
order_by => $self->{_order_method}->{"$funcname"},
schema=>$schema,
continue_on_error => $args{continue_on_error});
}
else {
return $self->call_procedure( procname => $funcname,
args => \@in_args,
order_by => $self->{_order_method}->{"$funcname"},
schema=>$schema,
continue_on_error => $args{continue_on_error});
}
}
Возможности для будущих улучшений
Первоначальные улучшения включают замену API перечислимого аргумента на API, в котором хеш-адрес может быть передан с перезаписью части или всех аргументов, отправленных в базу данных. Это продолжит делать API гибким и динамичным, но позволит сделать клиентский код более насыщенным. API заказа также необходимо перенести в фактический вызов API, а не в отдельный вызов.
Интерфейс следующего поколения в разработке
Интерфейс следующего поколения будет поддерживать вызов API, например:
SELECT * FROM save('(,A-12334,"Test, Inc.",232)'::entity);
Основной проблемой здесь является рекурсивное построение того, что по сути является потенциально вложенной структурой CSV. Например, мы могли бы иметь:
SELECT * FROM save('(,JE-12334,Cash Transfer,2013-05-01,f,"{""(,4,-1000)"",""(,7,1200)"",""(,12,200)""}")'::journal_entry);
Бегство на самом деле не слишком сложно. Ключевыми проблемами на самом деле являются вопросы оптимизации производительности, такие как обеспечение правильного кэширования структур данных и тому подобное.
Однако в дополнение к этой проблеме я хотел бы иметь возможность определять оконные функции для наборов результатов через API, чтобы можно было добавлять текущие балансы в базу данных (где они могут быть выполнены наиболее эффективно).
Над этим уже проделана большая работа.
Лицензирование
Приведенный выше код распространяется по лицензии GNU General Public License версии 2 или по вашему выбору любой более поздней версии. Код не является самым чистым кодом, который мы написали на эту тему, но это код, который используется LedgerSMB в производстве.
Если вы хотите, чтобы с лицензией BSD работал код, который также, вероятно, является более чистым кодом, ознакомьтесь с
реализацией PHP, которую мы с Джоном Локком соединили.