package Class::DBI::Plugin::DeepAbstractSearch; our $VERSION = '0.08'; use strict; use warnings; use base 'Class::DBI::Plugin'; use SQL::Abstract; sub init { my $class = shift; $class->set_sql( deeply_and_broadly => qq{%s} ); } sub deep_search_where : Plugged { my $class = shift; my ($what, $from, $where, $bind) = $class->get_deep_where(@_); my $sql = <<""; SELECT DISTINCT $what FROM $from WHERE $where return $class->sth_to_objects($class->sql_deeply_and_broadly($sql), $bind); } sub count_deep_search_where : Plugged { my $class = shift; my ($what, $from, $where, $bind) = $class->get_deep_where(@_); my $sql = <<""; SELECT COUNT(*) FROM $from WHERE $where return $class->sql_deeply_and_broadly($sql)->select_val(@$bind); } # my ($what, $from, $where, $bind) = CDBI->get_deep_where($where, $attr); sub get_deep_where : Plugged { my $class = shift; my $where = (ref $_[0]) ? $_[0] : { @_ }; my $attr = (ref $_[0]) ? $_[1] : undef; my $order = ($attr) ? delete($attr->{order_by}) : undef; my $joins = {}; my $order_fields = ''; ## Collect tables $where = _transform_where($class, $joins, $where); if ($order) { my %order_fields; $order = join(", ", @$order) if ref $order; $order = _transform_order($class, $joins, $order, \%order_fields); $order_fields = join(", ", map { /\./ ? $_ : () } keys %order_fields); $order_fields = ", $order_fields" if $order_fields; } ## Translate to SQL my $sql = SQL::Abstract->new(%$attr); my($filter, @bind) = $sql->where($where, $order); $filter = "WHERE 1=1 $filter" unless $filter =~ /^\s*WHERE/i; my $op = (keys(%$joins) > 1) ? 'AND' : ''; $filter =~ s/^\s*WHERE/$op/i; ## Build __TABLEs__ my $tables = join(', ', map { "__TABLE($_->{class}=$_->{alias})__" } values %$joins) || "__TABLE__"; ## Build __JOINs__ my $join = join(' AND ', map { $_->{fclass} ? "__JOIN($_->{fclass} $_->{alias})__" : () } values %$joins); ## Build pseudo-query my $alias = $joins->{''}->{alias}; my $essential = defined ($alias) ? "__ESSENTIAL($alias)__" : "__ESSENTIAL__"; $sql = join("\0", "$essential$order_fields", $tables, "$join $filter"); ## Transform to real SQL $sql = $class->transform_sql($sql); return (split(/\0/, $sql), \@bind); } # Replace field names with fully qualified (table_alias.field) names sub _transform_where { my ($class, $joins, $where, $hint) = @_; my $ref = ref $where || ''; my $val; $hint ||= ''; if($ref eq 'ARRAY') { my @where = @$where; if ($hint ne 'exps' || $where->[0] !~ /^[a-z]/i) { ## transforming [ operator, expr1, expr2 ] ## or array in { operator => ['assigned', 'in-progress']} $val = []; while ($_ = shift @where) { push @$val, ((ref $_) ? _transform_where($class, $joins, $_) : $_); } } else { ## transforming [ field1 => expr1, field2 => expr2 ] ## or array in { operator => [ field1 => expr1, field2 => expr2 ]} $val = []; while ($_ = shift @where) { push @$val, _transform_field($class, $joins, $_); push @$val, _transform_where($class, $joins, shift @where); } } } elsif ($ref eq 'HASH') { $val = {}; foreach my $key (keys %$where) { if($key !~ /^[a-z]/i) { ## transforming { operator => expr } ## or operator in field => { operator => [ values ] } if($key =~ /^-?\s*(not[\s_]+)?(in|between)\s*$/i) { ## special case for IN and BETWEEN $hint = 'val'; } else { $hint ||= 'exps'; } $val->{$key} = _transform_where($class, $joins, $where->{$key}, $hint); } else { ## transforming { field => expr } $val->{_transform_field($class, $joins, $key)} = _transform_where($class, $joins, $where->{$key}, 'val'); } } } else { ## literal or SQL $val = $where; } $val; } # Change "table.field1.field2, table.field3.field4 DESC" into # "t_table_field1.field2, t_table_field3.field4 DESC" sub _transform_order { my ($class, $joins, $order, $order_fields) = @_; join(", ", map { my @ord = split /\s+/, $_; $ord[0] = _transform_field($class, $joins, $ord[0]); $order_fields->{$ord[0]} = 1; join(" ", @ord); } (split /\s*,\s*/, $order)); } # Change "table.field1.field2" into "t_table_field1.field2" sub _transform_field { my ($class, $joins, $field) = @_; my @path = split /\./, $field; $field = pop @path; my $join = _get_join($class, $joins, @path); "$join->{alias}.$field"; } # Return the join for (table, field1, field2, field3) sub _get_join { my ($class, $joins, @path) = @_; my $join_key = lc join('.', @path); my $join = $joins->{$join_key}; if(!$join) { if(my $field = pop @path) { ## Joined table my $prev_join = _get_join($class, $joins, @path); my $fcl = $prev_join->{class}; my $falias = $prev_join->{alias}; my $col = $fcl->find_column($field) or $class->_croak("$fcl doesn't contain column '$field'"); my $has_a = $fcl->meta_info('has_a') or $class->_croak("$fcl column '$col' doesn't have a 'has_a' relationship"); $has_a = $has_a->{$col} or $class->_croak("$fcl column '$col' doesn't have a 'has_a' relationship"); my $cl = $has_a->foreign_class; $join = { fclass => $falias, fkey => "$col", class => $cl, alias => "${falias}_$col" } } else { ## Primary table $join = { class => $class, alias => "t_" . $class->table } } ## Add join to list of joins $joins->{$join_key} = $join; } $join; } 1; __END__ =head1 NAME Class::DBI::Plugin::DeepAbstractSearch - deep_search_where() for Class::DBI =head1 SYNOPSIS use base 'Class::DBI'; use Class::DBI::Plugin::DeepAbstractSearch; my @cds = Music::CD->deep_search_where( { 'artist.name' => $artist_name } ); =head1 DESCRIPTION This plugin provides a L search method for L. It is similar to L, but allows you to search and sort by fields from joined tables. Note: When searching and sorting by the fields of the current class only, it is more efficient to use L. =head1 METHODS =head2 deep_search_where my @cds = Music::CD->deep_search_where( { 'artist.name' => $artist_name } ); This method will be exported into the calling class, and allows for searching of objects using L format based on fields from the calling class as well as using fields in classes related through a (chain of) 'has_a' relationships to the calling class. When specifying a field in a related class, you separate it with a period from the corresponding foreign key field in the primary class. package Music::Artist; use base 'Class::DBI'; Music::Artist->table('artist'); Music::Artist->columns(All => qw/artistid name/); Music::Artist->has_many(cds => 'Music::CD'); package Music::CD; use base 'Class::DBI'; Music::CD->table('cd'); Music::CD->columns(All => qw/cdid artist title year/); Music::CD->has_many(tracks => 'Music::Track'); Music::CD->has_a(artist => 'Music::Artist'); package Music::Track; use base 'Class::DBI'; Music::Track->table('track'); Music::Track->columns(All => qw/trackid cd position title/); ## Tracks on all CDs with the title "Greatest Hits" @tracks = Music::Track->deep_search_where( { 'cd.title' => "Greatest Hits" }, { sort_by => 'cd.title' } ); ## Tracks on CDs by Willie Nelson, sorted by CD Title and Track Position @tracks = Music::Track->deep_search_where( { 'cd.artist.name' => "Willie Nelson" }, { sort_by => 'cd.title, position' } ); ## First 3 Tracks on CDs, whose title contains "Outlaw", by Willie Nelson @tracks = Music::Track->deep_search_where( { 'cd.artist.name' => "Willie Nelson", 'cd.title' => { -like => '%Outlaw%' }, position => { '<=' => 3 } }, { sort_by => 'cd.title, position' } ); =head2 count_deep_search_where my $num_cds = Music::CD->count_deep_search_where( { 'artist.name' => $artist_name } ); This method will be exported into the calling class, and allows for counting of objects using L format based on fields from the calling class as well as using fields in classes related through a (chain of) 'has_a' relationships to the calling class. =head2 get_deep_where my ($what, $from, $where, $bind) = $class->get_deep_where($where, $attr); This method will be exported into the calling class, and allows for retrieving SQL fragments used for creating queries. The parameters are the same as to deep_search_where. =head1 AUTHOR Stepan Riha, C =head1 COPYRIGHT Copyright (C) 2005, 2007, 2008 Stepan Riha. All rights reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =head1 SEE ALSO L, L, L =cut