dynamoid 2.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -0
- data/LICENSE.txt +18 -16
- data/README.md +154 -15
- data/dynamoid.gemspec +2 -1
- data/lib/dynamoid/adapter.rb +9 -5
- data/lib/dynamoid/adapter_plugin/aws_sdk_v2.rb +115 -54
- data/lib/dynamoid/config.rb +20 -1
- data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +11 -0
- data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +25 -0
- data/lib/dynamoid/criteria/chain.rb +19 -11
- data/lib/dynamoid/document.rb +71 -0
- data/lib/dynamoid/finders.rb +23 -3
- data/lib/dynamoid/persistence.rb +102 -21
- data/lib/dynamoid/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c5d8f6269824b26eaae72aa50582f732cf12d32a
|
4
|
+
data.tar.gz: b2d435682f178120634e816294f9903c205c2a41
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8cda5990c7916d53b5a34fb1c6b66cba13191f51627bd100fb276354e391c73168a7c06b01c27da946b3b164e12b15727ef1f41aa1ebd60b4e17128ce6ab2e4c
|
7
|
+
data.tar.gz: 821224d319424d6bb75e76b8ba17ed9832a73d9efab46622b6666ab0239c46515b883a37797e3fbec527fec98b0cd09929e5804e81d926d633d4c41bacc4c9ba
|
data/CHANGELOG.md
CHANGED
@@ -12,6 +12,38 @@
|
|
12
12
|
|
13
13
|
* N/A
|
14
14
|
|
15
|
+
# 2.1.0
|
16
|
+
|
17
|
+
## Breaking
|
18
|
+
|
19
|
+
* N/A
|
20
|
+
|
21
|
+
## Improvements
|
22
|
+
|
23
|
+
* Feature: [#221](https://github.com/Dynamoid/Dynamoid/pull/221) Add field declaration option `of` to specify the type of `set` elements (@pratik60)
|
24
|
+
* Feature: [#223](https://github.com/Dynamoid/Dynamoid/pull/223) Add field declaration option `store_as_string` to store `datetime` as ISO-8601 formatted strings (@william101)
|
25
|
+
* Feature: [#228](https://github.com/Dynamoid/Dynamoid/pull/228) Add field declaration option `store_as_string` to store `date` as ISO-8601 formatted strings (@andrykonchin)
|
26
|
+
* Feature: [#229](https://github.com/Dynamoid/Dynamoid/pull/229) Support hash argument for `start` chain method (@mnussbaumer)
|
27
|
+
* Feature: [#236](https://github.com/Dynamoid/Dynamoid/pull/236) Change log level from `info` to `debug` for benchmark logging (@kicktheken)
|
28
|
+
* Feature: [#239](https://github.com/Dynamoid/Dynamoid/pull/239) Add methods for low-level updating: `.update`, `.update_fields` and `.upsert` (@andrykonchin)
|
29
|
+
* Feature: [#243](https://github.com/Dynamoid/Dynamoid/pull/243) Support `ne` condition operator (@andrykonchin)
|
30
|
+
* Feature: [#246](https://github.com/Dynamoid/Dynamoid/pull/246) Added support of backoff in batch operations (@andrykonchin)
|
31
|
+
* added global config options `backoff` and `backoff_strategies` to configure backoff
|
32
|
+
* added `constant` and `exponential` built-in backoff strategies
|
33
|
+
* `.find_all` and `.import` support new backoff options
|
34
|
+
|
35
|
+
## Fixes
|
36
|
+
|
37
|
+
* Bug: [#216](https://github.com/Dynamoid/Dynamoid/pull/216) Fix global index detection in queries with conditions other than equal (@andrykonchin)
|
38
|
+
* Bug: [#224](https://github.com/Dynamoid/Dynamoid/pull/224) Fix how `contains` operator works with `set` and `array` field types (@andrykonchin)
|
39
|
+
* Bug: [#225](https://github.com/Dynamoid/Dynamoid/pull/225) Fix equal conditions for `array` fields (@andrykonchin)
|
40
|
+
* Bug: [#229](https://github.com/Dynamoid/Dynamoid/pull/229) Repair support `start` chain method on Scan operation (@mnussbaumer)
|
41
|
+
* Bug: [#238](https://github.com/Dynamoid/Dynamoid/pull/238) Fix default value of `models_dir` config option (@baloran)
|
42
|
+
* Bug: [#244](https://github.com/Dynamoid/Dynamoid/pull/244) Allow to pass empty strings and sets to `.import` (@andrykonchin)
|
43
|
+
* Bug: [#246](https://github.com/Dynamoid/Dynamoid/pull/246) Batch operations (`batch_write_item` and `batch_read_item`) handle unprocessed items themselves (@andrykonchin)
|
44
|
+
* Bug: [#250](https://github.com/Dynamoid/Dynamoid/pull/250) Update outdated warning message about inefficient query and missing indices (@andrykonchin)
|
45
|
+
* Bug: [252](https://github.com/Dynamoid/Dynamoid/pull/252) Don't loose nanoseconds when store DateTime as float number
|
46
|
+
|
15
47
|
# 2.0.0
|
16
48
|
|
17
49
|
## Breaking
|
data/LICENSE.txt
CHANGED
@@ -1,20 +1,22 @@
|
|
1
|
+
MIT License
|
2
|
+
|
1
3
|
Copyright (c) 2012 Josh Symonds
|
4
|
+
Copyright (c) 2013 - 2018 Dynamoid, https://github.com/Dynamoid
|
2
5
|
|
3
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
the following conditions:
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
8
|
+
in the Software without restriction, including without limitation the rights
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
11
|
+
furnished to do so, subject to the following conditions:
|
10
12
|
|
11
|
-
The above copyright notice and this permission notice shall be
|
12
|
-
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
14
|
+
copies or substantial portions of the Software.
|
13
15
|
|
14
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
OF
|
20
|
-
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
22
|
+
SOFTWARE.
|
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# Dynamoid
|
2
2
|
|
3
|
-
You are viewing the README for
|
4
|
-
|
3
|
+
You are viewing the README for version 2 of Dynamoid. See the [CHANGELOG](https://github.com/Dynamoid/Dynamoid/blob/master/CHANGELOG.md#200) for details on breaking changes since 1.3.x.
|
4
|
+
|
5
5
|
For version 1.3.x use the [1-3-stable branch](https://github.com/Dynamoid/Dynamoid/blob/1-3-stable/README.md).
|
6
6
|
|
7
7
|
Dynamoid is an ORM for Amazon's DynamoDB for Ruby applications. It
|
@@ -19,13 +19,13 @@ But if you want a fast, scalable, simple, easy-to-use database (and a Gem that s
|
|
19
19
|
|------------------------ | ----------------- |
|
20
20
|
| gem name | dynamoid |
|
21
21
|
| license | MIT |
|
22
|
-
| download rank | [![Total Downloads](https://img.shields.io/gem/rt/Dynamoid.
|
23
|
-
| version | [![Gem Version](https://badge.fury.io/rb/dynamoid.
|
24
|
-
| dependencies | [![Dependency Status](https://gemnasium.com/badges/github.com/Dynamoid/Dynamoid.
|
25
|
-
| code quality | [![Code Climate](https://codeclimate.com/github/Dynamoid/Dynamoid.
|
26
|
-
| continuous integration | [![Build Status](https://
|
27
|
-
| test coverage | [![Coverage Status](https://coveralls.io/repos/github/Dynamoid/Dynamoid/badge.
|
28
|
-
| triage helpers | [![
|
22
|
+
| download rank | [![Total Downloads](https://img.shields.io/gem/rt/Dynamoid.svg)](https://rubygems.org/gems/dynamoid) |
|
23
|
+
| version | [![Gem Version](https://badge.fury.io/rb/dynamoid.svg)](https://rubygems.org/gems/dynamoid) |
|
24
|
+
| dependencies | [![Dependency Status](https://gemnasium.com/badges/github.com/Dynamoid/Dynamoid.svg)](https://gemnasium.com/github.com/Dynamoid/Dynamoid) [![Depfu](https://badges.depfu.com/badges/6661c063c8e77a5008344fc7283a50aa/status.svg)](https://depfu.com)|
|
25
|
+
| code quality | [![Code Climate](https://codeclimate.com/github/Dynamoid/Dynamoid.svg)](https://codeclimate.com/github/Dynamoid/Dynamoid) |
|
26
|
+
| continuous integration | [![Build Status](https://travis-ci.org/Dynamoid/Dynamoid.svg?branch=master)](https://travis-ci.org/Dynamoid/Dynamoid) |
|
27
|
+
| test coverage | [![Coverage Status](https://coveralls.io/repos/github/Dynamoid/Dynamoid/badge.svg?branch=master)](https://coveralls.io/github/Dynamoid/Dynamoid?branch=master) |
|
28
|
+
| triage helpers | [![CodeTriage Helpers](https://www.codetriage.com/dynamoid/dynamoid/badges/users.svg)](https://www.codetriage.com/dynamoid/dynamoid) |
|
29
29
|
| homepage | [https://github.com/Dynamoid/Dynamoid](https://github.com/Dynamoid/Dynamoid) |
|
30
30
|
| documentation | [http://rdoc.info/github/Dynamoid/Dynamoid/frames](http://rdoc.info/github/Dynamoid/Dynamoid/frames) |
|
31
31
|
|
@@ -34,11 +34,11 @@ But if you want a fast, scalable, simple, easy-to-use database (and a Gem that s
|
|
34
34
|
Installing Dynamoid is pretty simple. First include the Gem in your Gemfile:
|
35
35
|
|
36
36
|
```ruby
|
37
|
-
gem 'dynamoid', '~>
|
37
|
+
gem 'dynamoid', '~> 2'
|
38
38
|
```
|
39
39
|
## Prerequisities
|
40
40
|
|
41
|
-
Dynamoid depends on the aws-sdk, and this is tested on the current version of aws-sdk (~> 2), rails (
|
41
|
+
Dynamoid depends on the aws-sdk, and this is tested on the current version of aws-sdk (~> 2), rails (>= 4).
|
42
42
|
Hence the configuration as needed for aws to work will be dealt with by aws setup.
|
43
43
|
|
44
44
|
Here are the steps to setup aws-sdk.
|
@@ -49,7 +49,18 @@ gem 'aws-sdk', '~>2'
|
|
49
49
|
|
50
50
|
(or) include the aws-sdk in your Gemfile.
|
51
51
|
|
52
|
-
|
52
|
+
### AWS SDK Version Compatibility
|
53
|
+
|
54
|
+
Make sure you are using the version for the right AWS SDK.
|
55
|
+
|
56
|
+
| Dynamoid version | AWS SDK Version |
|
57
|
+
| ---------------- | --------------- |
|
58
|
+
| 0.x | 1.x |
|
59
|
+
| 1.x | 2.x |
|
60
|
+
| 2.x | 2.x |
|
61
|
+
| 3.x (unreleased) | 3.x |
|
62
|
+
|
63
|
+
### AWS Configuration
|
53
64
|
|
54
65
|
Configure AWS access:
|
55
66
|
[Reference](https://github.com/aws/aws-sdk-ruby)
|
@@ -70,6 +81,7 @@ Create config/initializers/aws.rb as follows:
|
|
70
81
|
Alternatively, if you don't want Aws connection settings to be overwritten for you entire project, you can specify connection settings for Dynamoid only, by setting those in the `Dynamoid.configure` clause:
|
71
82
|
|
72
83
|
```ruby
|
84
|
+
require 'dynamoid'
|
73
85
|
Dynamoid.configure do |config|
|
74
86
|
config.access_key = 'REPLACE_WITH_ACCESS_KEY_ID'
|
75
87
|
config.secret_key = 'REPLACE_WITH_SECRET_ACCESS_KEY'
|
@@ -83,6 +95,7 @@ For a full list of the DDB regions, you can go
|
|
83
95
|
Then you need to initialize Dynamoid config to get it going. Put code similar to this somewhere (a Rails initializer would be a great place for this if you're using Rails):
|
84
96
|
|
85
97
|
```ruby
|
98
|
+
require 'dynamoid'
|
86
99
|
Dynamoid.configure do |config|
|
87
100
|
config.namespace = "dynamoid_app_development" # To namespace tables created by Dynamoid from other tables you might have. Set to nil to avoid namespacing.
|
88
101
|
config.endpoint = 'http://localhost:3000' # [Optional]. If provided, it communicates with the DB listening at the endpoint. This is useful for testing with [Amazon Local DB] (http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html).
|
@@ -90,7 +103,7 @@ Then you need to initialize Dynamoid config to get it going. Put code similar to
|
|
90
103
|
|
91
104
|
```
|
92
105
|
|
93
|
-
### Compatibility Matrix
|
106
|
+
### Ruby & Rails Compatibility Matrix
|
94
107
|
|
95
108
|
| Ruby / Active Record | 4.0.x | 4.1.x | 4.2.x | 5.0.x |
|
96
109
|
|:---------------------:|:-----:|:-----:|:-----:|:-----:|
|
@@ -153,6 +166,44 @@ class Document
|
|
153
166
|
end
|
154
167
|
```
|
155
168
|
|
169
|
+
#### Note on date type
|
170
|
+
|
171
|
+
By default date fields are persisted as days count since 1 January 1970 like UNIX time. If you prefer dates to be stored as ISO-8601 formatted strings instead then set `store_as_string` to `true`
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
class Document
|
175
|
+
include DynamoId::Document
|
176
|
+
|
177
|
+
field :sent_at, :datetime, store_as_string: true
|
178
|
+
end
|
179
|
+
```
|
180
|
+
|
181
|
+
#### Note on datetime type
|
182
|
+
|
183
|
+
By default datetime fields are persisted as UNIX timestamps with milisecond precission in DynamoDB. If you prefer datetimes to be stored as ISO-8601 formatted strings instead then set `store_as_string` to `true`
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
class Document
|
187
|
+
include DynamoId::Document
|
188
|
+
|
189
|
+
field :sent_at, :datetime, store_as_string: true
|
190
|
+
end
|
191
|
+
```
|
192
|
+
|
193
|
+
### Note on set type
|
194
|
+
|
195
|
+
There is `of` option to declare the type of set elements. You can use
|
196
|
+
`:integer` value only
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
class Document
|
200
|
+
include DynamoId::Document
|
201
|
+
|
202
|
+
field :tags, :set, of: :integer
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
206
|
+
|
156
207
|
#### Magic Columns
|
157
208
|
|
158
209
|
You get magic columns of id (string), created_at (datetime), and updated_at (datetime) for free.
|
@@ -403,11 +454,13 @@ There are three types of limits that you can query with:
|
|
403
454
|
Using these in various combinations results in the underlying requests to be made in the smallest size possible and
|
404
455
|
the query returns once `record_limit` or `scan_limit` is satisfied. It will attempt to batch whenever possible.
|
405
456
|
|
406
|
-
You can thus limit the number of evaluated records, or select a record from which to start
|
457
|
+
You can thus limit the number of evaluated records, or select a record from which to start in order to support pagination.
|
407
458
|
|
408
459
|
```ruby
|
409
460
|
Address.record_limit(5).start(address) # Only 5 addresses starting at `address`
|
410
461
|
```
|
462
|
+
Where `address` is an instance of the model or a hash `{the_model_hash_key: 'value', the_model_range_key: 'value'}`:
|
463
|
+
Keep in mind that if you are passing a hash to `.start()` you need to explicitly define all required keys in it including range keys, depending on table or secondary indexes signatures, otherwise you'll get an `Aws::DynamoDB::Errors::ValidationException` either for `Exclusive Start Key must have same size as table's key schema` or `The provided starting key is invalid`
|
411
464
|
|
412
465
|
If you are potentially running over a large data set and this is especially true when using certain filters, you may
|
413
466
|
want to consider limiting the number of scanned records (the number of records DynamoDB infrastructure looks through
|
@@ -472,6 +525,41 @@ User.where("created_at.lt" => DateTime.now - 1.day).all
|
|
472
525
|
|
473
526
|
It also supports .gte and .lte. Turning those into symbols and allowing a Rails SQL-style string syntax is in the works. You can only have one range argument per query, because of DynamoDB's inherent limitations, so use it sensibly!
|
474
527
|
|
528
|
+
|
529
|
+
### Updating
|
530
|
+
|
531
|
+
In order to update document you can use high level methods
|
532
|
+
`#update_attributes`, `#update_attribute` and `.update`.
|
533
|
+
They run validation and collbacks.
|
534
|
+
|
535
|
+
```ruby
|
536
|
+
Address.find(id).update_attributes(city: 'Chicago')
|
537
|
+
Address.find(id).update_attribute(city, 'Chicago')
|
538
|
+
Address.update(id, city: 'Chicago')
|
539
|
+
Address.update(id, { city: 'Chicago' }, if: { deliverable: true })
|
540
|
+
```
|
541
|
+
|
542
|
+
There are also some low level methods `#update`, `.update_fields` and
|
543
|
+
`.upsert`. They don't run validation and callbacks (except `#update` - it
|
544
|
+
runs `update` callbacks). All of them support conditional updates.
|
545
|
+
`#upsert` will create new document if document with specified `id`
|
546
|
+
doesn't exist.
|
547
|
+
|
548
|
+
```ruby
|
549
|
+
Adderess.find(id).update do |i|
|
550
|
+
i.set city: 'Chicago'
|
551
|
+
i.add latitude: 100
|
552
|
+
i.delete set_of_numbers: 10
|
553
|
+
end
|
554
|
+
Adderess.find(id).update(if: { deliverable: true }) do |i|
|
555
|
+
i.set city: 'Chicago'
|
556
|
+
end
|
557
|
+
Address.update_fields(id, city: 'Chicago')
|
558
|
+
Address.update_fields(id, { city: 'Chicago' }, if: { deliverable: true })
|
559
|
+
Address.upsert(id, city: 'Chicago')
|
560
|
+
Address.upsert(id, { city: 'Chicago' }, if: { deliverable: true })
|
561
|
+
```
|
562
|
+
|
475
563
|
### Deleting
|
476
564
|
|
477
565
|
In order to delete some items `delete_all` method should be used.
|
@@ -587,9 +675,13 @@ Listed below are all configuration options.
|
|
587
675
|
* `sync_retry_max_times` - when Dynamoid creates or deletes table synchronously it checks for completion specified times. Default is 60 (times). It's a bit over 2 minutes by default
|
588
676
|
* `sync_retry_wait_seconds` - time to wait between retries. Default is 2 (seconds)
|
589
677
|
* `convert_big_decimal` - if `true` then Dynamoid converts numbers stored in `Hash` in `raw` field to float. Default is `false`
|
590
|
-
* `models_dir` - `dynamoid:create_tables` rake task loads DynamoDb models from this directory. Default is
|
678
|
+
* `models_dir` - `dynamoid:create_tables` rake task loads DynamoDb models from this directory. Default is `./app/models`.
|
591
679
|
* `application_timezone` - Dynamoid converts all `datetime` fields to specified time zone when loads data from the storage.
|
592
680
|
Acceptable values - `utc`, `local` (to use system time zone) and time zone name e.g. `Eastern Time (US & Canada)`. Default is `local`
|
681
|
+
* `store_datetime_as_string` - if `true` then Dynamoid stores :datetime fields in ISO 8601 string format. Default is `false`
|
682
|
+
* `store_date_as_string` - if `true` then Dynamoid stores :date fields in ISO 8601 string format. Default is `false`
|
683
|
+
* `backoff` - is a hash: key is a backoff strategy (symbol), value is parameters for the strategy. Is used in batch operations. Default id `nil`
|
684
|
+
* `backoff_strategies`: is a hash and contains all available strategies. Default is { constant: ..., exponential: ...}
|
593
685
|
|
594
686
|
|
595
687
|
## Concurrency
|
@@ -610,6 +702,52 @@ In this example, all saves to `MyTable` will raise an `Dynamoid::Errors::StaleOb
|
|
610
702
|
|
611
703
|
Calls to `update` and `update!` also increment the `lock_version`, however they do not check the existing value. This guarantees that a update operation will raise an exception in a concurrent save operation, however a save operation will never cause an update to fail. Thus, `update` is useful & safe only for doing atomic operations (e.g. increment a value, add/remove from a set, etc), but should not be used in a read-modify-write pattern.
|
612
704
|
|
705
|
+
|
706
|
+
### Backoff strategies
|
707
|
+
|
708
|
+
|
709
|
+
You can use several methods that run efficiently in batch mode like `.find_all` and `.import`.
|
710
|
+
|
711
|
+
The backoff strategy will be used when, for any reason, some items could not be processed as part of a batch mode command.
|
712
|
+
Operations will be re-run to process these items.
|
713
|
+
|
714
|
+
Exponential backoff is the recommended way to handle throughput limits exceeding and throttling on the table.
|
715
|
+
|
716
|
+
There are two built-in strategies - constant delay and truncated binary exponential backoff.
|
717
|
+
By default no backoff is used but you can specify one of the built-in ones:
|
718
|
+
|
719
|
+
```ruby
|
720
|
+
Dynamoid.configure do |config|
|
721
|
+
config.backoff = { constant: 2.second }
|
722
|
+
end
|
723
|
+
|
724
|
+
Dynamoid.configure do |config|
|
725
|
+
config.backoff = { exponential: { base_backoff: 0.2.seconds, ceiling: 10 } }
|
726
|
+
end
|
727
|
+
|
728
|
+
```
|
729
|
+
|
730
|
+
You can just specify strategy without any arguments to use default presets:
|
731
|
+
|
732
|
+
```ruby
|
733
|
+
Dynamoid.configure do |config|
|
734
|
+
config.backoff = :constant
|
735
|
+
end
|
736
|
+
```
|
737
|
+
|
738
|
+
You can use your own strategy in following way:
|
739
|
+
|
740
|
+
```ruby
|
741
|
+
Dynamoid.configure do |config|
|
742
|
+
config.backoff_strategies[:custom] = lambda do |n|
|
743
|
+
-> { sleep rand(n) }
|
744
|
+
end
|
745
|
+
|
746
|
+
config.backoff = { custom: 10 }
|
747
|
+
end
|
748
|
+
```
|
749
|
+
|
750
|
+
|
613
751
|
## Rake Tasks
|
614
752
|
|
615
753
|
* `rake dynamoid:create_tables`
|
@@ -676,6 +814,7 @@ Also, without contributors the project wouldn't be nearly as awesome. So many th
|
|
676
814
|
* [Pascal Corpet](https://github.com/pcorpet)
|
677
815
|
* [Brian Glusman](https://github.com/bglusman) *
|
678
816
|
* [Peter Boling](https://github.com/pboling) *
|
817
|
+
* [Andrew Konchin](https://github.com/andrykonchin) *
|
679
818
|
|
680
819
|
\* Current Maintainers
|
681
820
|
|
data/dynamoid.gemspec
CHANGED
data/lib/dynamoid/adapter.rb
CHANGED
@@ -51,7 +51,7 @@ module Dynamoid
|
|
51
51
|
def benchmark(method, *args)
|
52
52
|
start = Time.now
|
53
53
|
result = yield
|
54
|
-
Dynamoid.logger.
|
54
|
+
Dynamoid.logger.debug "(#{((Time.now - start) * 1000.0).round(2)} ms) #{method.to_s.split('_').collect(&:upcase).join(' ')}#{ " - #{args.inspect}" unless args.nil? || args.empty? }"
|
55
55
|
return result
|
56
56
|
end
|
57
57
|
|
@@ -80,12 +80,12 @@ module Dynamoid
|
|
80
80
|
# unless multiple ids are passed in.
|
81
81
|
#
|
82
82
|
# @since 0.2.0
|
83
|
-
def read(table, ids, options = {})
|
83
|
+
def read(table, ids, options = {}, &blk)
|
84
84
|
range_key = options.delete(:range_key)
|
85
85
|
|
86
86
|
if ids.respond_to?(:each)
|
87
87
|
ids = ids.collect{|id| range_key ? [id, range_key] : id}
|
88
|
-
batch_get_item({table => ids}, options)
|
88
|
+
batch_get_item({table => ids}, options, &blk)
|
89
89
|
else
|
90
90
|
options[:range_key] = range_key if range_key
|
91
91
|
get_item(table, ids, options)
|
@@ -144,8 +144,12 @@ module Dynamoid
|
|
144
144
|
# Method delegation with benchmark to the underlying adapter. Faster than relying on method_missing.
|
145
145
|
#
|
146
146
|
# @since 0.2.0
|
147
|
-
define_method(m) do |*args|
|
148
|
-
|
147
|
+
define_method(m) do |*args, &blk|
|
148
|
+
if blk.present?
|
149
|
+
benchmark("#{m.to_s}", *args) { adapter.send(m, *args, &blk) }
|
150
|
+
else
|
151
|
+
benchmark("#{m.to_s}", *args) { adapter.send(m, *args) }
|
152
|
+
end
|
149
153
|
end
|
150
154
|
end
|
151
155
|
|
@@ -18,6 +18,7 @@ module Dynamoid
|
|
18
18
|
# we declare schema in models
|
19
19
|
FIELD_MAP = {
|
20
20
|
eq: 'EQ',
|
21
|
+
ne: 'NE',
|
21
22
|
gt: 'GT',
|
22
23
|
lt: 'LT',
|
23
24
|
gte: 'GE',
|
@@ -83,36 +84,54 @@ module Dynamoid
|
|
83
84
|
@client
|
84
85
|
end
|
85
86
|
|
86
|
-
# Puts
|
87
|
+
# Puts multiple items in one table
|
88
|
+
#
|
89
|
+
# If optional block is passed it will be called for each written batch of items, meaning once per batch.
|
90
|
+
# Block receives boolean flag which is true if there are some unprocessed items, otherwise false.
|
91
|
+
#
|
92
|
+
# @example Saves several items to the table testtable
|
93
|
+
# Dynamoid::AdapterPlugin::AwsSdkV2.batch_write_item('table1', [{ id: '1', name: 'a' }, { id: '2', name: 'b'}])
|
94
|
+
#
|
95
|
+
# @example Pass block
|
96
|
+
# Dynamoid::AdapterPlugin::AwsSdkV2.batch_write_item('table1', items) do |bool|
|
97
|
+
# if bool
|
98
|
+
# puts 'there are unprocessed items'
|
99
|
+
# end
|
100
|
+
# end
|
87
101
|
#
|
88
102
|
# @param [String] table_name the name of the table
|
89
103
|
# @param [Array] items to be processed
|
90
104
|
# @param [Hash] additional options
|
105
|
+
# @param [Proc] optional block
|
91
106
|
#
|
92
107
|
# See:
|
93
108
|
# * http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html
|
94
109
|
# * http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_write_item-instance_method
|
95
|
-
#
|
96
|
-
# TODO handle rejections because of exceeding limit for the whole request - 16 MB,
|
97
|
-
# item size limit - 400 KB or because provisioned throughput is exceeded
|
98
110
|
def batch_write_item table_name, objects, options = {}
|
99
|
-
|
100
|
-
|
101
|
-
objects.each_slice(BATCH_WRITE_ITEM_REQUESTS_LIMIT) do |os|
|
102
|
-
requests << os.map { |o| { put_request: { item: o } } }
|
103
|
-
end
|
111
|
+
items = objects.map { |o| sanitize_item(o) }
|
104
112
|
|
105
113
|
begin
|
106
|
-
|
107
|
-
|
114
|
+
while items.present? do
|
115
|
+
batch = items.shift(BATCH_WRITE_ITEM_REQUESTS_LIMIT)
|
116
|
+
requests = batch.map { |item| { put_request: { item: item } } }
|
117
|
+
|
118
|
+
response = client.batch_write_item(
|
108
119
|
{
|
109
120
|
request_items: {
|
110
|
-
table_name =>
|
121
|
+
table_name => requests,
|
111
122
|
},
|
112
123
|
return_consumed_capacity: 'TOTAL',
|
113
124
|
return_item_collection_metrics: 'SIZE'
|
114
125
|
}.merge!(options)
|
115
126
|
)
|
127
|
+
|
128
|
+
if block_given?
|
129
|
+
yield(response.unprocessed_items.present?)
|
130
|
+
end
|
131
|
+
|
132
|
+
if response.unprocessed_items.present?
|
133
|
+
items += response.unprocessed_items[table_name].map { |r| r.put_request.item }
|
134
|
+
end
|
116
135
|
end
|
117
136
|
rescue Aws::DynamoDB::Errors::ConditionalCheckFailedException => e
|
118
137
|
raise Dynamoid::Errors::ConditionalCheckFailedException, e
|
@@ -121,17 +140,37 @@ module Dynamoid
|
|
121
140
|
|
122
141
|
# Get many items at once from DynamoDB. More efficient than getting each item individually.
|
123
142
|
#
|
143
|
+
# If optional block is passed `nil` will be returned and the block will be called for each read batch of items,
|
144
|
+
# meaning once per batch.
|
145
|
+
#
|
146
|
+
# Block receives parameters:
|
147
|
+
# * hash with items like `{ table_name: [items]}`
|
148
|
+
# * and boolean flag is true if there are some unprocessed keys, otherwise false.
|
149
|
+
#
|
124
150
|
# @example Retrieve IDs 1 and 2 from the table testtable
|
125
|
-
# Dynamoid::AdapterPlugin::AwsSdkV2.batch_get_item(
|
151
|
+
# Dynamoid::AdapterPlugin::AwsSdkV2.batch_get_item('table1' => ['1', '2'])
|
152
|
+
#
|
153
|
+
# @example Pass block to receive each batch
|
154
|
+
# Dynamoid::AdapterPlugin::AwsSdkV2.batch_get_item('table1' => ids) do |hash, bool|
|
155
|
+
# puts hash['table1']
|
156
|
+
#
|
157
|
+
# if bool
|
158
|
+
# puts 'there are unprocessed keys'
|
159
|
+
# end
|
160
|
+
# end
|
126
161
|
#
|
127
162
|
# @param [Hash] table_ids the hash of tables and IDs to retrieve
|
128
163
|
# @param [Hash] options to be passed to underlying BatchGet call
|
164
|
+
# @param [Proc] optional block can be passed to handle each batch of items
|
129
165
|
#
|
130
166
|
# @return [Hash] a hash where keys are the table names and the values are the retrieved items
|
131
167
|
#
|
168
|
+
# See:
|
169
|
+
# * http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_get_item-instance_method
|
170
|
+
#
|
132
171
|
# @since 1.0.0
|
133
172
|
#
|
134
|
-
# @todo: Provide support for passing options to underlying batch_get_item
|
173
|
+
# @todo: Provide support for passing options to underlying batch_get_item
|
135
174
|
def batch_get_item(table_ids, options = {})
|
136
175
|
request_items = Hash.new{|h, k| h[k] = []}
|
137
176
|
return request_items if table_ids.all?{|k, v| v.blank?}
|
@@ -140,19 +179,22 @@ module Dynamoid
|
|
140
179
|
|
141
180
|
table_ids.each do |t, ids|
|
142
181
|
next if ids.blank?
|
182
|
+
ids = Array(ids).dup
|
143
183
|
tbl = describe_table(t)
|
144
184
|
hk = tbl.hash_key.to_s
|
145
185
|
rng = tbl.range_key.to_s
|
146
186
|
|
147
|
-
|
187
|
+
while ids.present? do
|
188
|
+
batch = ids.shift(Dynamoid::Config.batch_size)
|
189
|
+
|
148
190
|
request_items = Hash.new{|h, k| h[k] = []}
|
149
191
|
|
150
192
|
keys = if rng.present?
|
151
|
-
Array(
|
193
|
+
Array(batch).map do |h, r|
|
152
194
|
{ hk => h, rng => r }
|
153
195
|
end
|
154
196
|
else
|
155
|
-
Array(
|
197
|
+
Array(batch).map do |id|
|
156
198
|
{ hk => id }
|
157
199
|
end
|
158
200
|
end
|
@@ -165,13 +207,29 @@ module Dynamoid
|
|
165
207
|
request_items: request_items
|
166
208
|
)
|
167
209
|
|
168
|
-
|
169
|
-
|
210
|
+
unless block_given?
|
211
|
+
results.data[:responses].each do |table, rows|
|
212
|
+
ret[table] += rows.collect { |r| result_item_to_hash(r) }
|
213
|
+
end
|
214
|
+
else
|
215
|
+
batch_results = Hash.new([].freeze)
|
216
|
+
|
217
|
+
results.data[:responses].each do |table, rows|
|
218
|
+
batch_results[table] += rows.collect { |r| result_item_to_hash(r) }
|
219
|
+
end
|
220
|
+
|
221
|
+
yield(batch_results, results.unprocessed_keys.present?)
|
222
|
+
end
|
223
|
+
|
224
|
+
if results.unprocessed_keys.present?
|
225
|
+
ids += results.unprocessed_keys[t].keys.map { |h| h[hk] }
|
170
226
|
end
|
171
227
|
end
|
172
228
|
end
|
173
229
|
|
174
|
-
|
230
|
+
unless block_given?
|
231
|
+
ret
|
232
|
+
end
|
175
233
|
end
|
176
234
|
|
177
235
|
# Delete many items at once from DynamoDB. More efficient than delete each item individually.
|
@@ -421,13 +479,8 @@ module Dynamoid
|
|
421
479
|
#
|
422
480
|
# See: http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#put_item-instance_method
|
423
481
|
def put_item(table_name, object, options = {})
|
424
|
-
item = {}
|
425
482
|
options ||= {}
|
426
|
-
|
427
|
-
object.each do |k, v|
|
428
|
-
next if v.nil? || ((v.is_a?(Set) || v.is_a?(String)) && v.empty?)
|
429
|
-
item[k.to_s] = v
|
430
|
-
end
|
483
|
+
item = sanitize_item(object)
|
431
484
|
|
432
485
|
begin
|
433
486
|
client.put_item(
|
@@ -480,39 +533,21 @@ module Dynamoid
|
|
480
533
|
record_limit = opts.delete(:record_limit)
|
481
534
|
scan_limit = opts.delete(:scan_limit)
|
482
535
|
batch_size = opts.delete(:batch_size)
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
opts.delete(:next_token).tap do |token|
|
487
|
-
break unless token
|
488
|
-
q[:exclusive_start_key] = {
|
489
|
-
hk => token[:hash_key_element],
|
490
|
-
rng => token[:range_key_element]
|
491
|
-
}
|
492
|
-
# For secondary indices the start key must contain the indices composite key
|
493
|
-
# but also the table's composite keys
|
494
|
-
q[:exclusive_start_key][table.hash_key] = token[:table_hash_key_element] if token[:table_hash_key_element]
|
495
|
-
q[:exclusive_start_key][table.range_key] = token[:table_range_key_element] if token[:table_range_key_element]
|
496
|
-
end
|
536
|
+
exclusive_start_key = opts.delete(:exclusive_start_key)
|
537
|
+
limit = [record_limit, scan_limit, batch_size].compact.min
|
497
538
|
|
498
539
|
key_conditions = {
|
499
540
|
hk => {
|
500
|
-
# TODO: Provide option for other operators like NE, IN, LE, etc
|
501
541
|
comparison_operator: EQ,
|
502
|
-
attribute_value_list:
|
503
|
-
opts.delete(:hash_value).freeze
|
504
|
-
]
|
542
|
+
attribute_value_list: attribute_value_list(EQ, opts.delete(:hash_value).freeze)
|
505
543
|
}
|
506
544
|
}
|
507
545
|
|
508
546
|
opts.each_pair do |k, v|
|
509
|
-
# TODO: ATM, only few comparison operators are supported, provide support for all operators
|
510
547
|
next unless(op = RANGE_MAP[k])
|
511
548
|
key_conditions[rng] = {
|
512
549
|
comparison_operator: op,
|
513
|
-
attribute_value_list:
|
514
|
-
opts.delete(k).freeze
|
515
|
-
].flatten # Flatten as BETWEEN operator specifies array of two elements
|
550
|
+
attribute_value_list: attribute_value_list(op, opts.delete(k).freeze)
|
516
551
|
}
|
517
552
|
end
|
518
553
|
|
@@ -520,12 +555,12 @@ module Dynamoid
|
|
520
555
|
opts.reject {|k, _| k.in? RANGE_MAP.keys}.each do |attr, hash|
|
521
556
|
query_filter[attr] = {
|
522
557
|
comparison_operator: FIELD_MAP[hash.keys[0]],
|
523
|
-
attribute_value_list: [
|
524
|
-
hash.values[0].freeze
|
525
|
-
].flatten # Flatten as BETWEEN operator specifies array of two elements
|
558
|
+
attribute_value_list: attribute_value_list(FIELD_MAP[hash.keys[0]], hash.values[0].freeze)
|
526
559
|
}
|
527
560
|
end
|
528
561
|
|
562
|
+
q[:limit] = limit if limit
|
563
|
+
q[:exclusive_start_key] = exclusive_start_key if exclusive_start_key
|
529
564
|
q[:table_name] = table_name
|
530
565
|
q[:key_conditions] = key_conditions
|
531
566
|
q[:query_filter] = query_filter
|
@@ -593,15 +628,16 @@ module Dynamoid
|
|
593
628
|
record_limit = select_opts.delete(:record_limit)
|
594
629
|
scan_limit = select_opts.delete(:scan_limit)
|
595
630
|
batch_size = select_opts.delete(:batch_size)
|
631
|
+
exclusive_start_key = select_opts.delete(:exclusive_start_key)
|
596
632
|
request_limit = [record_limit, scan_limit, batch_size].compact.min
|
597
633
|
request[:limit] = request_limit if request_limit
|
598
|
-
|
634
|
+
request[:exclusive_start_key] = exclusive_start_key if exclusive_start_key
|
635
|
+
|
599
636
|
if scan_hash.present?
|
600
637
|
request[:scan_filter] = scan_hash.reduce({}) do |memo, (attr, cond)|
|
601
|
-
# Flatten as BETWEEN operator specifies array of two elements
|
602
638
|
memo.merge(attr.to_s => {
|
603
639
|
comparison_operator: FIELD_MAP[cond.keys[0]],
|
604
|
-
attribute_value_list: [cond.values[0].freeze
|
640
|
+
attribute_value_list: attribute_value_list(FIELD_MAP[cond.keys[0]], cond.values[0].freeze)
|
605
641
|
})
|
606
642
|
end
|
607
643
|
end
|
@@ -748,6 +784,10 @@ module Dynamoid
|
|
748
784
|
conditions.delete(:unless_exists).try(:each) do |col|
|
749
785
|
expected[col.to_s][:exists] = false
|
750
786
|
end
|
787
|
+
conditions.delete(:if_exists).try(:each) do |col, val|
|
788
|
+
expected[col.to_s][:exists] = true
|
789
|
+
expected[col.to_s][:value] = val
|
790
|
+
end
|
751
791
|
conditions.delete(:if).try(:each) do |col, val|
|
752
792
|
expected[col.to_s][:value] = val
|
753
793
|
end
|
@@ -919,6 +959,21 @@ module Dynamoid
|
|
919
959
|
}
|
920
960
|
end
|
921
961
|
|
962
|
+
# Build an array of values for Condition
|
963
|
+
# Is used in ScanFilter and QueryFilter
|
964
|
+
# https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html
|
965
|
+
# @params [String] operator: value of RANGE_MAP or FIELD_MAP hash, e.g. "EQ", "LT" etc
|
966
|
+
# @params [Object] value: scalar value or array/set
|
967
|
+
def attribute_value_list(operator, value)
|
968
|
+
# For BETWEEN and IN operators we should keep value as is (it should be already an array)
|
969
|
+
# For all the other operators we wrap the value with array
|
970
|
+
if ["BETWEEN", "IN"].include?(operator)
|
971
|
+
[value].flatten
|
972
|
+
else
|
973
|
+
[value]
|
974
|
+
end
|
975
|
+
end
|
976
|
+
|
922
977
|
#
|
923
978
|
# Represents a table. Exposes data from the "DescribeTable" API call, and also
|
924
979
|
# provides methods for coercing values to the proper types based on the table's schema data
|
@@ -1035,6 +1090,12 @@ module Dynamoid
|
|
1035
1090
|
DELETE = 'DELETE'.freeze
|
1036
1091
|
PUT = 'PUT'.freeze
|
1037
1092
|
end
|
1093
|
+
|
1094
|
+
def sanitize_item(attributes)
|
1095
|
+
attributes.reject do |k, v|
|
1096
|
+
v.nil? || ((v.is_a?(Set) || v.is_a?(String)) && v.empty?)
|
1097
|
+
end
|
1098
|
+
end
|
1038
1099
|
end
|
1039
1100
|
end
|
1040
1101
|
end
|
data/lib/dynamoid/config.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require 'uri'
|
3
3
|
require 'dynamoid/config/options'
|
4
|
+
require 'dynamoid/config/backoff_strategies/constant_backoff'
|
5
|
+
require 'dynamoid/config/backoff_strategies/exponential_backoff'
|
4
6
|
|
5
7
|
module Dynamoid
|
6
8
|
|
@@ -26,8 +28,15 @@ module Dynamoid
|
|
26
28
|
option :sync_retry_max_times, default: 60 # a bit over 2 minutes
|
27
29
|
option :sync_retry_wait_seconds, default: 2
|
28
30
|
option :convert_big_decimal, default: false
|
29
|
-
option :models_dir, default: 'app/models' # perhaps you keep your dynamoid models in a different directory?
|
31
|
+
option :models_dir, default: './app/models' # perhaps you keep your dynamoid models in a different directory?
|
30
32
|
option :application_timezone, default: :local # available values - :utc, :local, time zone names
|
33
|
+
option :store_datetime_as_string, default: false # store Time fields in ISO 8601 string format
|
34
|
+
option :store_date_as_string, default: false # store Date fields in ISO 8601 string format
|
35
|
+
option :backoff, default: nil # callable object to handle exceeding of table throughput limit
|
36
|
+
option :backoff_strategies, default: {
|
37
|
+
constant: BackoffStrategies::ConstantBackoff,
|
38
|
+
exponential: BackoffStrategies::ExponentialBackoff
|
39
|
+
}
|
31
40
|
|
32
41
|
# The default logger for Dynamoid: either the Rails logger or just stdout.
|
33
42
|
#
|
@@ -55,5 +64,15 @@ module Dynamoid
|
|
55
64
|
end
|
56
65
|
end
|
57
66
|
|
67
|
+
def build_backoff
|
68
|
+
if backoff.is_a?(Hash)
|
69
|
+
name = backoff.keys[0]
|
70
|
+
args = backoff.values[0]
|
71
|
+
|
72
|
+
backoff_strategies[name].call(args)
|
73
|
+
else
|
74
|
+
backoff_strategies[backoff].call
|
75
|
+
end
|
76
|
+
end
|
58
77
|
end
|
59
78
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Dynamoid
|
2
|
+
module Config
|
3
|
+
module BackoffStrategies
|
4
|
+
# Truncated binary exponential backoff algorithm
|
5
|
+
# See https://en.wikipedia.org/wiki/Exponential_backoff
|
6
|
+
class ExponentialBackoff
|
7
|
+
def self.call(opts = {})
|
8
|
+
opts = { base_backoff: 0.5, ceiling: 3 }.merge(opts)
|
9
|
+
base_backoff = opts[:base_backoff]
|
10
|
+
ceiling = opts[:ceiling]
|
11
|
+
|
12
|
+
times = 1
|
13
|
+
|
14
|
+
lambda do
|
15
|
+
power = [times - 1, ceiling - 1].min
|
16
|
+
backoff = base_backoff * (2 ** power)
|
17
|
+
sleep backoff
|
18
|
+
|
19
|
+
times += 1
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -161,7 +161,10 @@ module Dynamoid #:nodoc:
|
|
161
161
|
def records_via_scan
|
162
162
|
if Dynamoid::Config.warn_on_scan
|
163
163
|
Dynamoid.logger.warn 'Queries without an index are forced to use scan and are generally much slower than indexed queries!'
|
164
|
-
Dynamoid.logger.warn "You can index this query by adding
|
164
|
+
Dynamoid.logger.warn "You can index this query by adding index declaration to #{source.to_s.downcase}.rb:"
|
165
|
+
Dynamoid.logger.warn "* global_secondary_index hash_key: 'some-name', range_key: 'some-another-name'"
|
166
|
+
Dynamoid.logger.warn "* local_secondary_indexe range_key: 'some-name'"
|
167
|
+
Dynamoid.logger.warn "Not indexed attributes: #{query.keys.sort.collect{|name| ":#{name}"}.join(', ')}"
|
165
168
|
end
|
166
169
|
|
167
170
|
Enumerator.new do |yielder|
|
@@ -196,6 +199,8 @@ module Dynamoid #:nodoc:
|
|
196
199
|
val = type_cast_condition_parameter(name, query[key])
|
197
200
|
|
198
201
|
hash = case operation
|
202
|
+
when 'ne'
|
203
|
+
{ ne: val }
|
199
204
|
when 'gt'
|
200
205
|
{ gt: val }
|
201
206
|
when 'lt'
|
@@ -258,6 +263,8 @@ module Dynamoid #:nodoc:
|
|
258
263
|
end
|
259
264
|
|
260
265
|
def type_cast_condition_parameter(key, value)
|
266
|
+
return value if [:array, :set].include?(source.attributes[key.to_sym][:type])
|
267
|
+
|
261
268
|
if !value.respond_to?(:to_ary)
|
262
269
|
source.dump_field(value, source.attributes[key.to_sym])
|
263
270
|
else
|
@@ -294,7 +301,7 @@ module Dynamoid #:nodoc:
|
|
294
301
|
# But only do so if projects ALL attributes otherwise we won't
|
295
302
|
# get back full data
|
296
303
|
source.global_secondary_indexes.each do |_, gsi|
|
297
|
-
next unless
|
304
|
+
next unless query.keys.map(&:to_s).include?(gsi.hash_key.to_s) && gsi.projected_attributes == :all
|
298
305
|
@hash_key = gsi.hash_key
|
299
306
|
@range_key = gsi.range_key
|
300
307
|
@index_name = gsi.name
|
@@ -309,21 +316,22 @@ module Dynamoid #:nodoc:
|
|
309
316
|
# If using a secondary index then we must include the index's composite key
|
310
317
|
# as well as the tables composite key.
|
311
318
|
def start_key
|
319
|
+
return @start if @start.is_a?(Hash)
|
312
320
|
hash_key = @hash_key || source.hash_key
|
313
321
|
range_key = @range_key || source.range_key
|
314
322
|
|
315
323
|
key = {}
|
316
|
-
key[
|
317
|
-
|
318
|
-
|
319
|
-
|
324
|
+
key[hash_key] = type_cast_condition_parameter(hash_key, @start.send(hash_key))
|
325
|
+
if range_key
|
326
|
+
key[range_key] = type_cast_condition_parameter(range_key, @start.send(range_key))
|
327
|
+
end
|
328
|
+
# Add table composite keys if they differ from secondary index used composite key
|
320
329
|
if hash_key != source.hash_key
|
321
|
-
key[
|
330
|
+
key[source.hash_key] = type_cast_condition_parameter(source.hash_key, @start.hash_key)
|
322
331
|
end
|
323
332
|
if source.range_key && range_key != source.range_key
|
324
|
-
key[
|
333
|
+
key[source.range_key] = type_cast_condition_parameter(source.range_key, @start.range_value)
|
325
334
|
end
|
326
|
-
|
327
335
|
key
|
328
336
|
end
|
329
337
|
|
@@ -334,7 +342,7 @@ module Dynamoid #:nodoc:
|
|
334
342
|
opts[:record_limit] = @record_limit if @record_limit
|
335
343
|
opts[:scan_limit] = @scan_limit if @scan_limit
|
336
344
|
opts[:batch_size] = @batch_size if @batch_size
|
337
|
-
opts[:
|
345
|
+
opts[:exclusive_start_key] = start_key if @start
|
338
346
|
opts[:scan_index_forward] = @scan_index_forward
|
339
347
|
opts
|
340
348
|
end
|
@@ -357,7 +365,7 @@ module Dynamoid #:nodoc:
|
|
357
365
|
opts[:record_limit] = @record_limit if @record_limit
|
358
366
|
opts[:scan_limit] = @scan_limit if @scan_limit
|
359
367
|
opts[:batch_size] = @batch_size if @batch_size
|
360
|
-
opts[:
|
368
|
+
opts[:exclusive_start_key] = start_key if @start
|
361
369
|
opts[:consistent_read] = true if @consistent_read
|
362
370
|
opts
|
363
371
|
end
|
data/lib/dynamoid/document.rb
CHANGED
@@ -119,6 +119,77 @@ module Dynamoid #:nodoc:
|
|
119
119
|
end
|
120
120
|
end
|
121
121
|
|
122
|
+
def update(hash_key, range_key_value=nil, attrs)
|
123
|
+
if range_key.present?
|
124
|
+
range_key_value = dump_field(range_key_value, attributes[self.range_key])
|
125
|
+
else
|
126
|
+
range_key_value = nil
|
127
|
+
end
|
128
|
+
|
129
|
+
model = find(hash_key, range_key: range_key_value, consistent_read: true)
|
130
|
+
model.update_attributes(attrs)
|
131
|
+
model
|
132
|
+
end
|
133
|
+
|
134
|
+
def update_fields(hash_key_value, range_key_value=nil, attrs={}, conditions={})
|
135
|
+
optional_params = [range_key_value, attrs, conditions].compact
|
136
|
+
if optional_params.first.is_a?(Hash)
|
137
|
+
range_key_value = nil
|
138
|
+
attrs, conditions = optional_params[0 .. 1]
|
139
|
+
else
|
140
|
+
range_key_value = optional_params.first
|
141
|
+
attrs, conditions = optional_params[1 .. 2]
|
142
|
+
end
|
143
|
+
|
144
|
+
options = if range_key
|
145
|
+
{ range_key: dump_field(range_key_value, attributes[range_key]) }
|
146
|
+
else
|
147
|
+
{}
|
148
|
+
end
|
149
|
+
|
150
|
+
(conditions[:if_exists] ||= {})[hash_key] = hash_key_value
|
151
|
+
options[:conditions] = conditions
|
152
|
+
|
153
|
+
begin
|
154
|
+
new_attrs = Dynamoid.adapter.update_item(table_name, hash_key_value, options) do |t|
|
155
|
+
attrs.symbolize_keys.each do |k, v|
|
156
|
+
t.set k => dump_field(v, attributes[k])
|
157
|
+
end
|
158
|
+
end
|
159
|
+
new(new_attrs)
|
160
|
+
rescue Dynamoid::Errors::ConditionalCheckFailedException
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def upsert(hash_key_value, range_key_value=nil, attrs={}, conditions={})
|
165
|
+
optional_params = [range_key_value, attrs, conditions].compact
|
166
|
+
if optional_params.first.is_a?(Hash)
|
167
|
+
range_key_value = nil
|
168
|
+
attrs, conditions = optional_params[0 .. 1]
|
169
|
+
else
|
170
|
+
range_key_value = optional_params.first
|
171
|
+
attrs, conditions = optional_params[1 .. 2]
|
172
|
+
end
|
173
|
+
|
174
|
+
options = if range_key
|
175
|
+
{ range_key: dump_field(range_key_value, attributes[range_key]) }
|
176
|
+
else
|
177
|
+
{}
|
178
|
+
end
|
179
|
+
|
180
|
+
options[:conditions] = conditions
|
181
|
+
|
182
|
+
begin
|
183
|
+
new_attrs = Dynamoid.adapter.update_item(table_name, hash_key_value, options) do |t|
|
184
|
+
attrs.symbolize_keys.each do |k, v|
|
185
|
+
t.set k => dump_field(v, attributes[k])
|
186
|
+
end
|
187
|
+
end
|
188
|
+
new(new_attrs)
|
189
|
+
rescue Dynamoid::Errors::ConditionalCheckFailedException
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
122
193
|
def deep_subclasses
|
123
194
|
subclasses + subclasses.map(&:deep_subclasses).flatten
|
124
195
|
end
|
data/lib/dynamoid/finders.rb
CHANGED
@@ -52,9 +52,11 @@ module Dynamoid
|
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
|
-
# Return objects found by the given array of ids, either hash keys, or hash/range key combinations using
|
55
|
+
# Return objects found by the given array of ids, either hash keys, or hash/range key combinations using BatchGetItem.
|
56
56
|
# Returns empty array if no results found.
|
57
57
|
#
|
58
|
+
# Uses backoff specified by `Dynamoid::Config.backoff` config option
|
59
|
+
#
|
58
60
|
# @param [Array<ID>] ids
|
59
61
|
# @param [Hash] options: Passed to the underlying query.
|
60
62
|
#
|
@@ -65,8 +67,26 @@ module Dynamoid
|
|
65
67
|
# find all the tweets using hash key and range key with consistent read
|
66
68
|
# Tweet.find_all([['1', 'red'], ['1', 'green']], :consistent_read => true)
|
67
69
|
def find_all(ids, options = {})
|
68
|
-
|
69
|
-
|
70
|
+
results = unless Dynamoid.config.backoff
|
71
|
+
items = Dynamoid.adapter.read(self.table_name, ids, options)
|
72
|
+
items ? items[self.table_name] : []
|
73
|
+
else
|
74
|
+
items = []
|
75
|
+
backoff = nil
|
76
|
+
Dynamoid.adapter.read(self.table_name, ids, options) do |hash, has_unprocessed_items|
|
77
|
+
items += hash[self.table_name]
|
78
|
+
|
79
|
+
if has_unprocessed_items
|
80
|
+
backoff ||= Dynamoid.config.build_backoff
|
81
|
+
backoff.call
|
82
|
+
else
|
83
|
+
backoff = nil
|
84
|
+
end
|
85
|
+
end
|
86
|
+
items
|
87
|
+
end
|
88
|
+
|
89
|
+
results ? results.map {|i| from_database(i) } : []
|
70
90
|
end
|
71
91
|
|
72
92
|
# Find one object directly by id.
|
data/lib/dynamoid/persistence.rb
CHANGED
@@ -117,25 +117,14 @@ module Dynamoid
|
|
117
117
|
value
|
118
118
|
end
|
119
119
|
when :set
|
120
|
-
|
120
|
+
undump_set(options, value)
|
121
121
|
when :datetime
|
122
|
-
|
123
|
-
value
|
124
|
-
else
|
125
|
-
case Dynamoid::Config.application_timezone
|
126
|
-
when :utc
|
127
|
-
ActiveSupport::TimeZone['UTC'].at(value).to_datetime
|
128
|
-
when :local
|
129
|
-
Time.at(value).to_datetime
|
130
|
-
when String
|
131
|
-
ActiveSupport::TimeZone[Dynamoid::Config.application_timezone].at(value).to_datetime
|
132
|
-
end
|
133
|
-
end
|
122
|
+
parse_datetime(value, options)
|
134
123
|
when :date
|
135
124
|
if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
|
136
125
|
value.to_date
|
137
126
|
else
|
138
|
-
|
127
|
+
parse_date(value, options)
|
139
128
|
end
|
140
129
|
when :boolean
|
141
130
|
if value == 't' || value == true
|
@@ -152,6 +141,17 @@ module Dynamoid
|
|
152
141
|
end
|
153
142
|
end
|
154
143
|
|
144
|
+
def undump_set(options, value)
|
145
|
+
case options[:of]
|
146
|
+
when :integer
|
147
|
+
value.map { |v| Integer(v) }.to_set
|
148
|
+
when :number
|
149
|
+
value.map { |v| BigDecimal.new(v.to_s) }.to_set
|
150
|
+
else
|
151
|
+
value.is_a?(Set) ? value : Set.new(value)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
155
|
def dump_field(value, options)
|
156
156
|
if (field_class = options[:type]).is_a?(Class)
|
157
157
|
if value.respond_to?(:dynamoid_dump)
|
@@ -174,9 +174,9 @@ module Dynamoid
|
|
174
174
|
when :array
|
175
175
|
!value.nil? ? value : nil
|
176
176
|
when :datetime
|
177
|
-
!value.nil? ? value
|
177
|
+
!value.nil? ? format_datetime(value, options) : nil
|
178
178
|
when :date
|
179
|
-
!value.nil? ? (value
|
179
|
+
!value.nil? ? format_date(value, options) : nil
|
180
180
|
when :serialized
|
181
181
|
options[:serializer] ? options[:serializer].dump(value) : value.to_yaml
|
182
182
|
when :raw
|
@@ -212,14 +212,38 @@ module Dynamoid
|
|
212
212
|
end
|
213
213
|
end
|
214
214
|
|
215
|
+
# Creates several models at once.
|
216
|
+
# Neither callbacks nor validations run.
|
217
|
+
# It works efficiently because of using BatchWriteItem.
|
218
|
+
#
|
219
|
+
# Returns array of models
|
220
|
+
#
|
221
|
+
# Uses backoff specified by `Dynamoid::Config.backoff` config option
|
222
|
+
#
|
223
|
+
# @param [Array<Hash>] items
|
224
|
+
#
|
225
|
+
# @example
|
226
|
+
# User.import([{ name: 'a' }, { name: 'b' }])
|
215
227
|
def import(objects)
|
216
|
-
documents = objects.map
|
217
|
-
self.build(attrs).tap
|
228
|
+
documents = objects.map do |attrs|
|
229
|
+
self.build(attrs).tap do |item|
|
218
230
|
item.hash_key = SecureRandom.uuid if item.hash_key.blank?
|
219
|
-
|
220
|
-
|
231
|
+
end
|
232
|
+
end
|
221
233
|
|
222
|
-
Dynamoid.
|
234
|
+
unless Dynamoid.config.backoff
|
235
|
+
Dynamoid.adapter.batch_write_item(self.table_name, documents.map(&:dump))
|
236
|
+
else
|
237
|
+
backoff = nil
|
238
|
+
Dynamoid.adapter.batch_write_item(self.table_name, documents.map(&:dump)) do |has_unprocessed_items|
|
239
|
+
if has_unprocessed_items
|
240
|
+
backoff ||= Dynamoid.config.build_backoff
|
241
|
+
backoff.call
|
242
|
+
else
|
243
|
+
backoff = nil
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
223
247
|
|
224
248
|
documents.each { |d| d.new_record = false }
|
225
249
|
documents
|
@@ -250,6 +274,63 @@ module Dynamoid
|
|
250
274
|
end
|
251
275
|
end
|
252
276
|
|
277
|
+
def format_datetime(value, options)
|
278
|
+
use_string_format = options[:store_as_string].nil? \
|
279
|
+
? Dynamoid.config.store_datetime_as_string \
|
280
|
+
: options[:store_as_string]
|
281
|
+
|
282
|
+
if use_string_format
|
283
|
+
value.to_time.iso8601
|
284
|
+
else
|
285
|
+
unless value.respond_to?(:to_i) && value.respond_to?(:nsec)
|
286
|
+
value = value.to_time
|
287
|
+
end
|
288
|
+
BigDecimal("%d.%09d" % [value.to_i, value.nsec])
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def format_date(value, options)
|
293
|
+
use_string_format = options[:store_as_string].nil? \
|
294
|
+
? Dynamoid.config.store_date_as_string \
|
295
|
+
: options[:store_as_string]
|
296
|
+
|
297
|
+
unless use_string_format
|
298
|
+
(value.to_date - UNIX_EPOCH_DATE).to_i
|
299
|
+
else
|
300
|
+
value.to_date.iso8601
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def parse_datetime(value, options)
|
305
|
+
return value if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
|
306
|
+
|
307
|
+
use_string_format = options[:store_as_string].nil? \
|
308
|
+
? Dynamoid.config.store_datetime_as_string \
|
309
|
+
: options[:store_as_string]
|
310
|
+
value = DateTime.iso8601(value).to_time.to_i if use_string_format
|
311
|
+
|
312
|
+
case Dynamoid::Config.application_timezone
|
313
|
+
when :utc
|
314
|
+
ActiveSupport::TimeZone['UTC'].at(value).to_datetime
|
315
|
+
when :local
|
316
|
+
Time.at(value).to_datetime
|
317
|
+
when String
|
318
|
+
ActiveSupport::TimeZone[Dynamoid::Config.application_timezone].at(value).to_datetime
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def parse_date(value, options)
|
323
|
+
use_string_format = options[:store_as_string].nil? \
|
324
|
+
? Dynamoid.config.store_date_as_string \
|
325
|
+
: options[:store_as_string]
|
326
|
+
|
327
|
+
unless use_string_format
|
328
|
+
UNIX_EPOCH_DATE + value.to_i
|
329
|
+
else
|
330
|
+
Date.iso8601(value)
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
253
334
|
# Evaluates the default value given, this is used by undump
|
254
335
|
# when determining the value of the default given for a field options.
|
255
336
|
#
|
data/lib/dynamoid/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dynamoid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josh Symonds
|
@@ -17,10 +17,11 @@ authors:
|
|
17
17
|
- Pascal Corpet
|
18
18
|
- Brian Glusman
|
19
19
|
- Peter Boling
|
20
|
+
- Andrew Konchin
|
20
21
|
autorequire:
|
21
22
|
bindir: exe
|
22
23
|
cert_chain: []
|
23
|
-
date:
|
24
|
+
date: 2018-04-21 00:00:00.000000000 Z
|
24
25
|
dependencies:
|
25
26
|
- !ruby/object:Gem::Dependency
|
26
27
|
name: activemodel
|
@@ -234,6 +235,8 @@ files:
|
|
234
235
|
- lib/dynamoid/associations/single_association.rb
|
235
236
|
- lib/dynamoid/components.rb
|
236
237
|
- lib/dynamoid/config.rb
|
238
|
+
- lib/dynamoid/config/backoff_strategies/constant_backoff.rb
|
239
|
+
- lib/dynamoid/config/backoff_strategies/exponential_backoff.rb
|
237
240
|
- lib/dynamoid/config/options.rb
|
238
241
|
- lib/dynamoid/criteria.rb
|
239
242
|
- lib/dynamoid/criteria/chain.rb
|
@@ -271,7 +274,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
271
274
|
version: '0'
|
272
275
|
requirements: []
|
273
276
|
rubyforge_project:
|
274
|
-
rubygems_version: 2.
|
277
|
+
rubygems_version: 2.4.5
|
275
278
|
signing_key:
|
276
279
|
specification_version: 4
|
277
280
|
summary: Dynamoid is an ORM for Amazon's DynamoDB
|