Ask use.perl.org - Class Tree + Driver API ?

Alias on 2007-05-29T16:52:08

Class trees are great. If your problem is complex and suitably hierarchal they provide the conceptual skeleton you need to solve the problem completely. I use class trees a lot.

Driver APIs are also great. They let you wrap an abstracted simple API over the top of a concept that has highly diverse implementations and hide the implementation details away.

They also let you reduce design risk. Introducing a driver API lets you drop in an alternative implementation at a later date with little to no pain, and even have the new (better, brighter, cooler) implementation co-exist with the old one. So upgrades can be rolled out across a subset of current users. I use driver APIs a lot.

Of course, if you could take both things that are themselves great, and COMBINE the two into some sort of "Driver Class Tree API" it should be stupendously awesome!

Except it isn't. In fact, it sucks rather hard. And I'm not sure how to make it suck less.

For example, I have a non-CPAN class tree for modeling SQL queries. It's code-generated for each application with the appropriate code to generate the SQL for the database for that project and does some other magical stuff I can only do easily because I'm using code-gen.

Suffice to say I'm really happy with it, since it allows things like this magically across arbitrary table structures. my $query = MyApp::Entity::Person->select; $query->condition( 'age', '==', 30 ); $query->condition( 'children.friends.school.teachers.pets.name', '==', 'Fido' ); $query->orderby( '+name' ); my $adam = $query->first( $dbh );

Now, much like a number of other CPAN modules that originated in my code-gen system, I'd love to abstract it out to CPAN one day.

But this class tree (there's about 15 classes) only works for one database. The "agnostic" version that gets generated has stuff like "if ( postgres ) { ... }" all through it, which obviously doesn't scale.

But how do I make the class structure work when 13 of those 15 classes may need to be subclasses/modified by the driver?

How do you typically add "driverness" to a class tree without resorting to ugly things like: sub condition { my $self = shift; my $condition_class = $self->driver_class('Condition'); my $condition = $condition_class->new( @_ ); push @{$self->{conditions}}, $condition; return 1; }

Anyone got a better solution?


Re: Ask use.perl.org - Class Tree + Driver API ?

cosimo on 2007-05-29T19:23:02

Hi Alias,

I don't know if I understood completely what you are trying to accomplish, but I think I have developed a similar system, that is used to access different data sources (txt, ftp, http, sql, AS/400, ...) with a single interface. Sql class in particular has the behaviour you mention, in that it supports Postgres, Mysql, Oracle, DB2, Sybase, Informix, ...

In my case, I have a "Driver" class with many subclasses like Pg, Oracle, Sybase, MSSQL, ... that are embedded in a $self->{driver} member.

When I need to specialize behaviour for (example) Postgres, I never test for driver name, but rather for a special "feature" (missing or implemented) by the database.

An example is worth 1000 words:

sub get_first_10_records
{
    my $self = $_[0];

    # Get underlying driver class (don't care if Pg, Mysql, ...)
    my $drv  = $self->driver();
    my $sql  = 'SELECT * FROM hgttg';
    my $has_ph = $drv->supports('placeholders');
    my $has_limit = $drv->supports('limit');

    if($has_ph)
    {
        $sql .= ' WHERE answer=?';
    }
    else
    {
        $sql .= ' WHERE answer=42';
    }

    if($has_limit)
    {
        $sql .= ' LIMIT 10';
    }

    my @recs = $self->fire_query($sql, 42);

    if(!$has_limit)
    {
       @recs = splice(@recs, 0, 10);
    }

    return(\@recs);
}

Don't get me wrong. This example is not very exciting, but I think the concept behind is interesting. Checking for features *across* all driver classes, creating a "common denominator" approach, rather than specializing by class.

Sorry if I have totally missed the point... :-)

DBIx::Class

ChrisDolan on 2007-05-30T05:27:44

I'm probably over-simplifying your problem, but I think that the solution adopted by DBIx::Class is a nice brain fit. Under Catalyst, I would say something like:

my $query = $c->model('Entity::Person');
$query = $query->search({age => ['==', 30]});
$query = $query->search({mumble...  => ['==', 'Fido']});
$query = $query->search(undef, {order_by => 'name ASC'});
my $adam = $query->first;
where I don't know how to write the "mumble" part off the top of my head, but it's probably accomplished with related_resultset calls. Interestingly, DBIx::Class has managed to keep the "driverness" to a very small set of variations in the DBIx::Class::Storage subclasses.

Re:DBIx::Class

Alias on 2007-05-31T07:32:54

Unfortunately, yes this oversimplifies the problem.

It solves the problem by getting rid of the class tree altogether and using Perl structures.

Here's another example of the sort of stuff I'm doing.

# We need a certain type of person
my $query = MyApp::Entity::Person->Fetch;
$query->condition('OptimizedFor', '==', 'fun');
$query->condition(
        MyApp::SQL::Collection->OR(
                MyApp::SQL::Clause->new('Lame', 'is null'),
                MyApp::SQL::Clause->new('Age', 'new('Name', '!in', 'Adam', 'Bob', 'Whatever'),
        )
);

# What projects are they in
my $projects = $query->traverse('FoundedProjects');
$projects->orderby('+Name');
$projects->limit(10);
my @projects = $projects->fetch( $dbh );

# Lets save that query so we can send it to bob,
# who is using the same application, but with Oracle.
$projects->save_as( 'WatchList.dat' );


As you can see, the SQL objects not only let me build up arbitrary nested structures, and create new queries with existing ones as subselects/joins, but the queries also serialize to a logical form, regardless of the database they will be executed on.

So it's basically an abstract tree for the queries of various types, that ignores the actual table names and column names (which might vary) and ignores the implementation method (for example, on Oracle, limit is implemented via a subselect).

This is where the main issue of Tree + Driver kicks in...

Re:DBIx::Class

Alias on 2007-05-31T07:33:49

Ack, my "less than" condition in the nested structure broke it :(

But you get the point.