Chained Searches: The Beauty of DBIx::Class And Catalyst
DBIx::Class and Catalyst have made my life much easier since I adopted them. I originally began my $work project with Class::DBI. The transition took some work but I’ve been happy as a moose in a brothel ever since. Aside from the occasional hairy query I don’t think I push the limits of either Cat or dbic very often. There is one feature, however, that I feel has really illustrated the enormous power these projects bring to the table.
Chained Searching
All of us probably implement ‘list’ actions much the same way:
sub list : Local {
my ($self, $c) = @;
# … acquire a $schema
OneMoGin::Util->setuppager($c);
$c->stash->{’attributes’}->{’orderby’} = ‘name’;
my @orders = $schema->resultset(’Order’)->search(
$c->stash->{’conditions’}, $c->stash->{’attributes’}
);
$c->stash()->{’orders’} = \@orders;
$c->stash()->{’template’} = ‘order/list.tmpl’;
}
This is pretty straightforward. The only trick is that I have a ’setuppager’ method that automagically grabs any page or count parameters from the request and stuffs them into the stash’s ‘attributes’. It makes sure that conditions and attributes are set to sane values. Anyway, on to the meat.
Besides Orders our new application has a pile of other entities: Customers, Products, etc! The list actions for the entities are basically the same. I’d like to have a single page where the user can search all of our entities. Something like this:

We won’t be covering how to create this markup for this. Showing the controllers should be enough. The boring parts are thus: My search page submits to a root ’search’ controller that assures that the entity being searched is valid and that the search types and values are stored in the stash. Now for the fun part: Search/Order.pm
sub default : Private {
my ($self, $c) = @;
#… get a $schema
$c->stash->{’search_rs’} = $schema->resultset(’Order’);
my $count = 0;
foreach my $search (@{ $c->stash->{’searchby’} }) {
if($self->can(”search$search”)) {
$c->forward(”search_$search”,
[ $c->stash->{'value'}->[$count] ]
);
}
$count++;
}
$c->forward(’/order/list’);
}
As you can see, we create a ‘base’ ResultSet that, run as-is, would return all Orders. We expect that the ’searchby’ and ‘value’ params will be arrayrefs. These would be something like “search?searchby=date&value=today&searchby=name&value=smith”. For each ’searchby’ we make sure this controller has a method of the same name, then we call each one! Now for the individual search actions.
sub searchdate : Private {
my ($self, $c, $date) = @;
my $dt = DateTime::Format::DateParse->parsedatetime($date);
# ignoring potential parse failures since this is an example
my $start = $dt->ymd(’-').’ 00:00:00′;
my $end = $dt->ymd(’-').’ 23:59:59′;
my $rs = $c->stash->{’searchrs’};
$c->stash->{’searchrs’} = $rs->search({
‘me.datecreated’ => {
-between => [ $start, $end ]
}
});
}
Hot damn! We take the date given by the default action and build a between query off of the ResultSet we have in the stash. The beauty of this is that we can continue to build of our ResultSet ad nauseam. Things can get hairy fast:
sub searchshipname : Private {
my ($self, $c) = @;
my $rs = $c->stash->{’searchrs’};
$c->stash->{’searchrs’} = $rs->search(
{
‘-nest’ => [
'address.namefirst' => {
-like => "\%$name\%"
},
'address.namelast' => {
-like => "\%$name\%"
}
]
}, {
‘join’ => {
’shipment’ => ‘address’
}
}
);
}
DBIx::Class doesn’t actually execute a query until you try and pull data out of it. So, finally, in our list action we can do:
my @orders = $c->stash->{'search_rs'}->search(
$c->stash->{'constraints'},
$c->stash->{'attributes'}
);
Now we’ve come full circle. One might want to add an if to create a ‘default’ resultset if there isn’t one in the stash. That way your list controller can serve your chained searches as well as simple lists!

Comments (3 comments)
[...] An understanding of chained resultset usage. [...]
Hanging Antlers On Your ResultSets - One Mo’ Gin - Sometimes Once Isn’t Enough / February 28th, 2008, 7:51 pm / #
In the first example, the last two lines contain $c->stash()-> - is it a typo, or a kind of magic?
Bruno / March 26th, 2008, 6:39 am / #
It’s me being verbose. The parens are optional and I am generally ridiculed for using them.
gphat / March 26th, 2008, 7:37 am / #
Post a comment