File Coverage

blib/lib/Mongoose/Join.pm
Criterion Covered Total %
statement 18 117 15.3
branch 0 32 0.0
condition 0 6 0.0
subroutine 6 24 25.0
pod 12 12 100.0
total 36 191 18.8


line stmt bran cond sub pod time code
1             package Mongoose::Join;
2             $Mongoose::Join::VERSION = '2.01';
3 1     1   7 use Moose;
  1         2  
  1         8  
4 1     1   5730 use Moose::Util::TypeConstraints;
  1         2  
  1         7  
5 1     1   1809 use Moose::Meta::TypeConstraint::Parameterizable;
  1         2  
  1         37  
6 1     1   5 use Moose::Meta::TypeConstraint::Registry;
  1         2  
  1         22  
7 1     1   4 use Tie::IxHash;
  1         1  
  1         220  
8              
9             my $REGISTRY = Moose::Meta::TypeConstraint::Registry->new;
10             $REGISTRY->add_type_constraint(
11             Moose::Meta::TypeConstraint::Parameterizable->new(
12             name => 'Mongoose::Join',
13             package_defined_in => __PACKAGE__,
14             parent => find_type_constraint('Item'),
15             constraint => sub { die 'constrained' },
16             constraint_generator => sub {
17             my $type_parameter = shift;
18             sub { return {} };
19             }
20             )
21             );
22              
23             Moose::Util::TypeConstraints::add_parameterizable_type( $REGISTRY->get_type_constraint('Mongoose::Join') );
24              
25             has 'class' => ( is => 'rw', isa => 'Str' );
26             has 'field' => ( is => 'rw', isa => 'Str' );
27             has 'with_class' => ( is => 'rw', isa => 'Str' );
28             has '_with_collection_name' => ( is => 'rw', isa => 'Str' );
29             has 'parent' => ( is => 'rw', isa => 'BSON::OID' );
30              
31             # once the object is expanded, it has children too
32             has 'children' => ( is => 'rw', isa => 'ArrayRef', default => sub{[]} );
33              
34             # before being saved, objects are stored in this buffer
35             has 'buffer' => ( is => 'rw', isa => 'HashRef', default => sub { {} } );
36              
37             # deleting happens at a later moment, meanwhile delete candidates are here
38             has 'delete_buffer' => ( is => 'rw', isa => 'HashRef', default => sub { {} } );
39              
40 1     1   6 use Scalar::Util qw/refaddr/;
  1         10  
  1         1208  
