Мартин Фаулер высказался в пользу баз данных NoSQL в пользу того, что ожидание того, что приложение будет напрямую манипулировать реляционными данными, гораздо менее понятно с точки зрения дизайна приложения, чем инкапсуляция базы данных в слабо связанный интерфейс (например, веб-сервис). Я бы на самом деле пошел еще дальше и указал бы, что такой подход неизменно приводит и к плохому проектированию базы данных, потому что информационный макет становится программным API-контрактом, и, таким образом, нужно либо тратить много времени и усилий на отделение логической схемы от физической структуры хранения, либо один конец иметь окостеневшую физическую структуру, которая никогда не изменится.
Эта проблема давно была понятна в сообществе реляционных баз данных. Однако настоящей проблемой является инструмент. Существует два традиционных инструмента для решения этой проблемы:
1. Обновляемые виды. Затем они формируют реляционный API, который позволяет базе данных хранить информацию отдельно от того, как ее видит приложение. Если вы используете ORM, это действительно ценный инструмент.
2. Хранимые процедуры. Они предоставляют процедурный API, но традиционно относительно хрупкий, основанный на том же подходе, который используется библиотеками. А именно, у вас обычно есть упорядоченная серия аргументов, и ожидается, что все пользователи API согласятся с порядком и количеством аргументов. Хотя это может сработать сносно для одной системы (и даже привести к «аду зависимостей»), оно создает значительные проблемы в большой гетерогенной среде, поскольку число приложений, которые должны координироваться в плане обновлений, становится очень большим. Oracle решает эту проблему, используя редакции на основе редакций, так что вы можете иметь параллельное управление версиями хранимых процедур и позволяет приложениям указывать, над какой редакцией они работают. Это похоже на параллельное управление версиями библиотек C, типичное для Linux,или параллельное управление версиями сборок в .Net.
На стороне приложения ORM стали популярными, но они все еще приводят к тому, что реляционный API является договорным, поэтому его лучше всего использовать с обновляемыми представлениями.
Частично из-за этих недостатков мы начали писать обходные пути для LedgerSMB, начиная с версии 1.3. Реализации специфичны для PostgreSQL . Совсем недавно я написал несколько модулей Perl, теперь на CPAN, для реализации этих концепций. Они создают общую платформу PGObject, которая предоставляет приложению доступ к хранимым процедурам PostgreSQL в слабосвязанной форме. Есть надежда, что будут написаны другие реализации тех же идей, и другие приложения будут использовать эту платформу.
Основная предпосылка заключается в том, что процедурный интерфейс, который можно обнаружить, обеспечивает более простое управление контрактами на программное обеспечение, чем тот, который нельзя обнаружить. Критерии обнаружения затем становятся контрактом на программное обеспечение.
PGObject позволяет построить то, что я называю «парадигмами API», на основе хранимых процедур. Парадигма API — это непротиворечивая спецификация того, как писать обнаруживаемые хранимые процедуры и затем повторно использовать их в приложении. Большинство пространств имен в PGObject представляют такие «парадигмы». В настоящее время исключениями являются пространства имен второго уровня Type, Util, Test и Debug. В настоящее время PGObject :: Simple — единственная доступная парадигма.
Ниже приведена общая запись пригодного в настоящее время подхода PGObject :: Simple и того, что делает каждый модуль:
PGObject
PGObject — это нижняя половина модуля. Он предназначен для обслуживания нескольких парадигм верхней половины (простая парадигма описана ниже, но также работает над парадигмой CompositeType, которая, вероятно, еще не будет готова изначально). У PGObject фактически одна обязанность: координировать работу компонентов приложения и базы данных. Это разделено на две под-обязанности:
- Найдите и запустите хранимые процедуры
- Кодировать / декодировать данные для запуска в # 1 выше.
В частности, PGObject не несет ответственности за управление соединениями с базой данных, поэтому каждый вызов обращенной к базе данных подпрограммы (поиск или запуск хранимой процедуры) требует передачи дескриптора базы данных.
The reason for this is that the database handles should be managed by the application not our CPAN modules and this needs to be flexible enough to handle the possibility that more than one database connection may be needed by an application. This is not a problem because developers will probably not call these functions unless they are writing their own top-half paradigms (in which case the number of places in their code where they issue calls to these functions will be very limited).
A hook is available to retrieve only functions with a specified first argument type. If more than one function is found that matches, an exception is thrown.
The Simple top-half paradigm (below) has a total of two such calls, and that’s probably typical.
The encoding/decoding system is handled by a few simple rules.
On delivery to the database, any parameter that can(‘to_db’) runs that method and inserts the return value in place of the parameter in the stored procedure. This allows one to have objects which specify how they serialize. Bigfloats can serialize as numbers, Datetime subclasses can serialize as date or timestamp strings, and more complex types could serialize however is deemed appropriate (to JSON, a native type string form, a composite type string form, etc).
On retrieval from the database, the type of each column is checked against a type registry (sub-registries may be used for multiple application support, and can be specified at call time as well). If the type is registered, the return value is passed to the $class->from_db method and the output returned in place of the original value. This allows for any database type to be mapped back to a handler class.
Currently PGObject::Type is a reserved namespace for dealing with released type handler classes. We have a type handler for DateTime and one for BigFloat written already and working on one for JSON database types.
PGObject::Simple
The second-level modules outside of a few reserved namespaces designate top-half paradigms for interacting with stored procedures. Currently only Simple is supported.
This must be subclassed to be used by an application and a method provided to retrieve or generate the appropriate database connection. This allows application-specific wrappers which can interface with other db connection management logic.
All options for PGObject->call_procedure supported including running aggregates, order by, etc. This means more options available for things like gl reports database-side than the current LedgerSMB code allows.
$object->call_dbmethod uses the args argument by using a hashref for typing the name to the value. If I want to have a ->save_as_new method, I can add args => {id => undef} to ensure that undef will be used in place of $self->{id}.
Both call_procedure (for enumerated arguments) and call_dbmethod (for named arguments) are supported both from the package and object. So you can MyClass->call_dbmethod(…) and $myobj->call_dbmethod. Naturally if the procedure takes args, you will need to specify them or it will just submit nulls.
PGObject::Simple::Role
This is a Moo/Moose role handler for PGObject::Simple.
One of the main features it has is the ability to declaratively define db methods. So instead of:
sub int { my $self = @_; return $self->call_dbmethod(funcname => 'foo_to_int'); }
You can just
dbmethod( int => (funcname => 'foo_to_int'));
We will probably move dbmethod off into another package so that it can be imported early and used elsewhere as well. This would allow it to be called without the outermost parentheses.
The overall benefits of this framework is that it allows for discoverable interfaces, and the ability to specify what an application needs to know on the database. This allows for many of the benefits of both relational and NoSQL databases at the same time including development flexibility, discoverable interfaces, encapsulation, and more.