File Coverage

blib/lib/Fey/Object/Iterator/FromSelect.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Fey::Object::Iterator::FromSelect;
2             {
3             $Fey::Object::Iterator::FromSelect::VERSION = '0.46';
4             }
5              
6 1     1   73919 use strict;
  1         3  
  1         40  
7 1     1   5 use warnings;
  1         2  
  1         33  
8 1     1   1379 use namespace::autoclean;
  1         21128  
  1         6  
9              
10 1     1   528 use Fey::Exceptions qw( param_error );
  0            
  0            
11             use Fey::ORM::Types qw( ArrayRef HashRef Maybe Str );
12              
13             use Devel::GlobalDestruction;
14             use Moose;
15             use MooseX::SemiAffordanceAccessor;
16             use MooseX::StrictConstructor;
17              
18             with 'Fey::ORM::Role::Iterator';
19              
20             has dbh => (
21             is => 'ro',
22             isa => 'DBI::db',
23             required => 1,
24             );
25              
26             has select => (
27             is => 'ro',
28             does => 'Fey::Role::SQL::ReturnsData',
29             required => 1,
30             );
31              
32             has bind_params => (
33             is => 'ro',
34             isa => ArrayRef,
35             lazy => 1,
36             default => sub { [ $_[0]->select()->bind_params() ] },
37             );
38              
39             has _sth => (
40             is => 'ro',
41             isa => 'DBI::st',
42             writer => '_set_sth',
43             predicate => '_has_sth',
44             clearer => '_clear_sth',
45             init_arg => undef,
46             lazy => 1,
47             builder => '_build_sth',
48             );
49              
50             has 'attribute_map' => (
51             is => 'ro',
52             isa => HashRef [ HashRef [Str] ],
53             default => sub { return {} },
54             );
55              
56             has _class_attributes_by_position => (
57             is => 'ro',
58             isa => HashRef [ HashRef [Str] ],
59             init_arg => undef,
60             lazy => 1,
61             builder => '_build_class_attributes_by_position',
62             );
63              
64             has raw_row => (
65             is => 'rw',
66             isa => Maybe [ArrayRef],
67             init_arg => undef,
68             writer => '_set_raw_row',
69             );
70              
71             sub BUILD {
72             my $self = shift;
73              
74             $self->_validate_attribute_map();
75             }
76              
77             sub _validate_attribute_map {
78             my $self = shift;
79              
80             my $map = $self->attribute_map();
81              
82             return unless keys %{$map};
83              
84             my %valid_classes = map { $_ => 1 } @{ $self->classes() };
85              
86             for my $class ( map { $_->{class} } values %{$map} ) {
87             die
88             "Cannot include a class in attribute_map ($class) unless it also in classes"
89             unless $valid_classes{$class};
90             }
91             }
92              
93             sub _get_next_result {
94             my $self = shift;
95              
96             my $sth = $self->_sth();
97              
98             my $row = $sth->fetchrow_arrayref();
99              
100             $self->_set_raw_row($row);
101              
102             return unless $row;
103              
104             my $map = $self->_class_attributes_by_position();
105              
106             my @result;
107             for my $class ( @{ $self->classes() } ) {
108             my %attr = map { $map->{$class}{$_} => $row->[$_] }
109             keys %{ $map->{$class} };
110             $attr{_from_query} = 1;
111              
112             # FIXME - This eval is kind of a band-aid. It is possible
113             # (especially with DBD::Mock) for %attr to contain bogus data
114             # (wrong types). However, it's also possible for %attr to
115             # contain undefs for non-NULLable columns when iterating over
116             # the results of a select, especially outer joins.
117             #
118             # In the outer join case, we do want to ignore object
119             # construction errors, but otherwise we don't.
120             #
121             # Fortunately, bogus data is unlikely, unless the caller
122             # explicitly provides a bad attribute_map, or a valid
123             # attribute_map and a crazy query. It also can happen pretty
124             # easily with DBD::Mock.
125             push @result, $self->_new_object( $class, \%attr );
126             }
127              
128             return \@result;
129             }
130              
131             sub _new_object {
132             my $self = shift;
133             my $class = shift;
134             my $attr = shift;
135              
136             eval { $class->new($attr) } || undef;
137             }
138              
139             sub _build_sth {
140             my $self = shift;
141              
142             my $sth = $self->dbh()->prepare( $self->select()->sql( $self->dbh() ) );
143              
144             $sth->execute( @{ $self->bind_params() } );
145              
146             return $sth;
147             }
148              
149             sub _has_explicit_attribute_map {
150             my $self = shift;
151              
152             return keys %{ $self->attribute_map() };
153             }
154              
155             sub _build_class_attributes_by_position {
156             my $self = shift;
157              
158             return $self->_remap_explicit_attribute_map()
159             if $self->_has_explicit_attribute_map;
160              
161             my $x = 0;
162             my %map;
163              
164             for my $s ( $self->select()->select_clause_elements() ) {
165             if ( $s->can('table') ) {
166             my $class = Fey::Meta::Class::Table->ClassForTable( $s->table() );
167              
168             $map{$class}{$x}
169             = $s->can('alias_name') ? $s->alias_name() : $s->name();
170             }
171              
172             $x++;
173             }
174              
175             return \%map;
176             }
177              
178             sub _remap_explicit_attribute_map {
179             my $self = shift;
180              
181             my $explicit_map = $self->attribute_map();
182              
183             my %map;
184             for my $pos ( keys %{$explicit_map} ) {
185             $map{ $explicit_map->{$pos}{class} }{$pos}
186             = $explicit_map->{$pos}{attribute};
187             }
188              
189             return \%map;
190             }
191              
192             sub reset {
193             my $self = shift;
194              
195             $self->_finish_handle();
196             $self->_clear_sth();
197             $self->_reset_index();
198              
199             return;
200             }
201              
202             sub DEMOLISH {
203             my $self = shift;
204              
205             $self->_finish_handle();
206             }
207              
208             sub _finish_handle {
209             my $self = shift;
210              
211             # We really don't care about cleanly finishing statement handles
212             # in this case, and this code just doesn't work so well in that
213             # case anyway.
214             return if in_global_destruction();
215              
216             return unless $self->_has_sth();
217              
218             $self->_sth()->finish() if $self->_sth()->{Active};
219             }
220              
221             __PACKAGE__->meta()->make_immutable();
222              
223             1;
224              
225             # ABSTRACT: Wraps a DBI statement handle to construct objects from the results
226              
227             __END__
228              
229             =pod
230              
231             =head1 NAME
232              
233             Fey::Object::Iterator::FromSelect - Wraps a DBI statement handle to construct objects from the results
234              
235             =head1 VERSION
236              
237             version 0.46
238              
239             =head1 SYNOPSIS
240              
241             use Fey::Object::Iterator::FromSelect;
242              
243             my $iter = Fey::Object::Iterator::FromSelect->new(
244             classes => 'MyApp::User',
245             select => $select,
246             dbh => $dbh,
247             bind_params => \@bind,
248             );
249              
250             print $iter->index(); # 0
251              
252             while ( my $user = $iter->next() ) {
253             print $iter->index(); # 1, 2, 3, ...
254             print $user->username();
255             }
256              
257             $iter->reset();
258              
259             =head1 DESCRIPTION
260              
261             This class implements an iterator on top of a DBI statement
262             handle. Each call to C<next()> returns one or more objects based on
263             the data returned by the statement handle.
264              
265             =head1 METHODS
266              
267             This class provides the following methods:
268              
269             =head2 Fey::Object::Iterator::FromSelect->new(...)
270              
271             This method constructs a new iterator. It accepts the following
272             parameters:
273              
274             =over 4
275              
276             =item * classes
277              
278             This can be a single class name, or an array reference of class
279             names. These should be classes associated with the tables from which
280             data is being C<SELECT>ed. The iterator will return an object of each
281             class in order when C<< $iterator->next() >> is called.
282              
283             This can be any class, not just a class which uses
284             L<Fey::ORM::Table>. However, the iterator methods below which return hashes
285             only work when all the classes have a C<Table()> method.
286              
287             =item * dbh
288              
289             A connected DBI handle
290              
291             =item * select
292              
293             This can be any object which does the L<Fey::Role::SQL::ReturnsData>
294             role. Usually this will be a L<Fey::SQL::Select> object. This object should be
295             a query which returns the data that this iterator will iterate over.
296              
297             =item * bind_params
298              
299             This should be an array reference of one or more bind params for the
300             C<SELECT>.
301              
302             This is an optional parameter. If it not passed, then the bind
303             parameters will be obtained by calling the C<bind_params()> method on
304             the "select" parameter.
305              
306             =item * attribute_map
307              
308             This lets you explicitly map an element of the C<SELECT> clause to a
309             specific class's attribute.
310              
311             See L<ATTRIBUTE MAPPING> for more details.
312              
313             =back
314              
315             =head2 $iterator->index()
316              
317             This returns the current index value of the iterator. When the object
318             is first constructed, this index is 0, and it is incremented once for
319             each row fetched by calling C<< $iterator->next() >>.
320              
321             =head2 $iterator->next()
322              
323             This returns the next set of objects, based on data retrieved by the
324             query. In list context this returns all the objects. In scalar context
325             it returns the first object.
326              
327             It is possible that one or more of the objects it returns will be
328             undefined, though this should really only happen with an outer
329             join. The statement handle will be executed the first time this method
330             is called.
331              
332             If the statement handle is exhausted, this method returns false.
333              
334             =head2 $iterator->remaining()
335              
336             This returns all of the I<remaining> sets of objects. If the iterator
337             is for a single class, it returns a list of objects of that class. If
338             it is for multiple objects, it returns a list of array references.
339              
340             =head2 $iterator->all()
341              
342             This returns all of the sets of objects. If necessary, it will call
343             C<< $iterator->reset() >> first. If the iterator is for a single
344             class, it returns a list of objects of that class. If it is for
345             multiple objects, it returns a list of array references.
346              
347             =head2 $iterator->next_as_hash()
348              
349             Returns the next set of objects as a hash. The keys are the names of
350             the object's associated table.
351              
352             If the statement handle is exhausted, this method returns false.
353              
354             This method will throw an exception unless all of the iterator's classes have
355             a C<Table()> method.
356              
357             =head2 $iterator->remaining_as_hashes()
358              
359             This returns all of the I<remaining> sets of objects as a list of hash
360             references. Each hash ref is keyed on the table name of the associated
361             object's class.
362              
363             This method will throw an exception unless all of the iterator's classes have
364             a C<Table()> method.
365              
366             =head2 $iterator->all_as_hashes()
367              
368             This returns all of the sets of objects as a list of hash
369             references. If necessary, it will call C<< $iterator->reset() >>
370             first. Each hash ref is keyed on the table name of the associated
371             object's class.
372              
373             This method will throw an exception unless all of the iterator's classes have
374             a C<Table()> method.
375              
376             =head2 $iterator->reset()
377              
378             Resets the iterator so that the next call to C<< $iterator->next() >>
379             returns the first objects. Internally this means that the statement
380             handle will be executed again. It's possible that data will have
381             changed in the DBMS since then, meaning that the iterator will return
382             different objects after a reset.
383              
384             =head2 $iterator->raw_row()
385              
386             Returns an array reference containing the I<raw> data returned by the query on
387             the most recent call to C<< $iterator->next() >>. Once the iterator is
388             exhausted, this method returns C<undef>.
389              
390             =head2 $iterator->DEMOLISH()
391              
392             This method will call C<< $sth->finish() >> on its C<DBI> statement
393             handle if necessary.
394              
395             =head1 ATTRIBUTE MAPPING
396              
397             This class tries to automatically map each element of the C<SELECT>
398             clause to a class's attribute. You can also provide your own explicit
399             mappings as needed.
400              
401             In the absence of an explicit mapping, it checks to see if the element
402             has a C<table()> method. If it does, it calls C<<
403             Fey::Meta::Class::Table->ClassForTable >> in order to get a class name
404             for the table. Then it uses the value of C<name()> (for column
405             objects) or C<alias_name()> (for column alias objects) as the name of
406             the attribute to be passed to the class's constructor.
407              
408             If the class is not listed in the iterator's "classes" attribute, then
409             it will simply be ignored.
410              
411             If the element does not have a C<table()> method or an explicit
412             mapping, it is ignored.
413              
414             This default works for most queries, where you're just selecting some
415             or all of the columns from one or more tables.
416              
417             In more exotic cases, you can specify an explicit mapping. The mapping
418             maps a C<SELECT> clause element to a specify class's attribute. The
419             map would look something like this:
420              
421             Fey::Object::Iterator::FromSelect->new
422             ( classes => [ 'User', 'Message' ],
423             dbh => $dbh,
424             select => $select,
425             attribute_map => { 0 => { class => 'User',
426             attribute => 'user_id',
427             },
428             1 => { class => 'User',
429             attribute => 'username',
430             },
431             3 => { class => 'Message',
432             attribute => 'message_id',
433             },
434             },
435             );
436              
437             The keys in the mapping are positions in the list of C<SELECT> clause
438             elements. The numbers start from zero (0) just like a Perl array. The values
439             are themselves a hash reference specifying a "class" and "attribute" of that
440             class.
441              
442             This explicit mapping is useful for more "exotic" queries. For example:
443              
444             SELECT Message.user_id, COUNT(message_id) AS message_count
445             FROM Message
446             ORDER BY message_count DESC
447             GROUP BY user_id
448             LIMIT 10
449              
450             This query selects to the top 10 most frequent message posters from a
451             C<Message> table. Assuming our C<User> class has a C<message_count>
452             attribute, we'd like to create a list of C<User> objects from this
453             query.
454              
455             Fey::Object::Iterator::FromSelect->new
456             ( classes => [ 'User', 'Message' ],
457             dbh => $dbh,
458             select => $select,
459             attribute_map => { 0 => { class => 'User',
460             attribute => 'user_id',
461             },
462             1 => { class => 'User',
463             attribute => 'message_count',
464             },
465             );
466              
467             Explicit mappings to classes not listed in the "classes" attribute
468             cause an error at object construction time.
469              
470             =head1 ROLES
471              
472             This class does the L<Fey::ORM::Role::Iterator> role.
473              
474             =head1 AUTHOR
475              
476             Dave Rolsky <autarch@urth.org>
477              
478             =head1 COPYRIGHT AND LICENSE
479              
480             This software is copyright (c) 2011 by Dave Rolsky.
481              
482             This is free software; you can redistribute it and/or modify it under
483             the same terms as the Perl 5 programming language system itself.
484              
485             =cut