Using joins

From ClassDBI

Jump to: navigation, search

Based on a discussion from the mailing list, there appear to be a couple of ways to handle joins using CDBI:

A simple case of using set_sql to create joins is provided in the documentation.

However, it is possible to do more advanced joins, such as creating dynamic WHERE statements based on user input such as a web-based search form. The code to setup this method would look like (untested):

   $class->set_sql(my_query => <<"");
   SELECT __ESSENTIAL__
   FROM   __TABLE(Class::One=c1)__,
          __TABLE(Class::Two=c2)__
   WHERE  __JOIN(c1 c2)__
   AND    %s

The above call to set_sql will generate a search_my_query function as described in the documentation. However, you can create an alternate method which can define how the WHERE clause is built:

 sub my_join_search {
   my ($class, $args) = @_;
   my $where_clause = join " AND ", map "$_ LIKE ?", keys %$args;
   return $class->sth_to_objects(
       $class->sql_my_query($where_clause), values %args
       );
 }

Tony made the following remarks regarding the above solution:

To be really robust you'll need to cope with overriden accessor names etc, and you can abstract away that LIKE to be more generic so you don't have to repeat yourself for ILIKE or EQUALS or whatever. But that should give the gist. It's how search() works in Class::DBI itself, and you can always look at it for some more details on how to abstract this further. And, as I said earlier, I should really abstract more of this out further so that writing these yourself is even easier...

Using SQL::Abstract in the my_join_search() helps a lot in abstracting the details:

 sub my_join_search {
   my ($class, @args) = @_;
   my $sql = SQL::Abstract->new();
   my ($where, @bind) = $sql->where(@args);
   return $class->sth_to_objects(
       $class->sql_my_query($where), \@bind
       );
 }

-- RhesaRozendaal

UPDATE: If using the set_sql from above, you need to add the following line before the "return $class..." to avoid SQL syntax errors:

    $where =~ s/^\s*WHERE//;

-- WilliamMcKee


Building on the above methods, I had class A 1-to-many B many-to-1 C many-to-1 D, and I wanted to build a "virtual view" to find all B's where conditions were true in both A and D. So, I created a set_sql in B's class:

 __PACKAGE__->set_sql(abcd_view => q{
 SELECT __ESSENTIAL__
 FROM a NATURAL JOIN b NATURAL JOIN c NATURAL JOIN d
 %s
 });

And then code to call it in B:

 sub abcd_where {
   my $class = shift;
   my $sql = SQL::Abstract->new;
   my ($where, @bind) = $sql->where(@_);
   # untaint $where
   $where =~ /(.*)/s or die;
   $where = $1;
   return $class->sth_to_objects($class->sql_abcd_view($where), \@bind);
 }

And now I can say things like this in my main code.

 my @good_b = B->abcd_where({'a.foo' => 15, 'd.bar' => [3, 5, 9]});

Rock on.

-- RandalSchwartz


If you like the CDBI::AbstractSearch interface (and who wouldn't?) but need to search (or sort) on fields from joined tables (and who doesn't?), you can use the Class::DBI::Plugin::DeepAbstractSearch plugin. It automatically generates the necessary joins when building the query. It uses the SQL::Abstract syntax, but you can specified "dotted" fields:

 my @cds = Music::CD->deep_search_where( { 'artist.name' => $artist_name } );
 my @tracks = Music::Track->deep_search_where(
   {
     'cd.artist.name' => $artist_name,
     'cd.year' => { '>=', $year }
     'track_position' => { '<=', $num_tracks }
   },
   {
     order_by => 'cd.artist.name, cd.year DESC, track_position'
   }
 );

When processing form input, your code usually looks like this:

 my $cgi = ...;
 my $where = {};
 $where->{ 'cd.artist.name' } => $cgi->param('artist_name')
   if $cgi->param('artist_name');
 $where->{ 'cd.year' } => { '>=', $cgi->param('year') }
   if $cgi->param('year');
 $where->{ 'track_position' } => { '<=', $cgi->param('track_position') }
   if $cgi->param('track_position');
 $where->{ 'cd.title' } => { -like => '%' . $cgi->param('title') . '%' }
   if $cgi->param('title');

 my @tracks = Music::Track->deep_search_where ($where, { order_by => ... });

-- StepanRiha

Rather than creating complex joins, I have been experimenting with postgresql views. I then create a Class::DBI class for the view as if it were a normal table.

I am not sure if Class::DBI is intended to work with views in this way but so-far it seems to work ok with the exception of Lazy Population for column names. I have had to declare all the views columns as being Essential to ensure that it populates the object correctly.

-- IcyDee

Personal tools