File Coverage

blib/lib/Elastic/Model/Role/Doc.pm
Criterion Covered Total %
statement 33 91 36.2
branch 0 26 0.0
condition 0 2 0.0
subroutine 11 22 50.0
pod 8 9 88.8
total 52 150 34.6


line stmt bran cond sub pod time code
1             package Elastic::Model::Role::Doc;
2             $Elastic::Model::Role::Doc::VERSION = '0.51';
3 23     23   16837 use Moose::Role;
  23         53  
  23         224  
4              
5 23     23   126604 use Elastic::Model::Trait::Exclude;
  23         83  
  23         162  
6 23     23   84077 use MooseX::Types::Moose qw(Maybe Bool HashRef);
  23         58  
  23         186  
7 23     23   111781 use Elastic::Model::Types qw(Timestamp UID);
  23         57  
  23         227  
8 23     23   136627 use Scalar::Util qw(refaddr);
  23         56  
  23         1660  
9 23     23   130 use Try::Tiny;
  23         43  
  23         1457  
10 23     23   18265 use Time::HiRes();
  23         36539  
  23         680  
11 23     23   161 use Carp;
  23         44  
  23         1651  
12 23     23   145 use namespace::autoclean;
  23         48  
  23         260  
13              
14             #===================================
15             has 'uid' => (
16             #===================================
17                 isa => UID,
18                 is => 'ro',
19                 required => 1,
20                 writer => '_set_uid',
21                 traits => ['Elastic::Model::Trait::Exclude'],
22                 exclude => 1,
23                 handles => [ 'id', 'type' ]
24             );
25              
26             #===================================
27             has 'timestamp' => (
28             #===================================
29                 traits => ['Elastic::Model::Trait::Field'],
30                 isa => Timestamp,
31                 is => 'rw',
32                 exclude => 0
33             );
34              
35             #===================================
36             has '_can_inflate' => (
37             #===================================
38                 isa => Bool,
39                 is => 'rw',
40                 default => 0,
41                 traits => ['Elastic::Model::Trait::Exclude'],
42                 exclude => 1,
43             );
44              
45             #===================================
46             has '_source' => (
47             #===================================
48                 isa => Maybe [HashRef],
49                 is => 'ro',
50                 traits => ['Elastic::Model::Trait::Exclude'],
51                 lazy => 1,
52                 exclude => 1,
53                 builder => '_get_source',
54                 writer => '_set_source',
55             );
56              
57 23     23   4568 no Moose::Role;
  23         47  
  23         496  
