package Fey::Object::Iterator::FromSelect; use strict; use warnings; our $VERSION = '0.30'; use Fey::Exceptions qw( param_error ); use Devel::GlobalDestruction; use Moose; use MooseX::SemiAffordanceAccessor; use MooseX::StrictConstructor; with 'Fey::ORM::Role::Iterator'; has dbh => ( is => 'ro', isa => 'DBI::db', required => 1, ); has select => ( is => 'ro', does => 'Fey::Role::SQL::ReturnsData', required => 1, ); has bind_params => ( is => 'ro', isa => 'ArrayRef', lazy => 1, default => sub { [ $_[0]->select()->bind_params() ] }, ); has _sth => ( is => 'ro', isa => 'DBI::st', writer => '_set_sth', predicate => '_has_sth', clearer => '_clear_sth', init_arg => undef, lazy => 1, builder => '_build_sth', ); has 'attribute_map' => ( is => 'ro', isa => 'HashRef[HashRef[Str]]', default => sub { return {} }, ); has _class_attributes_by_position => ( is => 'ro', isa => 'HashRef[HashRef[Str]]', init_arg => undef, lazy => 1, builder => '_build_class_attributes_by_position', ); has raw_row => ( is => 'rw', isa => 'Maybe[ArrayRef]', init_arg => undef, writer => '_set_raw_row', ); no Moose; __PACKAGE__->meta()->make_immutable(); sub BUILD { my $self = shift; $self->_validate_attribute_map(); } sub _validate_attribute_map { my $self = shift; my $map = $self->attribute_map(); return unless keys %{ $map }; my %valid_classes = map { $_ => 1 } @{ $self->classes() }; for my $class ( map { $_->{class} } values %{ $map } ) { die "Cannot include a class in attribute_map ($class) unless it also in classes" unless $valid_classes{$class}; } } sub _get_next_result { my $self = shift; my $sth = $self->_sth(); my $row = $sth->fetchrow_arrayref(); $self->_set_raw_row($row); return unless $row; my $map = $self->_class_attributes_by_position(); my @result; for my $class ( @{ $self->classes() } ) { my %attr = map { $map->{$class}{$_} => $row->[$_] } keys %{ $map->{$class} }; $attr{_from_query} = 1; # FIXME - This eval is kind of a band-aid. It is possible # (especially with DBD::Mock) for %attr to contain bogus data # (wrong types). However, it's also possible for %attr to # contain undefs for non-NULLable columns when iterating over # the results of a select, especially outer joins. # # In the outer join case, we do want to ignore object # construction errors, but otherwise we don't. # # Fortunately, bogus data is unlikely, unless the caller # explicitly provides a bad attribute_map, or a valid # attribute_map and a crazy query. It also can happen pretty # easily with DBD::Mock. push @result, eval { $class->new( \%attr ) } || undef; } return \@result; } sub _build_sth { my $self = shift; my $sth = $self->dbh()->prepare( $self->select()->sql( $self->dbh() ) ); $sth->execute( @{ $self->bind_params() } ); return $sth; } sub _has_explicit_attribute_map { my $self = shift; return keys %{ $self->attribute_map() }; } sub _build_class_attributes_by_position { my $self = shift; return $self->_remap_explicit_attribute_map() if $self->_has_explicit_attribute_map; my $x = 0; my %map; for my $s ( $self->select()->select_clause_elements() ) { if ( $s->can('table') ) { my $class = Fey::Meta::Class::Table->ClassForTable( $s->table() ); $map{$class}{$x} = $s->can('alias_name') ? $s->alias_name() : $s->name(); } $x++; } return \%map; } sub _remap_explicit_attribute_map { my $self = shift; my $explicit_map = $self->attribute_map(); my %map; for my $pos ( keys %{ $explicit_map } ) { $map{ $explicit_map->{$pos}{class} }{$pos} = $explicit_map->{$pos}{attribute}; } return \%map; } sub reset { my $self = shift; $self->_finish_handle(); $self->_clear_sth(); $self->_reset_index(); return; } sub DEMOLISH { my $self = shift; $self->_finish_handle(); } sub _finish_handle { my $self = shift; # We really don't care about cleanly finishing statement handles # in this case, and this code just doesn't work so well in that # case anyway. return if in_global_destruction(); return unless $self->_has_sth(); $self->_sth()->finish() if $self->_sth()->{Active}; } no Moose; no Moose::Util::TypeConstraints; __PACKAGE__->meta()->make_immutable(); 1; __END__ =head1 NAME Fey::Object::Iterator::FromSelect - Wraps a DBI statement handle to construct objects from the results =head1 SYNOPSIS use Fey::Object::Iterator::FromSelect; my $iter = Fey::Object::Iterator::FromSelect->new ( classes => 'MyApp::User', select => $select, dbh => $dbh, bind_params => \@bind, ); print $iter->index(); # 0 while ( my $user = $iter->next() ) { print $iter->index(); # 1, 2, 3, ... print $user->username(); } $iter->reset(); =head1 DESCRIPTION This class implements an iterator on top of a DBI statement handle. Each call to C returns one or more objects based on the data returned by the statement handle. =head1 METHODS This class provides the following methods: =head2 Fey::Object::Iterator::FromSelect->new(...) This method constructs a new iterator. It accepts the following parameters: =over 4 =item * classes This can be a single class name, or an array reference of class names. These should be classes associated with the tables from which data is being C. This is an optional parameter. If it not passed, then the bind parameters will be obtained by calling the C method on the "select" parameter. =item * attribute_map This lets you explicitly map an element of the C clause to a class's attribute. You can also provide your own explicit mappings as needed. In the absence of an explicit mapping, it checks to see if the element has a C method. If it does, it calls C<< Fey::Meta::Class::Table->ClassForTable >> in order to get a class name for the table. Then it uses the value of C (for column objects) or C (for column alias objects) as the name of the attribute to be passed to the class's constructor. If the class is not listed in the iterator's "classes" attribute, then it will simply be ignored. If the element does not have a C method or an explicit mapping, it is ignored. This default works for most queries, where you're just selecting some or all of the columns from one or more tables. In more exotic cases, you can specify an explicit mapping. The mapping maps a C clause elements. The numbers start from zero (0) just like a Perl array. The values are themselves a hash reference specifying a "class" and "attribute" of that class. This explicit mapping is useful for more "exotic" queries. For example: SELECT Message.user_id, COUNT(message_id) AS message_count FROM Message ORDER BY message_count DESC GROUP BY user_id LIMIT 10 This query selects to the top 10 most frequent message posters from a C table. Assuming our C class has a C attribute, we'd like to create a list of C objects from this query. Fey::Object::Iterator::FromSelect->new ( classes => [ 'User', 'Message' ], dbh => $dbh, select => $select, attribute_map => { 0 => { class => 'User', attribute => 'user_id', }, 1 => { class => 'User', attribute => 'message_count', }, ); Explicit mappings to classes not listed in the "classes" attribute cause an error at object construction time. =head1 ROLES This class does the L role. =head1 AUTHOR Dave Rolsky, =head1 BUGS See L for details. =head1 COPYRIGHT & LICENSE Copyright 2006-2009 Dave Rolsky, All Rights Reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. The full text of the license can be found in the LICENSE file included with this module. =cut