—our
$VERSION
=
'0.08'
;
use
strict;
use
warnings;
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<SQL::Abstract> search method for L<Class::DBI>.
It is similar to L<Class::DBI::AbstractSearch>, 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<Class::DBI::AbstractSearch>.
=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<SQL::Abstract> 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<SQL::Abstract> 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<sriha@cpan.org>
=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<Class::DBI>, L<SQL::Abstract>, L<Class::DBI::AbstractSearch>
=cut