58              
59             #===================================
60             sub has_changed {
61             #===================================
62 0     0 1       my $self = shift;
63 0               my $old = $self->old_values;
64 0 0             return '' unless keys %$old;
65                 return @_
66 0 0                 ? exists $old->{ $_[0] }
67                     : 1;
68             }
69              
70             #===================================
71             sub old_values {
72             #===================================
73 0     0 1       my $self = shift;
74 0 0             return {} if $self->_can_inflate;
75 0               my $current = $self->model->deflate_object($self);
76 0 0             my %original = %{ $self->uid->from_store ? $self->_source : {} };
  0            
77 0               my $json = $self->model->json;
78 0               my %old;
79              
80 0               my ( $o, $c, $o_str, $c_str );
81 0               my $meta = Class::MOP::class_of($self);
82 0               my $model = $self->model;
83 0               for my $key ( keys %$current ) {
84 0 0                 unless ( exists $original{$key} ) {
85 0                       $old{$key} = undef;
86 0                       next;
87                     }
88 23     23   9724         no warnings 'uninitialized';
  23         50  
  23         16562  
89 0                   ( $o, $c ) = ( delete $original{$key}, $current->{$key} );
90 0 0                 ( $o_str, $c_str )
91 0                       = map { ref $_ ? $json->encode($_) : $_ } ( $o, $c );
92              
93 0 0                 if ( $o_str ne $c_str ) {
94 0                       $old{$key} = $meta->inflator_for( $model, $key )->($o);
95                     }
96                 }
97                 $old{$_} = $meta->inflator_for( $model, $_ )->( $original{$_} )
98 0                   for keys %original;
99 0               return \%old;
100             }
101              
102             #===================================
103             sub old_value {
104             #===================================
105 0     0 0       die('old_value($attr) has been removed. Use old_values->{$attr} instead');
106             }
107              
108             #===================================
109             sub _get_source {
110             #===================================
111 0     0         my $self = shift;
112 0               $self->model->get_doc_source(
113                     uid => $self->uid,
114                     ignore => 404,
115                     @_
116                 );
117             }
118              
119             #===================================
120             sub _inflate_doc {
121             #===================================
122 0     0         my $self = $_[0];
123 0 0             my $source = $self->_source
124                     or return bless( $self, 'Elastic::Model::Deleted' )->croak;
125              
126 0               $self->_can_inflate(0);
127                 eval {
128 0                   $self->model->inflate_object( $self, $source );
129 0                   1;
130 0 0             } or do {
131 0                   my $error = $@;
132 0                   $self->_can_inflate(1);
133 0                   die $error;
134                 };
135             }
136              
137             #===================================
138             sub touch {
139             #===================================
140 0     0 1       my $self = shift;
141 0               $self->timestamp( int( Time::HiRes::time * 1000 + 0.5 ) / 1000 );
142 0               $self;
143             }
144              
145             #===================================
146             sub save {
147             #===================================
148 0     0 1       my $self = shift;
149              
150 0 0             unless ( $self->_can_inflate ) {
151 0                   $self->touch;
152 0                   $self->model->save_doc( doc => $self, @_ );
153                 }
154 0               $self;
155             }
156              
157             #===================================
158 0     0 1   sub overwrite { shift->save( @_, version => 0 ) }
159             #===================================
160              
161             #===================================
162             sub delete {
163             #===================================
164 0     0 1       my $self = shift;
165 0 0             $self->model->delete_doc( uid => $self->uid, @_ )
166                     or return;
167 0               bless $self, 'Elastic::Model::Deleted';
168             }
169              
170             #===================================
171             sub has_been_deleted {
172             #===================================
173 0     0 1       my $self = shift;
174 0               my $uid = $self->uid;
175 0 0             $uid->from_store or return 0;
176 0               !$self->model->doc_exists( uid => $uid, @_ );
177             }
178              
179             #===================================
180             sub terms_indexed_for_field {
181             #===================================
182 0     0 1       my $self = shift;
183 0 0             my $field = shift or croak "Missing required param (fieldname)";
184 0   0           my $size = shift || 20;
185              
186 0               my $uid = $self->uid;
187 0               return $self->model->view->domain( $uid->index )->type( $uid->type )
188                     ->filterb( _id => $uid->id )
189                     ->facets( field => { terms => { field => $field, size => 20 } } )
190                     ->size(0)->search->facet('field');
191             }
192              
193             1;
194              
195             =pod
196            
197             =encoding UTF-8
198            
199             =head1 NAME
200            
201             Elastic::Model::Role::Doc - The role applied to your Doc classes
202            
203             =head1 VERSION
204            
205             version 0.51
206            
207             =head1 SYNOPSIS
208            
209             =head2 Creating a doc
210            
211             $doc = $domain->new_doc(
212             user => {
213             id => 123, # auto-generated if not specified
214             email => 'clint@domain.com',
215             name => 'Clint'
216             }
217             );
218            
219             $doc->save;
220             $uid = $doc->uid;
221            
222             =head2 Retrieving a doc
223            
224             $doc = $domain->get( user => 123 );
225             $doc = $model->get_doc( uid => $uid );
226            
227             =head2 Updating a doc
228            
229             $doc->name('John');
230            
231             print $doc->has_changed(); # 1
232             print $doc->has_changed('name'); # 1
233             print $doc->has_changed('email'); # 0
234             dump $doc->old_values; # { name => 'Clint' }
235            
236             $doc->save;
237             print $doc->has_changed(); # 0
238            
239             =head2 Deleting a doc
240            
241             $doc->delete;
242             print $doc->has_been_deleted # 1
243            
244             =head1 DESCRIPTION
245            
246             L<Elastic::Model::Role::Doc> is applied to your "doc" classes (ie those classes
247             that you want to be stored in Elasticsearch), when you include this line:
248            
249             use Elastic::Doc;
250            
251             This document explains the changes that are made to your class by applying the
252             L<Elastic::Model::Role::Doc> role. Also see L<Elastic::Doc>.
253            
254             =head1 ATTRIBUTES
255            
256             The following attributes are added to your class:
257            
258             =head2 uid
259            
260             The L<uid|Elastic::Model::UID> is the unique identifier for your doc in
261             Elasticsearch. It contains an L<index|Elastic::Model::UID/"index">,
262             a L<type|Elastic::Model::UID/"type">, an L<id|Elastic::Model::UID/"id"> and
263             possibly a L<routing|Elastic::Model::UID/"routing">. This is what is required
264             to identify your document uniquely in Elasticsearch.
265            
266             The UID is created when you create your document, eg:
267            
268             $doc = $domain->new_doc(
269             user => {
270             id => 123,
271             other => 'foobar'
272             }
273             );
274            
275             =over
276            
277             =item *
278            
279             C<index> : initially comes from the C<< $domain->name >> - this is changed
280             to the actual domain name when you save your doc.
281            
282             =item *
283            
284             C<type> : comes from the first parameter passed to
285             L<new_doc()|Elastic::Model::Domain/"new_doc()"> (C<user> in this case).
286            
287             =item *
288            
289             C<id> : is optional - if you don't provide it, then it will be
290             auto-generated when you save it to Elasticsearch.
291            
292             =back
293            
294             B<Note:> the C<namespace_name/type/ID> of a document must be unique.
295             Elasticsearch can enforce uniqueness for a single index, but when your
296             L<namespace|Elastic::Model::Namespace> contains multiple indices, it is up
297             to you to ensure uniqueness. Either leave the ID blank, in which case
298             Elasticsearch will generate a unique ID, or ensure that the way you
299             generate IDs will not cause a collision.
300            
301             =head2 type / id
302            
303             $type = $doc->type;
304             $id = $doc->id;
305            
306             C<type> and C<id> are provided as convenience, read-only accessors which
307             call the equivalent accessor on L</uid>.
308            
309             You can defined your own C<id()> and C<type()> methods, in which case they
310             won't be imported, or you can import them under a different name, eg:
311            
312             package MyApp::User;
313             use Elastic::Doc;
314            
315             with 'Elastic::Model::Role::Doc' => {
316             -alias => {
317             id => 'doc_id',
318             type => 'doc_type',
319             }
320             };
321            
322             =head2 timestamp
323            
324             $timestamp = $doc->timestamp($timestamp);
325            
326             This stores the last-modified time (in epoch seconds with milli-seconds), which
327             is set automatically when your doc is saved. The C<timestamp> is indexed
328             and can be used in queries.
329            
330             =head2 Private attributes
331            
332             These private attributes are also added to your class, and are documented
333             here so that you don't override them without knowing what you are doing:
334            
335             =head3 _can_inflate
336            
337             A boolean indicating whether the object has had its attributes values
338             inflated already or not.
339            
340             =head3 _source
341            
342             The raw uninflated source value as loaded from Elasticsearch.
343            
344             =head1 METHODS
345            
346             =head2 save()
347            
348             $doc->save( %args );
349            
350             Saves the C<$doc> to Elasticsearch. If this is a new doc, and a doc with the
351             same type and ID already exists in the same index, then Elasticsearch
352             will throw an exception.
353            
354             Also see L<Elastic::Model::Bulk> for bulk indexing of multiple docs.
355            
356             If the doc was previously loaded from Elasticsearch, then that doc will be
357             updated. However, because Elasticsearch uses
358             L<optimistic locking|http://en.wikipedia.org/wiki/Optimistic_locking>
359             (ie the doc version number is incremented on every change), it is possible that
360             another process has already updated the C<$doc> while the current process has
361             been working, in which case it will throw a conflict error.
362            
363             For instance:
364            
365             ONE TWO
366             --------------------------------------------------
367             get doc 1-v1
368             get doc 1-v1
369             save doc 1-v2
370             save doc1-v2
371             -> # conflict error
372            
373             =head3 on_conflict
374            
375             If you don't care, and you just want to overwrite what is stored in Elasticsearch
376             with the current values, then use L</overwrite()> instead of L</save()>. If you
377             DO care, then you can handle this situation gracefully, using the
378             C<on_conflict> parameter:
379            
380             $doc->save(
381             on_conflict => sub {
382             my ($original_doc,$new_doc) = @_;
383             # resolve conflict
384            
385             }
386             );
387            
388             See L</has_been_deleted()> for a fuller example of an L</on_conflict> callback.
389            
390             The doc will only be saved if it has changed. If you want to force saving
391             on a doc that hasn't changed, then you can do:
392            
393             $doc->touch->save;
394            
395             =head3 on_unique
396            
397             If you have any L<unique attributes|Elastic::Manual::Attributes/unique_key> then
398             you can catch unique-key conflicts with the C<on_unique> handler.
399            
400             $doc->save(
401             on_unique => sub {
402             my ($doc,$conflicts) = @_;
403             # do something
404             }
405             )
406            
407             The C<$conflicts> hashref will contain a hashref whose keys are the name
408             of the L<unique_keys|Elastic::Manual::Attributes/unique_key> that have
409             conflicts, and whose values are the values of those keys which already exist,
410             and so cannot be overwritten.
411            
412             See L<Elastic::Manual::Attributes::Unique> for more.
413            
414             =head2 overwrite()
415            
416             $doc->overwrite( %args );
417            
418             L</overwrite()> is exactly the same as L</save()> except it will overwrite
419             any previous doc, regardless of whether another process has created or updated
420             a doc with the same UID in the meantime.
421            
422             =head2 delete()
423            
424             $doc->delete;
425            
426             This will delete the current doc. If the doc has already been updated
427             to a new version by another process, it will throw a conflict error. You
428             can override this and delete the document anyway with:
429            
430             $doc->delete( version => 0 );
431            
432             The C<$doc> will be reblessed into the L<Elastic::Model::Deleted> class,
433             and any attempt to access its attributes will throw an error.
434            
435             =head2 has_been_deleted()
436            
437             $bool = $doc->has_been_deleted();
438            
439             As a rule, you shouldn't delete docs that are currently in use elsewhere in
440             your application, otherwise you have to wrap all of your code in C<eval>s
441             to ensure that you're not accessing a stale doc.
442            
443             However, if you do need to delete current docs, then L</has_been_deleted()>
444             checks if the doc exists in Elasticsearch. For instance, you
445             might have an L</on_conflict> handler which looks like this:
446            
447             $doc->save(
448             on_conflict => sub {
449             my ($original, $new) = @_;
450            
451             return $original->overwrite
452             if $new->has_been_deleted;
453            
454             for my $attr ( keys %{ $old->old_values }) {
455             $new->$attr( $old->$attr ):
456             }
457            
458             $new->save
459             }
460             );
461            
462             It is a much better approach to remove docs from the main flow of your
463             application (eg, set a C<status> attribute to C<"deleted">) then physically
464             delete the docs only after some time has passed.
465            
466             =head2 touch()
467            
468             $doc = $doc->touch()
469            
470             Updates the L</"timestamp"> to the current time.
471            
472             =head2 has_changed()
473            
474             Has the value for any attribute changed?
475            
476             $bool = $doc->has_changed;
477            
478             Has the value of attribute C<$attr_name> changed?
479            
480             $bool = $doc->has_changed($attr_name);
481            
482             B<Note:> If you're going to check more than one attribute, rather get
483             all the L</old_values()> and check if the attribute name exists in the
484             returned hash, rather than calling L<has_changed()> multiple times.
485            
486             =head2 old_values()
487            
488             \%old_vals = $doc->old_values();
489            
490             Returns a hashref containing the original values of any attributes that have
491             been changed. If an attribute wasn't set originally, but is now, it will
492             be included in the hash with the value C<undef>.
493            
494             =head2 terms_indexed_for_field()
495            
496             $terms = $doc->terms_indexed_for_field( $fieldname, $size );
497            
498             This method is useful for debugging queries and analysis - it returns
499             the actual terms (ie after analysis) that have been indexed for
500             field C<$fieldname> in the current doc. C<$size> defaults to 20.
501            
502             =head2 Private methods
503            
504             These private methods are also added to your class, and are documented
505             here so that you don't override them without knowing what you are doing:
506            
507             =head3 _inflate_doc
508            
509             Inflates the attribute values from the hashref stored in L</"_source">.
510            
511             =head3 _get_source / _set_source
512            
513             The raw doc source from Elasticsearch.
514            
515             =head1 AUTHOR
516            
517             Clinton Gormley <drtech@cpan.org>
518            
519             =head1 COPYRIGHT AND LICENSE
520            
521             This software is copyright (c) 2015 by Clinton Gormley.
522            
523             This is free software; you can redistribute it and/or modify it under
524             the same terms as the Perl 5 programming language system itself.
525            
526             =cut
527              
528             __END__
529            
530             # ABSTRACT: The role applied to your Doc classes
531            
532