dynamoid 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 | [](https://rubygems.org/gems/dynamoid) |
|
|
23
|
+
| version | [](https://rubygems.org/gems/dynamoid) |
|
|
24
|
+
| dependencies | [](https://gemnasium.com/github.com/Dynamoid/Dynamoid) [](https://depfu.com)|
|
|
25
|
+
| code quality | [](https://codeclimate.com/github/Dynamoid/Dynamoid) |
|
|
26
|
+
| continuous integration | [](https://travis-ci.org/Dynamoid/Dynamoid) |
|
|
27
|
+
| test coverage | [](https://coveralls.io/github/Dynamoid/Dynamoid?branch=master) |
|
|
28
|
+
| triage helpers | [](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
|