Revert r482[45], by implementing a better version of r4760
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / ResultClass / HashRefInflator.pm
1 package DBIx::Class::ResultClass::HashRefInflator;
2
3 use strict;
4 use warnings;
5
6 =head1 NAME
7
8 DBIx::Class::ResultClass::HashRefInflator
9
10 =head1 SYNOPSIS
11
12  use DBIx::Class::ResultClass::HashRefInflator;
13
14  my $rs = $schema->resultset('CD');
15
16  $rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
17     or
18  $rs->result_class(DBIx::Class::ResultClass::HashRefInflator->new (%args));
19
20  while (my $hashref = $rs->next) {
21     ...
22  }
23
24 =head1 DESCRIPTION
25
26 DBIx::Class is faster than older ORMs like Class::DBI but it still isn't 
27 designed primarily for speed. Sometimes you need to quickly retrieve the data
28 from a massive resultset, while skipping the creation of fancy row objects.
29 Specifying this class as a C<result_class> for a resultset will change C<< $rs->next >>
30 to return a plain data hash-ref (or a list of such hash-refs if C<< $rs->all >> is used).
31
32 There are two ways of using this class:
33
34 =over
35
36 =item *
37
38 Supply an instance of DBIx::Class::ResultClass::HashRefInflator to
39 C<< $rs->result_class >>. See L</ARGUMENTS> for a list of valid
40 arguments to new().
41
42 =item *
43
44 Another way is to simply supply the class name as a string to
45 C<< $rs->result_class >>. Equivalent to passing
46 DBIx::Class::ResultClass::HashRefInflator->new().
47
48 =back
49
50 There are two ways of applying this class to a resultset:
51
52 =over
53
54 =item *
55
56 Specify C<< $rs->result_class >> on a specific resultset to affect only that
57 resultset (and any chained off of it); or
58
59 =item *
60
61 Specify C<< __PACKAGE__->result_class >> on your source object to force all
62 uses of that result source to be inflated to hash-refs - this approach is not
63 recommended.
64
65 =back
66
67 =cut
68
69 ##############
70 # NOTE
71 #
72 # Generally people use this to gain as much speed as possible. If a new &mk_hash is
73 # implemented, it should be benchmarked using the maint/benchmark_hashrefinflator.pl
74 # script (in addition to passing all tests of course :). Additional instructions are
75 # provided in the script itself.
76 #
77
78 # This coderef is a simple recursive function
79 # Arguments: ($me, $prefetch) from inflate_result() below
80 my $mk_hash;
81 $mk_hash = sub {
82     if (ref $_[0] eq 'ARRAY') {     # multi relationship
83         return [ map { $mk_hash->(@$_) || () } (@_) ];
84     }
85     else {
86         my $hash = {
87             # the main hash could be an undef if we are processing a skipped-over join
88             $_[0] ? %{$_[0]} : (),
89
90             # the second arg is a hash of arrays for each prefetched relation
91             map
92                 { $_ => $mk_hash->( @{$_[1]->{$_}} ) }
93                 ( $_[1] ? (keys %{$_[1]}) : () )
94         };
95
96         # if there is at least one defined column consider the resultset real
97         # (and not an emtpy has_many rel containing one empty hashref)
98         for (values %$hash) {
99             return $hash if defined $_;
100         }
101
102         return undef;
103     }
104 };
105
106 # This is the inflator
107 my $inflate_hash;
108 $inflate_hash = sub {
109     my ($hri_instance, $schema, $rc, $data) = @_;
110
111     foreach my $column (keys %{$data}) {
112
113         if (ref $data->{$column} eq 'HASH') {
114             $inflate_hash->($hri_instance, $schema, $schema->source ($rc)->related_class ($column), $data->{$column});
115         } 
116         elsif (ref $data->{$column} eq 'ARRAY') {
117             foreach my $rel (@{$data->{$column}}) {
118                 $inflate_hash->($hri_instance, $schema, $schema->source ($rc)->related_class ($column), $rel);
119             }
120         }
121         else {
122             # "null is null is null"
123             next if not defined $data->{$column};
124
125             # cache the inflator coderef
126             unless (exists $hri_instance->{_inflator_cache}{$rc}{$column}) {
127                 $hri_instance->{_inflator_cache}{$rc}{$column} = exists $schema->source ($rc)->_relationships->{$column}
128                     ? undef     # currently no way to inflate a column sharing a name with a rel 
129                     : $rc->column_info($column)->{_inflate_info}{inflate}
130                 ;
131             }
132
133             if ($hri_instance->{_inflator_cache}{$rc}{$column}) {
134                 $data->{$column} = $hri_instance->{_inflator_cache}{$rc}{$column}->($data->{$column});
135             }
136         }
137     }
138 };
139
140
141 =head1 METHODS
142
143 =head2 new
144
145  $class->new( %args );
146  $class->new({ %args });
147
148 Creates a new DBIx::Class::ResultClass::HashRefInflator object. Takes the following
149 arguments:
150
151 =over
152
153 =item inflate_columns
154
155 Sometimes you still want all your data to be inflated to the corresponding 
156 objects according to the rules you defined in your table classes (e.g. you
157 want all dates in the resulting hash to be replaced with the equivalent 
158 DateTime objects). Supplying C<< inflate_columns => 1 >> to the constructor will
159 interrogate the processed columns and apply any inflation methods declared 
160 via L<DBIx::Class::InflateColumn/inflate_column> to the contents of the 
161 resulting hash-ref.
162
163 =back
164
165 =cut
166
167 sub new {
168     my $self = shift;
169     my $args = { (ref $_[0] eq 'HASH') ? %{$_[0]} : @_ };
170     return bless ($args, $self)
171 }
172
173 =head2 inflate_result
174
175 Inflates the result and prefetched data into a hash-ref (invoked by L<DBIx::Class::ResultSet>)
176
177 =cut
178
179
180 sub inflate_result {
181     my ($self, $source, $me, $prefetch) = @_;
182
183     my $hashref = $mk_hash->($me, $prefetch);
184
185     # if $self is an instance and inflate_columns is set
186     if ( (ref $self) and $self->{inflate_columns} ) {
187         $inflate_hash->($self, $source->schema, $source->result_class, $hashref);
188     }
189
190     return $hashref;
191 }
192
193
194 =head1 CAVEATS
195
196 =over
197
198 =item *
199
200 This will not work for relationships that have been prefetched. Consider the
201 following:
202
203  my $artist = $artitsts_rs->search({}, {prefetch => 'cds' })->first;
204
205  my $cds = $artist->cds;
206  $cds->result_class('DBIx::Class::ResultClass::HashRefInflator');
207  my $first = $cds->first; 
208
209 C<$first> will B<not> be a hashref, it will be a normal CD row since 
210 HashRefInflator only affects resultsets at inflation time, and prefetch causes
211 relations to be inflated when the master C<$artist> row is inflated.
212
213 =item *
214
215 When using C<inflate_columns>, the inflation method lookups are cached in the
216 HashRefInflator object for additional speed. If you modify column inflators at run
217 time, make sure to grab a new instance of this class to avoid cached surprises.
218
219 =back
220
221 =cut
222
223 1;