File Coverage

blib/lib/MooseX/Storage/IO/AmazonDynamoDB.pm
Criterion Covered Total %
statement 26 26 100.0
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 35 35 100.0


line stmt bran cond sub pod time code
1             package MooseX::Storage::IO::AmazonDynamoDB;
2              
3 1     1   1280 use strict;
  1         4  
  1         39  
4 1     1   34 use 5.014;
  1         5  
5             our $VERSION = '0.08';
6              
7 1     1   1190 use Data::Dumper;
  1         12489  
  1         118  
8 1     1   13 use JSON::MaybeXS;
  1         4  
  1         85  
9 1     1   1012 use MooseX::Role::Parameterized;
  1         766496  
  1         4  
10 1     1   32852 use MooseX::Storage;
  1         23939  
  1         3  
11 1     1   748 use PawsX::DynamoDB::DocumentClient;
  1         648712  
  1         43  
12 1     1   817 use Types::Standard qw(HasMethods);
  1         76162  
  1         20  
13 1     1   1171 use namespace::autoclean;
  1         4  
  1         15  
14              
15             parameter key_attr => (
16             isa => 'Str',
17             required => 1,
18             );
19              
20             parameter table_name => (
21             isa => 'Maybe[Str]',
22             default => undef,
23             );
24              
25             parameter table_name_method => (
26             isa => 'Str',
27             default => 'dynamo_db_table_name',
28             );
29              
30             parameter document_client_attribute_name => (
31             isa => 'Str',
32             default => 'dynamodb_document_client',
33             );
34              
35             parameter document_client_builder => (
36             isa => 'CodeRef',
37             default => sub { sub { PawsX::DynamoDB::DocumentClient->new() } },
38             );
39              
40             parameter force_type => (
41             isa => 'HashRef',
42             default => sub {{}},
43             );
44              
45             role {
46             my $p = shift;
47              
48             requires 'pack';
49             requires 'unpack';
50              
51             my $table_name_method = $p->table_name_method;
52             my $client_attr = $p->document_client_attribute_name;
53             my $client_builder = $p->document_client_builder;
54             my $force_type = $p->force_type;
55              
56             has $client_attr => (
57             is => 'ro',
58             isa => HasMethods[qw(get put)],
59             lazy => 1,
60             traits => [ 'DoNotSerialize' ],
61             default => $client_builder,
62             );
63              
64             method $table_name_method => sub {
65             my $class = ref $_[0] || $_[0];
66             return $p->table_name if $p->table_name;
67             die "$class: no table name defined!";
68             };
69              
70             method load => sub {
71             my ( $class, $item_key, %args ) = @_;
72             my $client = $args{dynamodb_document_client} || $client_builder->();
73             my $inject = $args{inject} || {};
74             my $table_name = $class->$table_name_method();
75              
76             my $packed = $client->get(
77             TableName => $table_name,
78             Key => {
79             $p->key_attr => $item_key,
80             },
81             ConsistentRead => 1,
82             force_type => $force_type,
83             );
84              
85             return undef unless $packed;
86              
87             # Deserialize JSON values
88             foreach my $key (keys %$packed) {
89             my $value = $packed->{$key};
90             if ($value && $value =~ /^\$json\$v(\d+)\$:(.+)$/) {
91             my ($version, $json) = ($1, $2);
92             state $coder = JSON::MaybeXS->new(
93             utf8 => 1,
94             canonical => 1,
95             allow_nonref => 1,
96             );
97             $packed->{$key} = $coder->decode($json);
98             }
99             }
100              
101             return $class->unpack(
102             $packed,
103             inject => {
104             %$inject,
105             $client_attr => $client,
106             }
107             );
108             };
109              
110             method store => sub {
111             my ( $self ) = @_;
112             my $client = $self->$client_attr;
113             my $table_name = $self->$table_name_method();
114             my $packed = $self->pack;
115             $client->put(
116             TableName => $table_name,
117             Item => $packed,
118             force_type => $force_type,
119             );
120             };
121             };
122              
123             1;
124             __END__
125              
126             =encoding utf-8
127              
128             =head1 NAME
129              
130             MooseX::Storage::IO::AmazonDynamoDB - Store and retrieve Moose objects to AWS's DynamoDB, via MooseX::Storage.
131              
132             =head1 SYNOPSIS
133              
134             First, create a table in DynamoDB. Currently only single-keyed tables are supported.
135              
136             aws dynamodb create-table \
137             --table-name my_docs \
138             --key-schema "AttributeName=doc_id,KeyType=HASH" \
139             --attribute-definitions "AttributeName=doc_id,AttributeType=S" \
140             --provisioned-throughput "ReadCapacityUnits=2,WriteCapacityUnits=2"
141              
142             Then, configure your Moose class via a call to Storage:
143              
144             package MyDoc;
145             use Moose;
146             use MooseX::Storage;
147              
148             with Storage(io => [ 'AmazonDynamoDB' => {
149             table_name => 'my_docs',
150             key_attr => 'doc_id',
151             force_type => { doc_id => 'S' },
152             }]);
153              
154             has 'doc_id' => (is => 'ro', isa => 'Str', required => 1);
155             has 'title' => (is => 'rw', isa => 'Str');
156             has 'body' => (is => 'rw', isa => 'Str');
157             has 'tags' => (is => 'rw', isa => 'ArrayRef');
158             has 'authors' => (is => 'rw', isa => 'HashRef');
159              
160             1;
161              
162             Now you can store/load your class to DyanmoDB:
163              
164             use MyDoc;
165              
166             # Create a new instance of MyDoc
167             my $doc = MyDoc->new(
168             doc_id => 'foo12',
169             title => 'Foo',
170             body => 'blah blah',
171             tags => [qw(horse yellow angry)],
172             authors => {
173             jdoe => {
174             name => 'John Doe',
175             email => 'jdoe@gmail.com',
176             roles => [qw(author reader)],
177             },
178             bsmith => {
179             name => 'Bob Smith',
180             email => 'bsmith@yahoo.com',
181             roles => [qw(editor reader)],
182             },
183             },
184             );
185              
186             # Save it to DynamoDB
187             $doc->store();
188              
189             # Load the saved data into a new instance
190             my $doc2 = MyDoc->load('foo12');
191              
192             # This should say 'Bob Smith'
193             print $doc2->authors->{bsmith}{name};
194              
195             =head1 DESCRIPTION
196              
197             MooseX::Storage::IO::AmazonDynamoDB is a Moose role that provides an io layer for L<MooseX::Storage> to store/load your Moose objects to Amazon's DynamoDB NoSQL database service.
198              
199             You should understand the basics of L<Moose>, L<MooseX::Storage>, and L<DynamoDB|http://aws.amazon.com/dynamodb/> before using this module.
200              
201             This module uses L<Paws> as its client library to the DynamoDB service, via L<PawsX::DynamoDB::DocumentClient>. By default it uses the Paws configuration defaults (region, credentials, etc.). You can customize this behavior - see L<"CLIENT CONFIGURATION">.
202              
203             At a bare minimum the consuming class needs to tell this role what table to use and what field to use as a primary key - see L<"table_name"> and L<"key_attr">.
204              
205             =head2 BREAKING CHANGES IN v0.07
206              
207             v0.07 transitioned the underlying DynamoDB client from L<Amazon::DynamoDB> to L<Paws::Dynamodb>, in order to stay more up-to-date with AWS features. Any existing code which customized the client configuration will break when upgrading to v0.07. Support for creating tables was also removed.
208              
209             The following role parameters were removed: client_attr, client_builder_method, client_class, client_args_method, host, port, ssl, dynamodb_local, create_table_method.
210              
211             The following attibutes were removed: dynamo_db_client
212              
213             The following methods were removed: build_dynamo_db_client, dynamo_db_client_args, dynamo_db_create_table
214              
215             The dynamo_db_client parameter to load() was removed, in favor of dynamodb_document_client.
216              
217             The dynamo_db_client and async parameters to store() were removed.
218              
219             Please see See L<"CLIENT CONFIGURATION"> for details on how to configure your client in v0.07 and above.
220              
221             =head1 PARAMETERS
222              
223             There are many parameters you can set when consuming this role that configure it in different ways.
224              
225             =head2 REQUIRED
226              
227             =head3 key_attr
228              
229             "key_attr" is a required parameter when consuming this role. It specifies an attribute in your class that will provide the primary key value for storing your object to DynamoDB. Currently only single primary keys are supported, or what DynamoDB calls "Hash Type Primary Key" (see their L<documentation|http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DataModel.html#DataModel.PrimaryKey>). See the L<"SYNOPSIS"> for an example.
230              
231             =head2 OPTIONAL
232              
233             =head3 table_name
234              
235             Specifies the name of the DynamoDB table to use for your objects - see the example in the L<"SYNOPSIS">. Alternatively, you can return the table name via a class method - see L<"dynamo_db_table_name">.
236              
237             =head3 table_name_method
238              
239             By default, this role will add a method named 'dynamo_db_table_name' to your class (see below for method description). If you want to use a different name for this method (e.g., because it conflicts with an existing method), you can change it via this parameter.
240              
241             =head3 force_type
242              
243             Gets passed to L<Net::Amazon::DynamoDB::Marshaler> when converting our packed data to DynamoDB format.
244              
245             It is highly recommended that you set the types for any attributes that are part of a key (either key_attr, or an attribute that's part of an index). Read up on force_type in L<Net::Amazon::DynamoDB::Marshaler> for more details.
246              
247             =head3 document_client_attribute_name
248              
249             By default, this role adds an attribute to your class named 'dynamodb_document_client' (see below for attribute description). If you want to use a different name for this attribute, you can change it via this parameter.
250              
251             =head3 parameter document_client_builder
252              
253             Allows customization of the PawsX::DynamoDB::DocumentClient object used to interact with DynamoDB. See L<"CLIENT CONFIGURATION"> for more details.
254              
255             =head1 ATTRIBUTES
256              
257             =head2 dynamodb_document_client
258              
259             This role adds an attribute named "dynamodb_document_client" to your consuming class. This attribute holds an instance of L<PawsX::DynamoDB::DocumentClient> that will be used to communicate with the DynamoDB service.
260              
261             You can change this attribute's name via the document_client_attribute_name parameter.
262              
263             The attribute is lazily built via document_client_builder. See L<"CLIENT CONFIGURATION"> for more details.
264              
265             =head1 METHODS
266              
267             Following are methods that will be added to your consuming class.
268              
269             =head2 $obj->store()
270              
271             Object method. Stores the packed Moose object to DynamoDb.
272              
273             =head2 $obj = $class->load($key, [, dynamodb_document_client => $client ][, inject => { key => val, ... } ])
274              
275             Class method. Queries DynamoDB with a primary key, and returns a new Moose object built from the resulting data. Returns undefined if they key could not be found in DyanmoDB.
276              
277             The first argument is the primary key to use, and is required.
278              
279             Optional parameters can be specified following the key:
280              
281             =over 4
282              
283             =item dynamodb_document_client - Directly provide a PawsX::DynamoDB::DocumentClient object, instead of trying to build one using the class' configuration.
284              
285             =item inject - supply additional arguments to the class' new function, or override ones from the resulting data.
286              
287             =back
288              
289             =head2 dynamo_db_table_name
290              
291             A class method that will return the table name to use. This method will be called if the L<"table_name"> parameter is not set. So you could rewrite the Moose class in the L<"SYNOPSIS"> like this:
292              
293             package MyDoc;
294             use Moose;
295             use MooseX::Storage;
296              
297             with Storage(io => [ 'AmazonDynamoDB' => {
298             key_attr => 'doc_id',
299             }]);
300              
301             ...
302              
303             sub dynamo_db_table_name {
304             my $class = shift;
305             return $ENV{DEVELOPMENT} ? 'my_docs_dev' : 'my_docs';
306             }
307              
308             You can change this method's name via the table_name_method parameter.
309              
310             =head1 CLIENT CONFIGURATION
311              
312             This role uses the 'dynamodb_document_client' attribute (assuming you didn't rename it via 'document_client_attribute_name') to interact with DynamoDB. This attribute is lazily built, and should hold an instance of L<PawsX::DynamoDB::DocumentClient>.
313              
314             The client is built by a coderef that is stored in the role's document_client_builder parameter. By default, that coderef is simply:
315              
316             sub { return PawsX::DynamoDB::DocumentClient->new(); }
317              
318             If you need to customize the client, you do so by providing your own builder coderef. For instance, you could set the region directly:
319              
320             package MyDoc;
321             use Moose;
322             use MooseX::Storage;
323             use PawsX::DynamoDB::DocumentClient;
324              
325             with Storage(io => [ 'AmazonDynamoDB' => {
326             table_name => 'my_docs',
327             key_attr => 'doc_id',
328             document_client_builder => \&_build_document_client,
329             }]);
330              
331             sub _build_document_client {
332             my $region = get_my_region_somehow();
333             return PawsX::DynamoDB::DocumentClient->new(region => $region);
334             }
335              
336             See L<"DYNAMODB LOCAL"> for an example of configuring our Paws client to run against a locally running dynamodb clone.
337              
338             Note: the dynamodb_document_client attribute is not typed to a strict isa('PawsX::DynamoDB::DocumentClient'), but instead requires an object that has a 'get' and 'put' method. So you can provide some kind of mocked object, but that is left as an exercise to the reader - although examples are welcome!
339              
340             =head1 DYNAMODB LOCAL
341              
342             Here's an example of configuring your client to run against DynamoDB Local based on an environment variable. Make sure you've read L<CLIENT CONFIGURATION>. More information about DynamoDB Local can be found at L<AWS|http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html>.
343              
344             package MyDoc;
345             use Moose;
346             use MooseX::Storage;
347             use Paws;
348             use Paws::Credential::Explicit;
349             use PawsX::DynamoDB::DocumentClient;
350              
351             with Storage(io => [ 'AmazonDynamoDB' => {
352             table_name => $table_name,
353             key_attr => 'doc_id',
354             document_client_builder => \&_build_document_client,
355             }]);
356              
357             sub _build_document_client {
358             if ($ENV{DYNAMODB_LOCAL}) {
359             my $dynamodb = Paws->service(
360             'DynamoDB',
361             region => 'us-east-1',
362             region_rules => [ { uri => 'http://localhost:8000'} ],
363             credentials => Paws::Credential::Explicit->new(
364             access_key => 'XXXXXXXXX',
365             secret_key => 'YYYYYYYYY',
366             ),
367             max_attempts => 2,
368             );
369             return PawsX::DynamoDB::DocumentClient->new(dynamodb => $dynamodb);
370             }
371             return PawsX::DynamoDB::DocumentClient->new();
372             }
373              
374             =head1 NOTES
375              
376             =head2 Strongly consistent reads
377              
378             When executing load(), this module will always use strongly consistent reads when calling DynamoDB's GetItem operation. Read about DyanmoDB's consistency model in their L<FAQ|http://aws.amazon.com/dynamodb/faqs/> to learn more.
379              
380             =head2 Format level (freeze/thaw)
381              
382             Note that this role does not need you to implement a 'format' level for your object, i.e freeze/thaw. You can add one if you want it for other purposes.
383              
384             =head2 Pre-v0.07 objects
385              
386             Before v0.07, this module stored objects to DyanmoDB using L<Amazon::DynamoDB>. It worked around some issues with that module by serializing certain data types to JSON. Objects stored using this old system will be deserialized correctly.
387              
388             =head1 SEE ALSO
389              
390             =over 4
391              
392             =item L<Moose>
393              
394             =item L<MooseX::Storage>
395              
396             =item L<Amazon's DynamoDB Homepage|http://aws.amazon.com/dynamodb/>
397              
398             =item L<PawsX::DynamoDB::DocumentClient> - DynamoDB client.
399              
400             =item L<Paws> - AWS library.
401              
402             =back
403              
404             =head1 AUTHOR
405              
406             Steve Caldwell E<lt>scaldwell@gmail.comE<gt>
407              
408             =head1 COPYRIGHT
409              
410             Copyright 2015- Steve Caldwell E<lt>scaldwell@gmail.comE<gt>
411              
412             =head1 LICENSE
413              
414             This library is free software; you can redistribute it and/or modify
415             it under the same terms as Perl itself.
416              
417             =head1 ACKNOWLEDGEMENTS
418              
419             Thanks to L<Campus Explorer|http://www.campusexplorer.com>, who allowed me to release this code as open source.
420              
421             =cut