Один из вопросов, которые я получил от поста на прошлой неделе, был о том, как работает локатор сервисов 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, которую мы с Джоном Локком соединили.