41              
42             sub add {
43 0     0 1   my ( $self, @objs ) = @_;
44 0           for my $obj (@objs) {
45 0           $self->buffer->{ refaddr $obj } = $obj;
46             }
47 0           @objs;
48             }
49              
50             sub remove {
51 0     0 1   my ( $self, @objs ) = @_;
52              
53             # if the collection is live, remove from memory
54 0 0         if( my $buffer = $self->buffer ) {
55 0           for my $obj (@objs) {
56 0 0         next unless defined $obj;
57 0           delete $buffer->{ refaddr $obj };
58             }
59             }
60              
61             # children get cleaned too
62 0 0         if( defined ( my $children = $self->children ) ) {
63 0           for my $obj (@objs) {
64 0           my $id = $obj->_id;
65 0 0         next unless defined $id;
66             $self->children([
67 0           grep { $_->id ne $id } _build_rel( @{$children} )
  0            
  0            
68             ]);
69             }
70             }
71              
72             # save action for later (when save is called)
73 0 0         my $delete_buffer = defined $self->delete_buffer
74             ? $self->delete_buffer
75             : $self->delete_buffer({});
76 0           for my $obj (@objs) {
77 0           $delete_buffer->{ refaddr $obj } = $obj;
78             }
79             }
80              
81             sub collection {
82 0     0 1   my $self = shift;
83 0 0         defined $self->with_class
84             and return $self->with_class->collection;
85             }
86              
87             sub with_collection_name {
88 0     0 1   my $self = shift;
89 0 0         defined $self->_with_collection_name
90             and return $self->_with_collection_name;
91              
92 0           $self->_with_collection_name( Mongoose->class_config($self->with_class)->{collection_name} );
93             }
94              
95       0     sub _insert { #TODO insert and commit
96             }
97              
98             sub _save {
99 0     0     my ( $self, $parent, @scope ) = @_;
100              
101 0           my $children = delete $self->{children};
102 0 0         if( ref $children eq 'Mongoose::Join' ) {
103 0           $children = $children->children;
104             }
105 0 0         my @objs = _build_rel( @{$children ||[]} );
  0            
106              
107 0           my $collection_name = $self->with_collection_name;
108              
109             # load buffers
110 0           my $buffer = delete $self->{buffer};
111 0           my $delete_buffer = delete $self->{delete_buffer};
112              
113             # save buffered children
114 0           for ( keys %{ $buffer } ) {
  0            
115 0           my $obj = delete $buffer->{$_};
116 0 0         next if exists $delete_buffer->{ refaddr $obj };
117 0           $obj->save( @scope );
118 0           push @objs, _build_rel({ '$ref' => $collection_name, '$id' => $obj->_id });
119             }
120              
121             # adjust
122 0           $self->buffer( $buffer ); # restore the list
123 0           $self->delete_buffer({});
124              
125             # make sure unique children is saved
126 0           my %unique = map { $_->id => $_ } @objs;
  0            
127 0           @objs = values %unique;
128 0           $self->children( \@objs );
129 0           return @objs;
130             }
131              
132             sub _children_refs {
133 0     0     my ($self)=@_;
134 0           my @found;
135             $self->find->each( sub{
136 0     0     push @found, _build_rel({ '$ref' => $_[0]->_collection_name, '$id' => $_[0]->{_id} });
137 0           });
138 0           return @found;
139             }
140              
141             sub find {
142 0     0 1   my ( $self, $opts, @scope ) = @_;
143 0           my $class = $self->with_class;
144 0   0       $opts ||= {};
145 0           my $children = $self->children;
146 0 0         if( ref $children eq 'Mongoose::Join' ) {
147 0           $children = $children->children;
148             }
149 0 0         my @children = map { $_->id } _build_rel( @{$children ||[]} );
  0            
  0            
150              
151 0           $opts->{_id} = { '$in' => \@children };
152 0           return $class->find( $opts, @scope );
153             }
154              
155             sub count {
156 0     0 1   my $self = shift;
157 0           $self->_call_rel_method( count => @_ );
158             }
159              
160             sub find_one {
161 0     0 1   my $self = shift;
162 0           $self->_call_rel_method( find_one => @_ );
163             }
164              
165             sub _call_rel_method {
166 0     0     my ( $self, $method, $opts, @scope ) = @_;
167 0           my $class = $self->with_class;
168              
169 0   0       $opts ||= {};
170 0 0         $opts->{_id} = { '$in' => [ map { $_->id } _build_rel( @{$self->children ||[]} ) ] };
  0            
  0            
171              
172 0           return $class->$method( $opts, @scope );
173             }
174              
175 0     0 1   sub first { shift->find_one }
176              
177             sub query {
178 0     0 1   my ( $self, $opts, $attrs, @scope ) = @_;
179 0           my $class = $self->with_class;
180 0   0       $opts ||= {};
181 0 0         my @children = map { $_->id } _build_rel( @{$self->children ||[]} );
  0            
  0            
182 0           $opts->{_id} = { '$in' => \@children };
183 0           return $class->query( $opts, $attrs, @scope );
184             }
185              
186             sub all {
187 0     0 1   my $self = shift;
188 0           return $self->find(@_)->all;
189             }
190              
191             sub hash_on {
192 0     0 1   my $self = shift;
193 0           my $key = shift;
194 0           my %hash;
195             map {
196 0 0         $hash{ $_->{$key} } = $_ unless exists $hash{ $_->{$key} };
  0            
197             } $self->find(@_)->all;
198 0           return %hash;
199             }
200              
201             sub hash_array {
202 0     0 1   my $self = shift;
203 0           my $key = shift;
204 0           my %hash;
205             map {
206 0           push @{ $hash{ $_->{$key} } }, $_;
  0            
  0            
207             } $self->find(@_)->all;
208 0           return %hash;
209             }
210              
211             # make sure all refs what's expected on the MongoDB driver in use
212             sub _build_rel {
213 0     0     map { ref $_ eq 'BSON::DBRef'
214             ? $_
215 0 0         : BSON::DBRef->new( ref => $_->{'$ref'}, id => $_->{'$id'} )
216             } @_;
217             }
218              
219             =head1 NAME
220              
221             Mongoose::Join - simple class relationship resolver
222              
223             =head1 SYNOPSIS
224              
225             package Author;
226             use Moose;
227             with 'Mongoose::Document';
228             has 'articles' => (
229             is => 'rw',
230             isa => 'Mongoose::Join[Article]',
231             default => sub { Mongoose::Join->new( with_class => 'Article' ) }
232             );
233              
234             =head1 DESCRIPTION
235              
236             This module can be used to establish relationships
237             between two C<Mongoose::Document> classes. It should not
238             be used with C<Mongoose::EmbeddedDocument> classes.
239              
240             All object relationships are stored as reference C<$id> arrays
241             into the parent object. This translates into a slight performance hit
242             when loading the parent class, but not as much as loading all
243             objects at one as when using an C<ArrayRef>.
244              
245             B<Attention>: the relationship attribute needs to be initialized to
246             an instance of C<Mongoose::Join> before it can be used.
247              
248             =head2 Mongoose::Class
249              
250             Take a look at L<Mongoose::Class>, it has nice syntatic sugar
251             that does most of the initialization behind the scenes for you.
252              
253             =head1 METHODS
254              
255             =head2 add
256              
257             Add (join) a Mongoose::Document object for later saving.
258              
259             Saving the parent Mongoose::Document will save both.
260              
261             my $author = Author->new;
262             $author->articles->add( Article->new );
263             $author->save; # saves all objects
264              
265             =head2 remove
266              
267             Delete from the relationship list.
268              
269             my $author = Author->find_one;
270             my $first_article = $author->articles->find_one;
271             $author->articles->remove( $first_article );
272              
273             =head2 find
274              
275             Run a MongoDB C<find> on the joint collection.
276              
277             # searches for articles belonging to this collection
278             my $cursor = $author->articles->find({ title=>'foo article' });
279             while( my $article = $cursor->next ) {
280             ...
281             }
282              
283             Returns a L<Mongoose::Cursor>.
284              
285             =head2 count
286              
287             Count relations.
288              
289             =head2 find_one
290              
291             Just like find, but returns the first row found.
292              
293             =head2 first
294              
295             Alias to C<find_one>
296              
297             $first_cd = $artist->cds->first;
298              
299             =head2 all
300              
301             Same as C<find>, but returns an ARRAY with all the results, instead
302             of a cursor.
303              
304             my @cds = $artist->cds->all;
305              
306             =head2 hash_on
307              
308             Same as C<all>, but returns a HASH instead of an ARRAY.
309             The hash will be indexed by the key name sent as the first parameter.
310             The hash value contains exactly one object. In case duplicate rows
311             with the same key value are found, the resulting hash will contain
312             the first one found.
313              
314             # ie. returns $cds{'111888888292adf0000003'} = <CD Object>;
315             my %cds = $artist->cds->hash_on( '_id' => { artist=>'Joe' });
316              
317             # ie. returns $joe_cds{'Title1'} = <CD Object>;
318             my %joe_cds = $artist->cds->hash_on( 'title' => { artist=>qr/Joe/ });
319              
320             =head2 hash_array
321              
322             Similar to C<hash_on>, but returns a hash with ALL rows found, grouped
323             by the key.
324              
325             # ie. returns $cds{'Title1'} = [ <CD1 Object>, <CD2 Object>, ... ];
326             my %cds = $artist->cds->hash_array( 'title' => { artist=>'Joe' });
327              
328             Hash values are ARRAYREFs with 1 or more rows.
329              
330             =head2 query
331              
332             Run a MongoDB C<query> on the joint collection.
333              
334             =head2 collection
335              
336             Returns the L<MongoDB::Collection> for the joint collection.
337              
338             =head2 with_collection_name
339              
340             Return the collection name for the joint Mongoose::Document.
341              
342             =cut
343              
344             __PACKAGE__->meta->make_immutable();