I figured it would probably be worth discussing the design and interface of PGObject, PGObject::Simple, and PGObject::Simple::Role here as well as other areas I am working on directly. The structure is somewhat different from what we have and the design choices somewhat based on lessons learned so far. There are basically three classes here with specific responsibilities.
PGObject's structure is divided into a "bottom half" and a "top half" where the bottom-half is database-facing and the top-half is developer-facing. With a top-half written, the developer should only need to hit the bottom half when writing new top-half components. All application code goes through the top half.
PGObject
PGObject is the bottom half module. It is designed to service multiple top-half paradigms (the Simple paradigm is described below, but also working on a CompositeType paradigm which probably won't be ready initially yet). PGObject has effectively one responsibility: coordinate between application components and the database. This is split into two sub-responsibilities:
1. Locate and run stored procedures
2. Encode/decode data for running in #1 above.
Specifically outside the responsibility of PGObject is anything to do with managing database connections, so every call to a database-facing routine (locating or running a stored procedure) requires a database handle to be passed to it.
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, some of which are already in place in our current 1.4 code, but others are not.
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.
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 works in a similar way and with a similar interface to LedgerSMB::DBObject but with a few key differences:
1. 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.
2. $object->call_procedure uses a different syntax more like $object->exec_method. $object->call_dbmethod is used instead of $object->exec_method for consistency's sake. This could be wrapped for the sake of backwards compatibility.
3. 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.
4. $object->call_dbmethod uses the args argument totally differently (this cannot be wrapped --- for enumerated arguments use call_procedure). Instead of an arrayref of arguments, we have a hashref, where the names take precedence over properties. 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}.
5. Both call_procedure and call_dbmethod 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.
This means that a lot of areas where the LedgerSMB code is remarkably brittle due to being dependent on enumerated arguments we can make it robust and easily.
PGObject::Simple::Role
This is a Moose role handler for PGObject::Simple. It handles a few things better than our current routines.
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.
--
Best Wishes,
Chris Travers
Efficito: Hosted Accounting and ERP. Robust and Flexible. No vendor lock-in.