dynamoid 3.13.0 → 3.14.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 +41 -4
- data/README.md +60 -1584
- data/lib/dynamoid/associations/association.rb +10 -2
- data/lib/dynamoid/associations/belongs_to.rb +3 -1
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +0 -1
- data/lib/dynamoid/associations/has_many.rb +0 -1
- data/lib/dynamoid/associations/has_one.rb +1 -2
- data/lib/dynamoid/associations/many_association.rb +1 -0
- data/lib/dynamoid/associations.rb +64 -48
- data/lib/dynamoid/components.rb +1 -0
- data/lib/dynamoid/criteria/chain.rb +43 -30
- data/lib/dynamoid/dirty.rb +13 -3
- data/lib/dynamoid/document.rb +29 -21
- data/lib/dynamoid/fields.rb +55 -41
- data/lib/dynamoid/finders.rb +1 -1
- data/lib/dynamoid/indexes.rb +29 -21
- data/lib/dynamoid/persistence/inc.rb +15 -7
- data/lib/dynamoid/persistence/save.rb +21 -14
- data/lib/dynamoid/persistence.rb +88 -54
- data/lib/dynamoid/transactions/mutation/base.rb +76 -0
- data/lib/dynamoid/transactions/mutation/builders/delete_request_builder.rb +44 -0
- data/lib/dynamoid/transactions/mutation/builders/update_request_builder.rb +139 -0
- data/lib/dynamoid/transactions/mutation/create.rb +52 -0
- data/lib/dynamoid/transactions/mutation/delete_with_instance.rb +80 -0
- data/lib/dynamoid/transactions/mutation/delete_with_primary_key.rb +62 -0
- data/lib/dynamoid/transactions/mutation/destroy.rb +96 -0
- data/lib/dynamoid/transactions/mutation/import.rb +84 -0
- data/lib/dynamoid/transactions/mutation/inc.rb +101 -0
- data/lib/dynamoid/transactions/mutation/increment.rb +63 -0
- data/lib/dynamoid/transactions/mutation/save.rb +194 -0
- data/lib/dynamoid/transactions/mutation/touch.rb +99 -0
- data/lib/dynamoid/transactions/mutation/update_attributes.rb +49 -0
- data/lib/dynamoid/transactions/mutation/update_fields.rb +185 -0
- data/lib/dynamoid/transactions/mutation/upsert.rb +94 -0
- data/lib/dynamoid/transactions/mutation.rb +935 -0
- data/lib/dynamoid/transactions/retrieval/find.rb +140 -0
- data/lib/dynamoid/transactions/retrieval.rb +150 -0
- data/lib/dynamoid/transactions.rb +63 -0
- data/lib/dynamoid/version.rb +1 -1
- data/lib/dynamoid.rb +1 -2
- metadata +23 -17
- data/lib/dynamoid/transaction_read/find.rb +0 -137
- data/lib/dynamoid/transaction_read.rb +0 -146
- data/lib/dynamoid/transaction_write/base.rb +0 -59
- data/lib/dynamoid/transaction_write/create.rb +0 -49
- data/lib/dynamoid/transaction_write/delete_with_instance.rb +0 -65
- data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +0 -64
- data/lib/dynamoid/transaction_write/destroy.rb +0 -84
- data/lib/dynamoid/transaction_write/item_updater.rb +0 -60
- data/lib/dynamoid/transaction_write/save.rb +0 -177
- data/lib/dynamoid/transaction_write/update_attributes.rb +0 -46
- data/lib/dynamoid/transaction_write/update_fields.rb +0 -247
- data/lib/dynamoid/transaction_write/upsert.rb +0 -113
- data/lib/dynamoid/transaction_write.rb +0 -673
data/README.md
CHANGED
|
@@ -14,1596 +14,129 @@
|
|
|
14
14
|
[![Keep-A-Changelog 1.0.0][📗keep-changelog-img]][📗keep-changelog]
|
|
15
15
|
[![Sponsor Project][🖇sponsor-img]][🖇sponsor]
|
|
16
16
|
|
|
17
|
-
Dynamoid is an ORM for Amazon's DynamoDB for Ruby applications. It
|
|
18
|
-
provides similar functionality to ActiveRecord and improves on Amazon's
|
|
19
|
-
existing
|
|
20
|
-
[HashModel](http://docs.amazonwebservices.com/AWSRubySDK/latest/AWS/Record/HashModel.html)
|
|
21
|
-
by providing better searching tools and native association support.
|
|
22
17
|
|
|
23
|
-
DynamoDB
|
|
24
|
-
is very different indeed from relational databases. It sacrifices
|
|
25
|
-
anything beyond the simplest relational queries and transactional
|
|
26
|
-
support to provide a fast, cost-efficient, and highly durable storage
|
|
27
|
-
solution. If your database requires complicated relational queries
|
|
28
|
-
then this modest Gem cannot provide them for you
|
|
29
|
-
and neither can DynamoDB. In those cases you would do better to look
|
|
30
|
-
elsewhere for your database needs.
|
|
18
|
+
A feature-rich and powerful Ruby ORM for Amazon DynamoDB, designed to provide a familiar ActiveRecord-like experience for Ruby applications.
|
|
31
19
|
|
|
32
|
-
But if you want a fast, scalable, simple, easy-to-use database (and a
|
|
33
|
-
Gem that supports it) then look no further!
|
|
34
20
|
|
|
35
|
-
##
|
|
21
|
+
## Key Features
|
|
36
22
|
|
|
37
|
-
|
|
38
|
-
|
|
23
|
+
* ActiveRecord-style DSL: Implements an interface and configuration similar to Rails' ActiveRecord.
|
|
24
|
+
* Querying & Persistence: Provides methods for finding, querying, and updating models.
|
|
25
|
+
* Advanced ORM Features: Supports associations, callbacks, validations, Dirty API, optimistic locking, and type casting.
|
|
26
|
+
* Additional Attribute Types: Supports types not natively provided by Amazon DynamoDB, such as `DateTime`, `Time`, and more.
|
|
27
|
+
* Transactions: Supports Amazon DynamoDB transactional operations.
|
|
39
28
|
|
|
40
|
-
```ruby
|
|
41
|
-
gem 'dynamoid'
|
|
42
|
-
```
|
|
43
|
-
## Prerequisites
|
|
44
|
-
|
|
45
|
-
Dynamoid depends on the aws-sdk, and this is tested on the current
|
|
46
|
-
version of aws-sdk (~> 3), rails (>= 4). Hence the configuration as
|
|
47
|
-
needed for aws to work will be dealt with by aws setup.
|
|
48
|
-
|
|
49
|
-
### AWS SDK Version Compatibility
|
|
50
|
-
|
|
51
|
-
Make sure you are using the version for the right AWS SDK.
|
|
52
|
-
|
|
53
|
-
| Dynamoid version | AWS SDK Version |
|
|
54
|
-
| ---------------- | --------------- |
|
|
55
|
-
| 0.x | 1.x |
|
|
56
|
-
| 1.x | 2.x |
|
|
57
|
-
| 2.x | 2.x |
|
|
58
|
-
| 3.x | 3.x |
|
|
59
|
-
|
|
60
|
-
### AWS Configuration
|
|
61
|
-
|
|
62
|
-
Configure AWS access:
|
|
63
|
-
[Reference](https://github.com/aws/aws-sdk-ruby)
|
|
64
|
-
|
|
65
|
-
For example, to configure AWS access:
|
|
66
|
-
|
|
67
|
-
Create `config/initializers/aws.rb` as follows:
|
|
68
|
-
|
|
69
|
-
```ruby
|
|
70
|
-
Aws.config.update(
|
|
71
|
-
region: 'us-west-2',
|
|
72
|
-
credentials: Aws::Credentials.new('REPLACE_WITH_ACCESS_KEY_ID', 'REPLACE_WITH_SECRET_ACCESS_KEY'),
|
|
73
|
-
)
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Alternatively, if you don't want Aws connection settings to be
|
|
77
|
-
overwritten for you entire project, you can specify connection settings
|
|
78
|
-
for Dynamoid only, by setting those in the `Dynamoid.configure` clause:
|
|
79
|
-
|
|
80
|
-
```ruby
|
|
81
|
-
require 'dynamoid'
|
|
82
|
-
Dynamoid.configure do |config|
|
|
83
|
-
config.access_key = 'REPLACE_WITH_ACCESS_KEY_ID'
|
|
84
|
-
config.secret_key = 'REPLACE_WITH_SECRET_ACCESS_KEY'
|
|
85
|
-
config.region = 'us-west-2'
|
|
86
|
-
end
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
Additionally, if you would like to pass in pre-configured AWS credentials
|
|
90
|
-
(e.g. you have an IAM role credential, you configure your credentials
|
|
91
|
-
elsewhere in your project, etc.), you may do so:
|
|
92
|
-
|
|
93
|
-
```ruby
|
|
94
|
-
require 'dynamoid'
|
|
95
|
-
|
|
96
|
-
credentials = Aws::AssumeRoleCredentials.new(
|
|
97
|
-
region: region,
|
|
98
|
-
access_key_id: key,
|
|
99
|
-
secret_access_key: secret,
|
|
100
|
-
role_arn: role_arn,
|
|
101
|
-
role_session_name: 'our-session'
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
Dynamoid.configure do |config|
|
|
105
|
-
config.region = 'us-west-2'
|
|
106
|
-
config.credentials = credentials
|
|
107
|
-
end
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
For a full list of the DDB regions, you can go
|
|
111
|
-
[here](http://docs.aws.amazon.com/general/latest/gr/rande.html#ddb_region).
|
|
112
|
-
|
|
113
|
-
Then you need to initialize Dynamoid config to get it going. Put code
|
|
114
|
-
similar to this somewhere (a Rails initializer would be a great place
|
|
115
|
-
for this if you're using Rails):
|
|
116
|
-
|
|
117
|
-
```ruby
|
|
118
|
-
require 'dynamoid'
|
|
119
|
-
Dynamoid.configure do |config|
|
|
120
|
-
# To namespace tables created by Dynamoid from other tables you might have.
|
|
121
|
-
# Set to nil to avoid namespacing.
|
|
122
|
-
config.namespace = 'dynamoid_app_development'
|
|
123
|
-
|
|
124
|
-
# [Optional]. If provided, it communicates with the DB listening at the endpoint.
|
|
125
|
-
# This is useful for testing with [DynamoDB Local] (http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html).
|
|
126
|
-
config.endpoint = 'http://localhost:3000'
|
|
127
|
-
end
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### Ruby & Rails Compatibility
|
|
131
|
-
|
|
132
|
-
Dynamoid supports Ruby >= 2.3 and Rails >= 4.2.
|
|
133
|
-
|
|
134
|
-
Its compatibility is tested against following Ruby versions: 2.3, 2.4,
|
|
135
|
-
2.5, 2.6, 2.7, 3.0, 3.1, 3.2, 3.3, 3.4, and 4.0, JRuby 9.4.x and against Rails versions: 4.2, 5.0, 5.1,
|
|
136
|
-
5.2, 6.0, 6.1, 7.0, 7.1, 7.2, 8.0, and 8.1.
|
|
137
|
-
|
|
138
|
-
## Setup
|
|
139
|
-
|
|
140
|
-
You *must* include `Dynamoid::Document` in every Dynamoid model.
|
|
141
|
-
|
|
142
|
-
```ruby
|
|
143
|
-
class User
|
|
144
|
-
include Dynamoid::Document
|
|
145
|
-
|
|
146
|
-
# fields declaration
|
|
147
|
-
end
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### Table
|
|
151
|
-
|
|
152
|
-
Dynamoid has some sensible defaults for you when you create a new table,
|
|
153
|
-
including the table name and the primary key column. But you can change
|
|
154
|
-
those if you like on table creation.
|
|
155
|
-
|
|
156
|
-
```ruby
|
|
157
|
-
class User
|
|
158
|
-
include Dynamoid::Document
|
|
159
|
-
|
|
160
|
-
table name: :awesome_users, key: :user_id, read_capacity: 5, write_capacity: 5
|
|
161
|
-
end
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
These fields will not change an existing table: so specifying a new
|
|
165
|
-
read_capacity and write_capacity here only works correctly for entirely
|
|
166
|
-
new tables. Similarly, while Dynamoid will look for a table named
|
|
167
|
-
`awesome_users` in your namespace, it won't change any existing tables
|
|
168
|
-
to use that name; and if it does find a table with the correct name, it
|
|
169
|
-
won't change its hash key, which it expects will be `user_id`. If this
|
|
170
|
-
table doesn't exist yet, however, Dynamoid will create it with these
|
|
171
|
-
options.
|
|
172
|
-
|
|
173
|
-
There is a basic support of DynamoDB's [Time To Live (TTL)
|
|
174
|
-
mechanism](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html).
|
|
175
|
-
If you declare a field as TTL field - it will be initialised if doesn't
|
|
176
|
-
have value yet. Default value is current time + specified seconds.
|
|
177
|
-
|
|
178
|
-
```ruby
|
|
179
|
-
class User
|
|
180
|
-
include Dynamoid::Document
|
|
181
|
-
|
|
182
|
-
table expires: { field: :ttl, after: 60 }
|
|
183
|
-
|
|
184
|
-
field :ttl, :integer
|
|
185
|
-
end
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
Field used to store expiration time (e.g. `ttl`) should be declared
|
|
189
|
-
explicitly and should have numeric type (`integer`, `number`) only.
|
|
190
|
-
`datetime` type is also possible but only if it's stored as number
|
|
191
|
-
(there is a way to store time as a string also).
|
|
192
|
-
|
|
193
|
-
It's also possible to override a global option `Dynamoid::Config.timestamps`
|
|
194
|
-
on a table level:
|
|
195
|
-
|
|
196
|
-
```ruby
|
|
197
|
-
table timestamps: false
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
This option controls generation of timestamp fields
|
|
201
|
-
`created_at`/`updated_at`.
|
|
202
|
-
|
|
203
|
-
It's also possible to override table capacity mode configured globally
|
|
204
|
-
with table level option `capacity_mode`. Valid values are
|
|
205
|
-
`:provisioned`, `:on_demand` and `nil`:
|
|
206
|
-
|
|
207
|
-
```ruby
|
|
208
|
-
table capacity_mode: :on_demand
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
If table capacity mode is on-demand, another related table-level options
|
|
212
|
-
`read_capacity` and `write_capacity` will be ignored.
|
|
213
|
-
|
|
214
|
-
### Fields
|
|
215
|
-
|
|
216
|
-
You'll have to define all the fields on the model and the data type of
|
|
217
|
-
each field. Every field on the object must be included here; if you miss
|
|
218
|
-
any they'll be completely bypassed during DynamoDB's initialization and
|
|
219
|
-
will not appear on the model objects.
|
|
220
|
-
|
|
221
|
-
By default, fields are assumed to be of type `string`. Other built-in
|
|
222
|
-
types are `integer`, `number`, `set`, `array`, `map`, `datetime`,
|
|
223
|
-
`date`, `boolean`, `binary`, `raw` and `serialized`. `array` and
|
|
224
|
-
`map` match List and Map DynamoDB types respectively. `raw` type means
|
|
225
|
-
you can store Ruby Array, Hash, String and numbers. If built-in types do
|
|
226
|
-
not suit you, you can use a custom field type represented by an
|
|
227
|
-
arbitrary class, provided that the class supports a compatible
|
|
228
|
-
serialization interface. The primary use case for using a custom field
|
|
229
|
-
type is to represent your business logic with high-level types, while
|
|
230
|
-
ensuring portability or backward-compatibility of the serialized
|
|
231
|
-
representation.
|
|
232
|
-
|
|
233
|
-
#### Note on boolean type
|
|
234
|
-
|
|
235
|
-
The boolean fields are stored as DynamoDB boolean values by default.
|
|
236
|
-
Dynamoid can store boolean values as strings as well - `'t'` and `'f'`.
|
|
237
|
-
So if you want to change the default format of boolean field you can
|
|
238
|
-
easily achieve this with `store_as_native_boolean` field option:
|
|
239
|
-
|
|
240
|
-
```ruby
|
|
241
|
-
class Document
|
|
242
|
-
include Dynamoid::Document
|
|
243
|
-
|
|
244
|
-
field :active, :boolean, store_as_native_boolean: false
|
|
245
|
-
end
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
#### Note on date type
|
|
249
|
-
|
|
250
|
-
By default date fields are persisted as days count since 1 January 1970
|
|
251
|
-
like UNIX time. If you prefer dates to be stored as ISO-8601 formatted
|
|
252
|
-
strings instead then set `store_as_string` to `true`
|
|
253
|
-
|
|
254
|
-
```ruby
|
|
255
|
-
class Document
|
|
256
|
-
include Dynamoid::Document
|
|
257
|
-
|
|
258
|
-
field :sent_on, :date, store_as_string: true
|
|
259
|
-
end
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
#### Note on datetime type
|
|
263
|
-
|
|
264
|
-
By default datetime fields are persisted as UNIX timestamps with
|
|
265
|
-
millisecond precision in DynamoDB. If you prefer datetimes to be stored
|
|
266
|
-
as ISO-8601 formatted strings instead then set `store_as_string` to
|
|
267
|
-
`true`
|
|
268
|
-
|
|
269
|
-
```ruby
|
|
270
|
-
class Document
|
|
271
|
-
include Dynamoid::Document
|
|
272
|
-
|
|
273
|
-
field :sent_at, :datetime, store_as_string: true
|
|
274
|
-
end
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
**WARNING:** Fields in numeric format are stored with nanoseconds as a
|
|
278
|
-
fraction part and precision could be lost. That's why `datetime` field
|
|
279
|
-
in numeric format shouldn't be used as a range key.
|
|
280
|
-
|
|
281
|
-
You have two options if you need to use a `datetime` field as a range
|
|
282
|
-
key:
|
|
283
|
-
* string format
|
|
284
|
-
* store `datetime` values without milliseconds (e.g. cut
|
|
285
|
-
them manually with `change` method - `Time.now.change(usec: 0)`
|
|
286
|
-
|
|
287
|
-
#### Note on set type
|
|
288
|
-
|
|
289
|
-
`Dynamoid`'s type `set` is stored as DynamoDB's Set attribute type.
|
|
290
|
-
DynamoDB supports only Set of strings, numbers and binary. Moreover Set
|
|
291
|
-
*must* contain elements of the same type only.
|
|
292
|
-
|
|
293
|
-
In order to use some other `Dynamoid`'s types you can specify `of`
|
|
294
|
-
option to declare the type of set elements.
|
|
295
|
-
|
|
296
|
-
As a result of that DynamoDB limitation, in Dynamoid only the following
|
|
297
|
-
scalar types are supported (note: does not support `boolean`):
|
|
298
|
-
`integer`, `number`, `date`, `datetime`, `serializable` and custom
|
|
299
|
-
types.
|
|
300
|
-
|
|
301
|
-
```ruby
|
|
302
|
-
class Document
|
|
303
|
-
include Dynamoid::Document
|
|
304
|
-
|
|
305
|
-
field :tags, :set, of: :integer
|
|
306
|
-
end
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
It's possible to specify field options like `store_as_string` for
|
|
310
|
-
`datetime` field or `serializer` for `serializable` field for `set`
|
|
311
|
-
elements type:
|
|
312
|
-
|
|
313
|
-
```ruby
|
|
314
|
-
class Document
|
|
315
|
-
include Dynamoid::Document
|
|
316
|
-
|
|
317
|
-
field :values, :set, of: { serialized: { serializer: JSON } }
|
|
318
|
-
field :dates, :set, of: { date: { store_as_string: true } }
|
|
319
|
-
field :datetimes, :set, of: { datetime: { store_as_string: false } }
|
|
320
|
-
end
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
DynamoDB doesn't allow empty strings in fields configured as `set`.
|
|
324
|
-
Abiding by this restriction, when `Dynamoid` saves a document it removes
|
|
325
|
-
all empty strings in set fields.
|
|
326
|
-
|
|
327
|
-
#### Note on array type
|
|
328
|
-
|
|
329
|
-
`Dynamoid`'s type `array` is stored as DynamoDB's List attribute type.
|
|
330
|
-
It can contain elements of different types (in contrast to Set attribute
|
|
331
|
-
type).
|
|
332
|
-
|
|
333
|
-
If you need to store in array field elements of `datetime`, `date`,
|
|
334
|
-
`serializable` or some custom type, which DynamoDB doesn't support
|
|
335
|
-
natively, you should specify element type with `of` option:
|
|
336
|
-
|
|
337
|
-
```ruby
|
|
338
|
-
class Document
|
|
339
|
-
include Dynamoid::Document
|
|
340
|
-
|
|
341
|
-
field :dates, :array, of: :date
|
|
342
|
-
end
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
#### Note on binary type
|
|
346
|
-
|
|
347
|
-
By default binary fields are persisted as DynamoDB String value encoded
|
|
348
|
-
in the Base64 encoding. DynamoDB supports binary data natively. To use
|
|
349
|
-
it instead of String a `store_binary_as_native` field option should be
|
|
350
|
-
set:
|
|
351
|
-
|
|
352
|
-
```ruby
|
|
353
|
-
class Document
|
|
354
|
-
include Dynamoid::Document
|
|
355
|
-
|
|
356
|
-
field :image, :binary, store_binary_as_native: true
|
|
357
|
-
end
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
There is also a global config option `store_binary_as_native` that is
|
|
361
|
-
`false` by default as well.
|
|
362
|
-
|
|
363
|
-
#### Magic Columns
|
|
364
|
-
|
|
365
|
-
You get magic columns of `id` (`string`), `created_at` (`datetime`), and
|
|
366
|
-
`updated_at` (`datetime`) for free.
|
|
367
|
-
|
|
368
|
-
```ruby
|
|
369
|
-
class User
|
|
370
|
-
include Dynamoid::Document
|
|
371
|
-
|
|
372
|
-
field :name
|
|
373
|
-
field :email
|
|
374
|
-
field :rank, :integer
|
|
375
|
-
field :number, :number
|
|
376
|
-
field :joined_at, :datetime
|
|
377
|
-
field :hash, :serialized
|
|
378
|
-
end
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
#### Default Values
|
|
382
|
-
|
|
383
|
-
You can optionally set a default value on a field using either a plain
|
|
384
|
-
value or a lambda:
|
|
385
|
-
|
|
386
|
-
```ruby
|
|
387
|
-
field :actions_taken, :integer, default: 0
|
|
388
|
-
field :joined_at, :datetime, default: -> { Time.now }
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
#### Aliases
|
|
392
|
-
|
|
393
|
-
It might be helpful to define an alias for already existing field when
|
|
394
|
-
naming convention used for a table differs from conventions common in
|
|
395
|
-
Ruby:
|
|
396
|
-
|
|
397
|
-
```ruby
|
|
398
|
-
field firstName, :string, alias: :first_name
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
This way there will be generated
|
|
402
|
-
setters/getters/`<name>?`/`<name>_before_type_cast` methods for both
|
|
403
|
-
original field name (`firstName`) and an alias (`first_name`).
|
|
404
|
-
|
|
405
|
-
```ruby
|
|
406
|
-
user = User.new(first_name: 'Michael')
|
|
407
|
-
user.first_name # => 'Michael'
|
|
408
|
-
user.firstName # => 'Michael'
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
#### Custom Types
|
|
412
|
-
|
|
413
|
-
To use a custom type for a field, suppose you have a `Money` type.
|
|
414
|
-
|
|
415
|
-
```ruby
|
|
416
|
-
class Money
|
|
417
|
-
# ... your business logic ...
|
|
418
|
-
|
|
419
|
-
def dynamoid_dump
|
|
420
|
-
'serialized representation as a string'
|
|
421
|
-
end
|
|
422
|
-
|
|
423
|
-
def self.dynamoid_load(_serialized_str)
|
|
424
|
-
# parse serialized representation and return a Money instance
|
|
425
|
-
Money.new(1.23)
|
|
426
|
-
end
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
class User
|
|
430
|
-
include Dynamoid::Document
|
|
431
|
-
|
|
432
|
-
field :balance, Money
|
|
433
|
-
end
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
If you want to use a third-party class (which does not support
|
|
437
|
-
`#dynamoid_dump` and `.dynamoid_load`) as your field type, you can use
|
|
438
|
-
an adapter class providing `.dynamoid_dump` and `.dynamoid_load` class
|
|
439
|
-
methods for your third-party class. `.dynamoid_load` can remain the same
|
|
440
|
-
from the previous example; here we just add a level of indirection for
|
|
441
|
-
serializing. Example:
|
|
442
|
-
|
|
443
|
-
```ruby
|
|
444
|
-
# Third-party Money class
|
|
445
|
-
class Money; end
|
|
446
|
-
|
|
447
|
-
class MoneyAdapter
|
|
448
|
-
def self.dynamoid_load(_money_serialized_str)
|
|
449
|
-
Money.new(1.23)
|
|
450
|
-
end
|
|
451
|
-
|
|
452
|
-
def self.dynamoid_dump(money_obj)
|
|
453
|
-
money_obj.value.to_s
|
|
454
|
-
end
|
|
455
|
-
end
|
|
456
|
-
|
|
457
|
-
class User
|
|
458
|
-
include Dynamoid::Document
|
|
459
|
-
|
|
460
|
-
field :balance, MoneyAdapter
|
|
461
|
-
end
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
Lastly, you can control the data type of your custom-class-backed field
|
|
465
|
-
at the DynamoDB level. This is especially important if you want to use
|
|
466
|
-
your custom field as a numeric range or for number-oriented queries. By
|
|
467
|
-
default custom fields are persisted as a string attribute, but your
|
|
468
|
-
custom class can override this with a `.dynamoid_field_type` class
|
|
469
|
-
method, which would return either `:string` or `:number`.
|
|
470
|
-
|
|
471
|
-
DynamoDB may support some other attribute types that are not yet
|
|
472
|
-
supported by Dynamoid.
|
|
473
|
-
|
|
474
|
-
If a custom type implements `#==` method you can specify `comparable:
|
|
475
|
-
true` option in a field declaration to specify that an object is safely
|
|
476
|
-
comparable for the purpose of detecting changes. By default old and new
|
|
477
|
-
objects will be compared by their serialized representation.
|
|
478
|
-
|
|
479
|
-
```ruby
|
|
480
|
-
class Money
|
|
481
|
-
# ...
|
|
482
|
-
|
|
483
|
-
def ==(other)
|
|
484
|
-
# comparison logic
|
|
485
|
-
end
|
|
486
|
-
end
|
|
487
|
-
|
|
488
|
-
class User
|
|
489
|
-
# ...
|
|
490
|
-
|
|
491
|
-
field :balance, Money, comparable: true
|
|
492
|
-
end
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
### Sort key
|
|
496
|
-
|
|
497
|
-
Along with partition key table may have a sort key. In order to declare
|
|
498
|
-
it in a model `range` class method should be used:
|
|
499
|
-
|
|
500
|
-
```ruby
|
|
501
|
-
class Post
|
|
502
|
-
include Dynamoid::Document
|
|
503
|
-
|
|
504
|
-
range :posted_at, :datetime
|
|
505
|
-
end
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
Second argument, type, is optional. Default type is `string`.
|
|
509
|
-
|
|
510
|
-
### Associations
|
|
511
|
-
|
|
512
|
-
Just like in ActiveRecord (or your other favorite ORM), Dynamoid uses
|
|
513
|
-
associations to create links between models.
|
|
514
|
-
|
|
515
|
-
**WARNING:** Associations are not supported for models with compound
|
|
516
|
-
primary key. If a model declares a range key it should not declare any
|
|
517
|
-
association itself and be referenced by an association in another model.
|
|
518
|
-
|
|
519
|
-
The only supported associations (so far) are `has_many`, `has_one`,
|
|
520
|
-
`has_and_belongs_to_many`, and `belongs_to`. Associations are very
|
|
521
|
-
simple to create: just specify the type, the name, and then any options
|
|
522
|
-
you'd like to pass to the association. If there's an inverse association
|
|
523
|
-
either inferred or specified directly, Dynamoid will update both objects
|
|
524
|
-
to point at each other.
|
|
525
|
-
|
|
526
|
-
```ruby
|
|
527
|
-
class User
|
|
528
|
-
include Dynamoid::Document
|
|
529
|
-
|
|
530
|
-
# ...
|
|
531
|
-
|
|
532
|
-
has_many :addresses
|
|
533
|
-
has_many :students, class: User
|
|
534
|
-
belongs_to :teacher, class_name: :user
|
|
535
|
-
belongs_to :group
|
|
536
|
-
belongs_to :group, foreign_key: :group_id
|
|
537
|
-
has_one :role
|
|
538
|
-
has_and_belongs_to_many :friends, inverse_of: :friending_users
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
class Address
|
|
542
|
-
include Dynamoid::Document
|
|
543
|
-
|
|
544
|
-
# ...
|
|
545
|
-
|
|
546
|
-
belongs_to :user # Automatically links up with the user model
|
|
547
|
-
end
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
Contrary to what you'd expect, association information is always
|
|
551
|
-
contained on the object specifying the association, even if it seems
|
|
552
|
-
like the association has a foreign key. This is a side effect of
|
|
553
|
-
DynamoDB's structure: it's very difficult to find foreign keys without
|
|
554
|
-
an index. Usually you won't find this to be a problem, but it does mean
|
|
555
|
-
that association methods that build new models will not work correctly -
|
|
556
|
-
for example, `user.addresses.new` returns an address that is not
|
|
557
|
-
associated to the user. We'll be correcting this ~soon~ maybe someday,
|
|
558
|
-
if we get a pull request.
|
|
559
|
-
|
|
560
|
-
### Validations
|
|
561
|
-
|
|
562
|
-
Dynamoid bakes in ActiveModel validations, just like ActiveRecord does.
|
|
563
|
-
|
|
564
|
-
```ruby
|
|
565
|
-
class User
|
|
566
|
-
include Dynamoid::Document
|
|
567
|
-
|
|
568
|
-
# ...
|
|
569
|
-
|
|
570
|
-
validates_presence_of :name
|
|
571
|
-
validates_format_of :email, with: /@/
|
|
572
|
-
end
|
|
573
|
-
```
|
|
574
|
-
|
|
575
|
-
To see more usage and examples of ActiveModel validations, check out the
|
|
576
|
-
[ActiveModel validation
|
|
577
|
-
documentation](http://api.rubyonrails.org/classes/ActiveModel/Validations.html).
|
|
578
|
-
|
|
579
|
-
If you want to bypass model validation, pass `validate: false` to `save`
|
|
580
|
-
call:
|
|
581
|
-
|
|
582
|
-
```ruby
|
|
583
|
-
model.save(validate: false)
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
### Callbacks
|
|
587
|
-
|
|
588
|
-
Dynamoid also employs ActiveModel callbacks. Right now the following
|
|
589
|
-
callbacks are supported:
|
|
590
|
-
- `save` (before, after, around)
|
|
591
|
-
- `create` (before, after, around)
|
|
592
|
-
- `update` (before, after, around)
|
|
593
|
-
- `validation` (before, after)
|
|
594
|
-
- `destroy` (before, after, around)
|
|
595
|
-
- `after_touch`
|
|
596
|
-
- `after_initialize`
|
|
597
|
-
- `after_find`
|
|
598
|
-
|
|
599
|
-
Example:
|
|
600
|
-
|
|
601
|
-
```ruby
|
|
602
|
-
class User
|
|
603
|
-
include Dynamoid::Document
|
|
604
|
-
|
|
605
|
-
# ...
|
|
606
|
-
|
|
607
|
-
before_save :set_default_password
|
|
608
|
-
after_create :notify_friends
|
|
609
|
-
after_destroy :delete_addresses
|
|
610
|
-
end
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
### STI
|
|
614
|
-
|
|
615
|
-
Dynamoid supports STI (Single Table Inheritance) like Active Record
|
|
616
|
-
does. You need just specify `type` field in a base class. Example:
|
|
617
|
-
|
|
618
|
-
```ruby
|
|
619
|
-
class Animal
|
|
620
|
-
include Dynamoid::Document
|
|
621
|
-
|
|
622
|
-
field :name
|
|
623
|
-
field :type
|
|
624
|
-
end
|
|
625
|
-
|
|
626
|
-
class Cat < Animal
|
|
627
|
-
field :lives, :integer
|
|
628
|
-
end
|
|
629
|
-
|
|
630
|
-
cat = Cat.create(name: 'Morgan')
|
|
631
|
-
animal = Animal.find(cat.id)
|
|
632
|
-
animal.class
|
|
633
|
-
#=> Cat
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
If you already have DynamoDB tables and `type` field already exists and
|
|
637
|
-
has its own semantic it leads to conflict. It's possible to tell
|
|
638
|
-
Dynamoid to use another field (even not existing) instead of `type` one
|
|
639
|
-
with `inheritance_field` table option:
|
|
640
|
-
|
|
641
|
-
```ruby
|
|
642
|
-
class Car
|
|
643
|
-
include Dynamoid::Document
|
|
644
|
-
|
|
645
|
-
table inheritance_field: :my_new_type
|
|
646
|
-
|
|
647
|
-
field :my_new_type
|
|
648
|
-
end
|
|
649
|
-
|
|
650
|
-
c = Car.create
|
|
651
|
-
c.my_new_type
|
|
652
|
-
#=> "Car"
|
|
653
|
-
```
|
|
654
|
-
|
|
655
|
-
### Type casting
|
|
656
|
-
|
|
657
|
-
Dynamoid supports type casting and tries to do it in the most convenient
|
|
658
|
-
way. Values for all fields (except custom type) are coerced to declared
|
|
659
|
-
field types.
|
|
660
|
-
|
|
661
|
-
Some obvious rules are used, e.g.:
|
|
662
|
-
|
|
663
|
-
for boolean field:
|
|
664
|
-
```ruby
|
|
665
|
-
document.boolean_field = 'off'
|
|
666
|
-
# => false
|
|
667
|
-
document.boolean_field = 'false'
|
|
668
|
-
# => false
|
|
669
|
-
document.boolean_field = 'some string'
|
|
670
|
-
# => true
|
|
671
|
-
```
|
|
672
|
-
|
|
673
|
-
or for integer field:
|
|
674
|
-
```ruby
|
|
675
|
-
document.integer_field = 42.3
|
|
676
|
-
# => 42
|
|
677
|
-
document.integer_field = '42.3'
|
|
678
|
-
# => 42
|
|
679
|
-
document.integer_field = true
|
|
680
|
-
# => 1
|
|
681
|
-
```
|
|
682
|
-
|
|
683
|
-
If time zone isn't specified for `datetime` value - application time
|
|
684
|
-
zone is used.
|
|
685
|
-
|
|
686
|
-
To access field value before type casting following method could be
|
|
687
|
-
used: `attributes_before_type_cast` and
|
|
688
|
-
`read_attribute_before_type_cast`.
|
|
689
|
-
|
|
690
|
-
There is `<name>_before_type_cast` method for every field in a model as
|
|
691
|
-
well.
|
|
692
|
-
|
|
693
|
-
### Dirty API
|
|
694
|
-
|
|
695
|
-
Dynamoid supports Dirty API which is equivalent to [Rails 5.2
|
|
696
|
-
`ActiveModel::Dirty`](https://api.rubyonrails.org/v5.2/classes/ActiveModel/Dirty.html).
|
|
697
|
-
There is only one limitation - change in place of field isn't detected
|
|
698
|
-
automatically.
|
|
699
|
-
|
|
700
|
-
## Usage
|
|
701
|
-
|
|
702
|
-
### Object Creation
|
|
703
|
-
|
|
704
|
-
Dynamoid's syntax is generally very similar to ActiveRecord's. Making
|
|
705
|
-
new objects is simple:
|
|
706
|
-
|
|
707
|
-
```ruby
|
|
708
|
-
u = User.new(name: 'Josh')
|
|
709
|
-
u.email = 'josh@joshsymonds.com'
|
|
710
|
-
u.save
|
|
711
|
-
```
|
|
712
|
-
|
|
713
|
-
Save forces persistence to the data store: a unique ID is also assigned,
|
|
714
|
-
but it is a string and not an auto-incrementing number.
|
|
715
|
-
|
|
716
|
-
```ruby
|
|
717
|
-
u.id # => '3a9f7216-4726-4aea-9fbc-8554ae9292cb'
|
|
718
|
-
```
|
|
719
|
-
|
|
720
|
-
To use associations, you use association methods very similar to
|
|
721
|
-
ActiveRecord's:
|
|
722
|
-
|
|
723
|
-
```ruby
|
|
724
|
-
address = u.addresses.create
|
|
725
|
-
address.city = 'Chicago'
|
|
726
|
-
address.save
|
|
727
|
-
```
|
|
728
|
-
|
|
729
|
-
To create multiple documents at once:
|
|
730
|
-
|
|
731
|
-
```ruby
|
|
732
|
-
User.create([{ name: 'Josh' }, { name: 'Nick' }])
|
|
733
|
-
```
|
|
734
|
-
|
|
735
|
-
There is an efficient and low-level way to create multiple documents
|
|
736
|
-
(without validation and callbacks running):
|
|
737
|
-
|
|
738
|
-
```ruby
|
|
739
|
-
users = User.import([{ name: 'Josh' }, { name: 'Nick' }])
|
|
740
|
-
```
|
|
741
|
-
|
|
742
|
-
### Querying
|
|
743
|
-
|
|
744
|
-
Querying can be done in one of the following ways:
|
|
745
|
-
|
|
746
|
-
```ruby
|
|
747
|
-
Address.find(address.id) # Find directly by ID.
|
|
748
|
-
Address.where(city: 'Chicago').all # Find by any number of matching criteria...
|
|
749
|
-
# Though presently only "where" is supported.
|
|
750
|
-
Address.find_by_city('Chicago') # The same as above, but using ActiveRecord's older syntax.
|
|
751
|
-
```
|
|
752
|
-
|
|
753
|
-
There is also a way to `#where` with a condition expression:
|
|
754
|
-
|
|
755
|
-
```ruby
|
|
756
|
-
Address.where('city = :c', c: 'Chicago')
|
|
757
|
-
```
|
|
758
|
-
|
|
759
|
-
A condition expression may contain operators (e.g. `<`, `>=`, `<>`),
|
|
760
|
-
keywords (e.g. `AND`, `OR`, `BETWEEN`) and built-in functions (e.g.
|
|
761
|
-
`begins_with`, `contains`) (see (documentation
|
|
762
|
-
)[https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html]
|
|
763
|
-
for full syntax description).
|
|
764
|
-
|
|
765
|
-
**Warning:** Values (specified for a String condition expression) are
|
|
766
|
-
sent as is so Dynamoid field types that aren't supported natively by
|
|
767
|
-
DynamoDB (e.g. `datetime` and `date`) require explicit casting.
|
|
768
|
-
|
|
769
|
-
**Warning:** String condition expressions will be used by DynamoDB only
|
|
770
|
-
at filtering, so conditions on key attributes should be specified as a
|
|
771
|
-
Hash to perform Query operation instead of Scan. Don't use key
|
|
772
|
-
attributes in `#where`'s String condition expressions.
|
|
773
|
-
|
|
774
|
-
And you can also query on associations:
|
|
775
|
-
|
|
776
|
-
```ruby
|
|
777
|
-
u.addresses.where(city: 'Chicago').all
|
|
778
|
-
```
|
|
779
|
-
|
|
780
|
-
But keep in mind Dynamoid - and document-based storage systems in
|
|
781
|
-
general - are not drop-in replacements for existing relational
|
|
782
|
-
databases. The above query does not efficiently perform a conditional
|
|
783
|
-
join, but instead finds all the user's addresses and naively filters
|
|
784
|
-
them in Ruby. For large associations this is a performance hit compared
|
|
785
|
-
to relational database engines.
|
|
786
|
-
|
|
787
|
-
**Warning:** There is a caveat with filtering documents by `nil` value
|
|
788
|
-
attribute. By default Dynamoid ignores attributes with `nil` value and
|
|
789
|
-
doesn't store them in a DynamoDB document. This behavior could be
|
|
790
|
-
changed with `store_attribute_with_nil_value` config option.
|
|
791
|
-
|
|
792
|
-
If Dynamoid ignores `nil` value attributes `null`/`not_null` operators
|
|
793
|
-
should be used in query:
|
|
794
|
-
|
|
795
|
-
```ruby
|
|
796
|
-
Address.where('postcode.null': true)
|
|
797
|
-
Address.where('postcode.not_null': true)
|
|
798
|
-
```
|
|
799
|
-
|
|
800
|
-
If Dynamoid keeps `nil` value attributes `eq`/`ne` operators should be
|
|
801
|
-
used instead:
|
|
802
|
-
|
|
803
|
-
```ruby
|
|
804
|
-
Address.where(postcode: nil)
|
|
805
|
-
Address.where('postcode.ne': nil)
|
|
806
|
-
```
|
|
807
|
-
|
|
808
|
-
#### Limits
|
|
809
|
-
|
|
810
|
-
There are three types of limits that you can query with:
|
|
811
|
-
|
|
812
|
-
1. `record_limit` - The number of evaluated records that are returned by
|
|
813
|
-
the query.
|
|
814
|
-
2. `scan_limit` - The number of scanned records that DynamoDB will look
|
|
815
|
-
at before returning.
|
|
816
|
-
3. `batch_size` - The number of records requested to DynamoDB per
|
|
817
|
-
underlying request, good for large queries!
|
|
818
|
-
|
|
819
|
-
Using these in various combinations results in the underlying requests
|
|
820
|
-
to be made in the smallest size possible and the query returns once
|
|
821
|
-
`record_limit` or `scan_limit` is satisfied. It will attempt to batch
|
|
822
|
-
whenever possible.
|
|
823
|
-
|
|
824
|
-
You can thus limit the number of evaluated records, or select a record
|
|
825
|
-
from which to start in order to support pagination.
|
|
826
|
-
|
|
827
|
-
```ruby
|
|
828
|
-
Address.record_limit(5).start(address) # Only 5 addresses starting at `address`
|
|
829
|
-
```
|
|
830
|
-
Where `address` is an instance of the model or a hash
|
|
831
|
-
`{the_model_hash_key: 'value', the_model_range_key: 'value'}`. Keep in
|
|
832
|
-
mind that if you are passing a hash to `.start()` you need to explicitly
|
|
833
|
-
define all required keys in it including range keys, depending on table
|
|
834
|
-
or secondary indexes signatures, otherwise you'll get an
|
|
835
|
-
`Aws::DynamoDB::Errors::ValidationException` either for `Exclusive Start
|
|
836
|
-
Key must have same size as table's key schema` or `The provided starting
|
|
837
|
-
key is invalid`
|
|
838
|
-
|
|
839
|
-
If you are potentially running over a large data set and this is
|
|
840
|
-
especially true when using certain filters, you may want to consider
|
|
841
|
-
limiting the number of scanned records (the number of records DynamoDB
|
|
842
|
-
infrastructure looks through when evaluating data to return):
|
|
843
|
-
|
|
844
|
-
```ruby
|
|
845
|
-
Address.scan_limit(5).start(address) # Only scan at most 5 records and return what's found starting from `address`
|
|
846
|
-
```
|
|
847
|
-
|
|
848
|
-
For large queries that return many rows, Dynamoid can use AWS' support
|
|
849
|
-
for requesting documents in batches:
|
|
850
|
-
|
|
851
|
-
```ruby
|
|
852
|
-
# Do some maintenance on the entire table without flooding DynamoDB
|
|
853
|
-
Address.batch(100).each { |addr| addr.do_some_work && sleep(0.01) }
|
|
854
|
-
Address.record_limit(10_000).batch(100).each { |addr| addr.do_some_work && sleep(0.01) } # Batch specified as part of a chain
|
|
855
|
-
```
|
|
856
|
-
|
|
857
|
-
The implication of batches is that the underlying requests are done in
|
|
858
|
-
the batch sizes to make the request and responses more manageable. Note
|
|
859
|
-
that this batching is for `Query` and `Scans` and not `BatchGetItem`
|
|
860
|
-
commands.
|
|
861
|
-
|
|
862
|
-
#### DynamoDB pagination
|
|
863
29
|
|
|
864
|
-
|
|
865
|
-
pagination](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html#Query.Pagination)
|
|
866
|
-
instead of fixed pages sizes. Each page results in a single Query or
|
|
867
|
-
Scan call to DynamoDB, but returns an unknown number of records.
|
|
30
|
+
## Quick start
|
|
868
31
|
|
|
869
|
-
Access to the native DynamoDB pages can be obtained via the
|
|
870
|
-
`find_by_pages` method, which yields arrays of records.
|
|
871
32
|
|
|
872
|
-
|
|
873
|
-
Address.find_by_pages do |addresses, metadata|
|
|
874
|
-
end
|
|
875
|
-
```
|
|
876
|
-
|
|
877
|
-
Each yielded pages returns page metadata as the second argument, which
|
|
878
|
-
is a hash including a key `:last_evaluated_key`. The value of this key
|
|
879
|
-
can be used for the `start` method to fetch the next page of records.
|
|
880
|
-
|
|
881
|
-
This way it can be used for instance to implement efficiently pagination
|
|
882
|
-
in web-applications:
|
|
883
|
-
|
|
884
|
-
```ruby
|
|
885
|
-
class UserController < ApplicationController
|
|
886
|
-
def index
|
|
887
|
-
next_page = params[:next_page_token] ? JSON.parse(Base64.decode64(params[:next_page_token])) : nil
|
|
888
|
-
|
|
889
|
-
records, metadata = User.start(next_page).find_by_pages.first
|
|
890
|
-
|
|
891
|
-
render json: {
|
|
892
|
-
records: records,
|
|
893
|
-
next_page_token: Base64.encode64(metadata[:last_evaluated_key].to_json)
|
|
894
|
-
}
|
|
895
|
-
end
|
|
896
|
-
end
|
|
897
|
-
```
|
|
898
|
-
|
|
899
|
-
#### Sort Conditions and Filters
|
|
900
|
-
|
|
901
|
-
You are able to optimize query with condition for sort key. Following
|
|
902
|
-
operators are available: `gt`, `lt`, `gte`, `lte`, `begins_with`,
|
|
903
|
-
`between` as well as equality:
|
|
904
|
-
|
|
905
|
-
```ruby
|
|
906
|
-
Address.where(latitude: 10_212)
|
|
907
|
-
Address.where('latitude.gt': 10_212)
|
|
908
|
-
Address.where('latitude.lt': 10_212)
|
|
909
|
-
Address.where('latitude.gte': 10_212)
|
|
910
|
-
Address.where('latitude.lte': 10_212)
|
|
911
|
-
Address.where('city.begins_with': 'Lon')
|
|
912
|
-
Address.where('latitude.between': [10_212, 20_000])
|
|
913
|
-
```
|
|
914
|
-
|
|
915
|
-
You are able to filter results on the DynamoDB side and specify
|
|
916
|
-
conditions for non-key fields. Following additional operators are
|
|
917
|
-
available: `in`, `contains`, `not_contains`, `null`, `not_null`:
|
|
918
|
-
|
|
919
|
-
```ruby
|
|
920
|
-
Address.where('city.in': %w[London Edenburg Birmingham])
|
|
921
|
-
Address.where('city.contains': ['on'])
|
|
922
|
-
Address.where('city.not_contains': ['ing'])
|
|
923
|
-
Address.where('postcode.null': false)
|
|
924
|
-
Address.where('postcode.not_null': true)
|
|
925
|
-
```
|
|
926
|
-
|
|
927
|
-
**WARNING:** Please take into account that `NULL` and `NOT_NULL`
|
|
928
|
-
operators check attribute presence in a document, not value. So if
|
|
929
|
-
attribute `postcode`'s value is `NULL`, `NULL` operator will return
|
|
930
|
-
false because attribute exists even if has `NULL` value.
|
|
931
|
-
|
|
932
|
-
#### Selecting some specific fields only
|
|
933
|
-
|
|
934
|
-
It could be done with `project` method:
|
|
935
|
-
|
|
936
|
-
```ruby
|
|
937
|
-
class User
|
|
938
|
-
include Dynamoid::Document
|
|
939
|
-
|
|
940
|
-
field :name
|
|
941
|
-
end
|
|
942
|
-
|
|
943
|
-
User.create(name: 'Alex')
|
|
944
|
-
user = User.project(:name).first
|
|
945
|
-
|
|
946
|
-
user.id # => nil
|
|
947
|
-
user.name # => 'Alex'
|
|
948
|
-
user.created_at # => nil
|
|
949
|
-
```
|
|
33
|
+
### Installation
|
|
950
34
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
Several fields could be specified:
|
|
954
|
-
|
|
955
|
-
```ruby
|
|
956
|
-
user = User.project(:name, :created_at)
|
|
957
|
-
```
|
|
958
|
-
|
|
959
|
-
### Consistent Reads
|
|
960
|
-
|
|
961
|
-
Querying supports consistent reading. By default, DynamoDB reads are
|
|
962
|
-
eventually consistent: if you do a write and then a read immediately
|
|
963
|
-
afterwards, the results of the previous write may not be reflected. If
|
|
964
|
-
you need to do a consistent read (that is, you need to read the results
|
|
965
|
-
of a write immediately) you can do so, but keep in mind that consistent
|
|
966
|
-
reads are twice as expensive as regular reads for DynamoDB.
|
|
967
|
-
|
|
968
|
-
```ruby
|
|
969
|
-
Address.find(address.id, consistent_read: true) # Find an address, ensure the read is consistent.
|
|
970
|
-
Address.where(city: 'Chicago').consistent.all # Find all addresses where the city is Chicago, with a consistent read.
|
|
971
|
-
```
|
|
972
|
-
|
|
973
|
-
### Range Finding
|
|
974
|
-
|
|
975
|
-
If you have a range index, Dynamoid provides a number of additional
|
|
976
|
-
other convenience methods to make your life a little easier:
|
|
977
|
-
|
|
978
|
-
```ruby
|
|
979
|
-
User.where('created_at.gt': DateTime.now - 1.day).all
|
|
980
|
-
User.where('created_at.lt': DateTime.now - 1.day).all
|
|
981
|
-
```
|
|
982
|
-
|
|
983
|
-
It also supports `gte` and `lte`. Turning those into symbols and
|
|
984
|
-
allowing a Rails SQL-style string syntax is in the works. You can only
|
|
985
|
-
have one range argument per query, because of DynamoDB inherent
|
|
986
|
-
limitations, so use it sensibly!
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
### Updating
|
|
990
|
-
|
|
991
|
-
In order to update document you can use high level methods
|
|
992
|
-
`#update_attributes`, `#update_attribute` and `.update`. They run
|
|
993
|
-
validation and callbacks.
|
|
35
|
+
Add Dynamoid to your `Gemfile`:
|
|
994
36
|
|
|
995
37
|
```ruby
|
|
996
|
-
|
|
997
|
-
Address.find(id).update_attribute(:city, 'Chicago')
|
|
998
|
-
Address.update(id, city: 'Chicago')
|
|
38
|
+
gem 'dynamoid'
|
|
999
39
|
```
|
|
1000
40
|
|
|
1001
|
-
|
|
1002
|
-
`.upsert`. They don't run validation and callbacks (except `#update` -
|
|
1003
|
-
it runs `update` callbacks). All of them support conditional updates.
|
|
1004
|
-
`#upsert` will create new document if document with specified `id`
|
|
1005
|
-
doesn't exist.
|
|
41
|
+
Or install it using `bundle`:
|
|
1006
42
|
|
|
1007
|
-
```
|
|
1008
|
-
|
|
1009
|
-
i.set city: 'Chicago'
|
|
1010
|
-
i.add latitude: 100
|
|
1011
|
-
i.delete set_of_numbers: 10
|
|
1012
|
-
end
|
|
1013
|
-
Address.find(id).update(if: { deliverable: true }) do |i|
|
|
1014
|
-
i.set city: 'Chicago'
|
|
1015
|
-
end
|
|
1016
|
-
Address.update_fields(id, city: 'Chicago')
|
|
1017
|
-
Address.update_fields(id, { city: 'Chicago' }, if: { deliverable: true })
|
|
1018
|
-
Address.upsert(id, city: 'Chicago')
|
|
1019
|
-
Address.upsert(id, { city: 'Chicago' }, if: { deliverable: true })
|
|
43
|
+
```shell
|
|
44
|
+
bundle add dynamoid
|
|
1020
45
|
```
|
|
1021
46
|
|
|
1022
|
-
|
|
1023
|
-
To idempotently create-but-not-update a record, apply the `unless_exists` condition
|
|
1024
|
-
to its keys when you upsert.
|
|
47
|
+
Alternatively, you can install the gem manually:
|
|
1025
48
|
|
|
1026
|
-
```
|
|
1027
|
-
|
|
49
|
+
```shell
|
|
50
|
+
gem install dynamoid
|
|
1028
51
|
```
|
|
1029
52
|
|
|
1030
|
-
### Deleting
|
|
1031
|
-
|
|
1032
|
-
In order to delete some items `delete_all` method should be used. Any
|
|
1033
|
-
callback won't be called. Items delete in efficient way in batch.
|
|
1034
|
-
|
|
1035
|
-
```ruby
|
|
1036
|
-
Address.where(city: 'London').delete_all
|
|
1037
|
-
```
|
|
1038
53
|
|
|
1039
|
-
###
|
|
54
|
+
### Usage
|
|
1040
55
|
|
|
1041
|
-
|
|
56
|
+
To define a model, include `Dynamoid::Document` and declare your fields. Dynamoid supports ActiveModel validations and automatic timestamps:
|
|
1042
57
|
|
|
1043
58
|
```ruby
|
|
1044
59
|
class User
|
|
1045
60
|
include Dynamoid::Document
|
|
1046
61
|
|
|
1047
|
-
field :name
|
|
1048
|
-
field :
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
end
|
|
1052
|
-
```
|
|
1053
|
-
|
|
1054
|
-
There are the following options:
|
|
1055
|
-
* `hash_key` - is used as hash key of an index,
|
|
1056
|
-
* `range_key` - is used as range key of an index,
|
|
1057
|
-
* `projected_attributes` - list of fields to store in an index or has a
|
|
1058
|
-
predefined value `:keys_only`, `:all`; `:keys_only` is a default,
|
|
1059
|
-
* `name` - an index will be created with this name when a table is
|
|
1060
|
-
created; by default name is generated and contains table name and keys
|
|
1061
|
-
names,
|
|
1062
|
-
* `read_capacity` - is used when table created and used as an index
|
|
1063
|
-
capacity; by default equals `Dynamoid::Config.read_capacity`,
|
|
1064
|
-
* `write_capacity` - is used when table created and used as an index
|
|
1065
|
-
capacity; by default equals `Dynamoid::Config.write_capacity`
|
|
1066
|
-
|
|
1067
|
-
The only mandatory option is `name`.
|
|
1068
|
-
|
|
1069
|
-
**WARNING:** In order to use global secondary index in `Document.where`
|
|
1070
|
-
implicitly you need to have all the attributes of the original table in
|
|
1071
|
-
the index and declare it with option `projected_attributes: :all`:
|
|
1072
|
-
|
|
1073
|
-
```ruby
|
|
1074
|
-
class User
|
|
1075
|
-
# ...
|
|
1076
|
-
|
|
1077
|
-
global_secondary_index hash_key: :age, projected_attributes: :all
|
|
1078
|
-
end
|
|
1079
|
-
```
|
|
1080
|
-
|
|
1081
|
-
There is only one implicit way to query Global and Local Secondary
|
|
1082
|
-
Indexes (GSI/LSI).
|
|
1083
|
-
|
|
1084
|
-
#### Implicit
|
|
1085
|
-
|
|
1086
|
-
The second way implicitly uses your GSI through the `where` clauses and
|
|
1087
|
-
deduces the index based on the query fields provided. Another added
|
|
1088
|
-
benefit is that it is built into query chaining so you can use all the
|
|
1089
|
-
methods used in normal querying. The explicit way from above would be
|
|
1090
|
-
rewritten as follows:
|
|
1091
|
-
|
|
1092
|
-
```ruby
|
|
1093
|
-
where(dynamo_primary_key_column_name => dynamo_primary_key_value,
|
|
1094
|
-
"#{range_column}.#{range_modifier}" => range_value)
|
|
1095
|
-
.scan_index_forward(false)
|
|
1096
|
-
```
|
|
1097
|
-
|
|
1098
|
-
The only caveat with this method is that because it is also used for
|
|
1099
|
-
general querying, it WILL NOT use a GSI unless it explicitly has defined
|
|
1100
|
-
`projected_attributes: :all` on the GSI in your model. This is because
|
|
1101
|
-
GSIs that do not have all attributes projected will only contain the
|
|
1102
|
-
index keys and therefore will not return objects with fully resolved
|
|
1103
|
-
field values. It currently opts to provide the complete results rather
|
|
1104
|
-
than partial results unless you've explicitly looked up the data.
|
|
1105
|
-
|
|
1106
|
-
*Future TODO could involve implementing `select` in chaining as well as
|
|
1107
|
-
resolving the fields with a second query against the table since a query
|
|
1108
|
-
against GSI then a query on base table is still likely faster than scan
|
|
1109
|
-
on the base table*
|
|
1110
|
-
|
|
1111
|
-
### Transactions in Dynamoid
|
|
1112
|
-
|
|
1113
|
-
> [!WARNING]
|
|
1114
|
-
> Please note that this API is experimental and can be changed in
|
|
1115
|
-
> future releases.
|
|
1116
|
-
|
|
1117
|
-
DynamoDB supports modifying and reading operations but there are some
|
|
1118
|
-
limitations:
|
|
1119
|
-
- read and write operation cannot be combined in the same transaction
|
|
1120
|
-
- operations are executed in batch, so operations should be given before
|
|
1121
|
-
actual execution and cannot be changed on the fly
|
|
1122
|
-
|
|
1123
|
-
#### Modifying transactions
|
|
1124
|
-
|
|
1125
|
-
Multiple modifying actions can be grouped together and submitted as an
|
|
1126
|
-
all-or-nothing operation. Atomic modifying operations are supported in
|
|
1127
|
-
Dynamoid using transactions. If any action in the transaction fails they
|
|
1128
|
-
all fail.
|
|
1129
|
-
|
|
1130
|
-
The following actions are supported:
|
|
1131
|
-
|
|
1132
|
-
* `#create`/`#create!` - add a new model if it does not already exist
|
|
1133
|
-
* `#save`/`#save!` - create or update model
|
|
1134
|
-
* `#update_attributes`/`#update_attributes!` - modifies one or more attributes from an existing
|
|
1135
|
-
model
|
|
1136
|
-
* `#delete` - remove an model without callbacks nor validations
|
|
1137
|
-
* `#destroy`/`#destroy!` - remove an model
|
|
1138
|
-
* `#upsert` - add a new model or update an existing one, no callbacks
|
|
1139
|
-
* `#update_fields` - update a model without its instantiation
|
|
1140
|
-
|
|
1141
|
-
These methods are supposed to behave exactly like their
|
|
1142
|
-
non-transactional counterparts.
|
|
1143
|
-
|
|
1144
|
-
##### Create models
|
|
1145
|
-
|
|
1146
|
-
Models can be created inside of a transaction. The partition and sort
|
|
1147
|
-
keys, if applicable, are used to determine uniqueness. Creating will
|
|
1148
|
-
fail with `Aws::DynamoDB::Errors::TransactionCanceledException` if a
|
|
1149
|
-
model already exists.
|
|
1150
|
-
|
|
1151
|
-
This example creates a user with a unique id and unique email address by
|
|
1152
|
-
creating 2 models. An additional model is upserted in the same
|
|
1153
|
-
transaction. Upsert will update `updated_at` but will not create
|
|
1154
|
-
`created_at`.
|
|
1155
|
-
|
|
1156
|
-
```ruby
|
|
1157
|
-
user_id = SecureRandom.uuid
|
|
1158
|
-
email = 'bob@bob.bob'
|
|
1159
|
-
|
|
1160
|
-
Dynamoid::TransactionWrite.execute do |txn|
|
|
1161
|
-
txn.create(User, id: user_id)
|
|
1162
|
-
txn.create(UserEmail, id: "UserEmail##{email}", user_id: user_id)
|
|
1163
|
-
txn.create(Address, id: 'A#2', street: '456')
|
|
1164
|
-
txn.upsert(Address, 'A#1', street: '123')
|
|
1165
|
-
end
|
|
1166
|
-
```
|
|
1167
|
-
|
|
1168
|
-
##### Save models
|
|
1169
|
-
|
|
1170
|
-
Models can be saved in a transaction. New records are created otherwise
|
|
1171
|
-
the model is updated. Save, create, update, validate and destroy
|
|
1172
|
-
callbacks are called around the transaction as appropriate. Validation
|
|
1173
|
-
failures will throw `Dynamoid::Errors::DocumentNotValid`.
|
|
1174
|
-
|
|
1175
|
-
```ruby
|
|
1176
|
-
user = User.find(1)
|
|
1177
|
-
article = Article.new(body: 'New article text', user_id: user.id)
|
|
1178
|
-
|
|
1179
|
-
Dynamoid::TransactionWrite.execute do |txn|
|
|
1180
|
-
txn.save(article)
|
|
1181
|
-
|
|
1182
|
-
user.last_article_id = article.id
|
|
1183
|
-
txn.save(user)
|
|
1184
|
-
end
|
|
1185
|
-
```
|
|
1186
|
-
|
|
1187
|
-
##### Update models
|
|
1188
|
-
|
|
1189
|
-
A model can be updated by providing a model or primary key, and the fields to update.
|
|
1190
|
-
|
|
1191
|
-
```ruby
|
|
1192
|
-
Dynamoid::TransactionWrite.execute do |txn|
|
|
1193
|
-
# change name and title for a user
|
|
1194
|
-
txn.update_attributes(user, name: 'bob', title: 'mister')
|
|
1195
|
-
|
|
1196
|
-
# sets the name and title for a user
|
|
1197
|
-
# The user is found by id (that equals 1)
|
|
1198
|
-
txn.update_fields(User, '1', name: 'bob', title: 'mister')
|
|
1199
|
-
|
|
1200
|
-
# sets the name, increments a count and deletes a field
|
|
1201
|
-
txn.update_fields(User, 1) do |t|
|
|
1202
|
-
t.set(name: 'bob')
|
|
1203
|
-
t.add(article_count: 1)
|
|
1204
|
-
t.delete(:title)
|
|
1205
|
-
end
|
|
1206
|
-
|
|
1207
|
-
# adds to a set of integers and deletes from a set of strings
|
|
1208
|
-
txn.update_fields(User, 2) do |t|
|
|
1209
|
-
t.add(friend_ids: [1, 2])
|
|
1210
|
-
t.delete(child_names: ['bebe'])
|
|
1211
|
-
end
|
|
1212
|
-
end
|
|
1213
|
-
```
|
|
1214
|
-
|
|
1215
|
-
##### Destroy or delete models
|
|
1216
|
-
|
|
1217
|
-
Models can be used or the model class and key can be specified.
|
|
1218
|
-
`#destroy` uses callbacks and validations. Use `#delete` to skip
|
|
1219
|
-
callbacks and validations.
|
|
1220
|
-
|
|
1221
|
-
```ruby
|
|
1222
|
-
article = Article.find('1')
|
|
1223
|
-
tag = article.tag
|
|
1224
|
-
|
|
1225
|
-
Dynamoid::TransactionWrite.execute do |txn|
|
|
1226
|
-
txn.destroy(article)
|
|
1227
|
-
txn.delete(tag)
|
|
1228
|
-
|
|
1229
|
-
txn.delete(Tag, '2') # delete record with hash key '2' if it exists
|
|
1230
|
-
txn.delete(Tag, 'key#abcd', 'range#1') # when sort key is required
|
|
1231
|
-
end
|
|
1232
|
-
```
|
|
1233
|
-
|
|
1234
|
-
##### Validation failures that don't raise
|
|
1235
|
-
|
|
1236
|
-
All of the transaction methods can be called without the `!` which
|
|
1237
|
-
results in `false` instead of a raised exception when validation fails.
|
|
1238
|
-
Ignoring validation failures can lead to confusion or bugs so always
|
|
1239
|
-
check return status when not using a method with `!`.
|
|
1240
|
-
|
|
1241
|
-
```ruby
|
|
1242
|
-
user = User.find('1')
|
|
1243
|
-
user.red = true
|
|
1244
|
-
|
|
1245
|
-
Dynamoid::TransactionWrite.execute do |txn|
|
|
1246
|
-
if txn.save(user) # won't raise validation exception
|
|
1247
|
-
txn.update_fields(UserCount, user.id, count: 5)
|
|
1248
|
-
else
|
|
1249
|
-
puts 'ALERT: user not valid, skipping'
|
|
1250
|
-
end
|
|
1251
|
-
end
|
|
1252
|
-
```
|
|
1253
|
-
|
|
1254
|
-
##### Incrementally building a transaction
|
|
1255
|
-
|
|
1256
|
-
Transactions can also be built without a block.
|
|
1257
|
-
|
|
1258
|
-
```ruby
|
|
1259
|
-
transaction = Dynamoid::TransactionWrite.new
|
|
1260
|
-
|
|
1261
|
-
transaction.create(User, id: user_id)
|
|
1262
|
-
transaction.create(UserEmail, id: "UserEmail##{email}", user_id: user_id)
|
|
1263
|
-
transaction.upsert(Address, 'A#1', street: '123')
|
|
1264
|
-
|
|
1265
|
-
transaction.commit # changes are persisted in this moment
|
|
1266
|
-
```
|
|
1267
|
-
|
|
1268
|
-
#### Reading transactions
|
|
1269
|
-
|
|
1270
|
-
Multiple reading actions can be grouped together and submitted as an
|
|
1271
|
-
all-or-nothing operation. Atomic operations are supported in Dynamoid
|
|
1272
|
-
using transactions. If any action in the transaction fails they all
|
|
1273
|
-
fail.
|
|
1274
|
-
|
|
1275
|
-
The following actions are supported:
|
|
1276
|
-
|
|
1277
|
-
* `#find` - load a single model or multiple models by its primary key
|
|
1278
|
-
|
|
1279
|
-
These methods are supposed to behave exactly like their
|
|
1280
|
-
non-transactional counterparts.
|
|
1281
|
-
|
|
1282
|
-
##### Find a model
|
|
1283
|
-
|
|
1284
|
-
The `#find` action can load single model or multiple ones. Different
|
|
1285
|
-
model classes can be mixed in the same transactions. Result is returned
|
|
1286
|
-
as a plain list of all the found models. The order is preserved.
|
|
1287
|
-
|
|
1288
|
-
```ruby
|
|
1289
|
-
user, address = Dynamoid::TransactionRead.execute do |t|
|
|
1290
|
-
t.find(User, user_id)
|
|
1291
|
-
t.find(Address, address_id)
|
|
1292
|
-
end
|
|
1293
|
-
```
|
|
1294
|
-
|
|
1295
|
-
Multiple primary keys can be specified at once:
|
|
62
|
+
field :name # Type defaults to :string
|
|
63
|
+
field :email
|
|
64
|
+
field :age, :integer
|
|
65
|
+
field :active, :boolean, default: true
|
|
1296
66
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
t.find(User, [id1, id2, id3])
|
|
67
|
+
validates :name, presence: true
|
|
68
|
+
validates :email, format: { with: /@/ }
|
|
1300
69
|
end
|
|
1301
70
|
```
|
|
1302
71
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
To run PartiQL statements `Dynamoid.adapter.execute` method should be
|
|
1306
|
-
used:
|
|
1307
|
-
|
|
1308
|
-
```ruby
|
|
1309
|
-
Dynamoid.adapter.execute("UPDATE users SET name = 'Mike' WHERE id = '1'")
|
|
1310
|
-
```
|
|
1311
|
-
|
|
1312
|
-
Parameters are also supported:
|
|
1313
|
-
|
|
1314
|
-
```ruby
|
|
1315
|
-
Dynamoid.adapter.execute('SELECT * FROM users WHERE id = ?', ['1'])
|
|
1316
|
-
```
|
|
1317
|
-
|
|
1318
|
-
## Configuration
|
|
1319
|
-
|
|
1320
|
-
Listed below are all configuration options.
|
|
1321
|
-
|
|
1322
|
-
* `adapter` - useful only for the gem developers to switch to a new
|
|
1323
|
-
adapter. Default and the only available value is `aws_sdk_v3`
|
|
1324
|
-
* `namespace` - prefix for table names, default is
|
|
1325
|
-
`dynamoid_#{application_name}_#{environment}` for Rails application
|
|
1326
|
-
and `dynamoid` otherwise
|
|
1327
|
-
* `logger` - by default it's a `Rails.logger` in Rails application and
|
|
1328
|
-
`stdout` otherwise. You can disable logging by setting `nil` or
|
|
1329
|
-
`false` values. Set `true` value to use defaults
|
|
1330
|
-
* `access_key` - DynamoDb custom access key for AWS credentials, override global
|
|
1331
|
-
AWS credentials if they're present
|
|
1332
|
-
* `secret_key` - DynamoDb custom secret key for AWS credentials, override global
|
|
1333
|
-
AWS credentials if they're present
|
|
1334
|
-
* `credentials` - DynamoDb custom pre-configured credentials, override global
|
|
1335
|
-
AWS credentials if they're present
|
|
1336
|
-
* `region` - DynamoDb custom credentials for AWS, override global AWS
|
|
1337
|
-
credentials if they're present
|
|
1338
|
-
* `batch_size` - when you try to load multiple items at once with
|
|
1339
|
-
* `batch_get_item` call Dynamoid loads them not with one api call but
|
|
1340
|
-
piece by piece. Default is 100 items
|
|
1341
|
-
* `capacity_mode` - used at a table creation and means whether a table
|
|
1342
|
-
read/write capacity mode will be on-demand or provisioned. Allowed
|
|
1343
|
-
values are `:on_demand` and `:provisioned`. Default value is `nil` which
|
|
1344
|
-
means provisioned mode will be used.
|
|
1345
|
-
* `read_capacity` - is used at table or indices creation. Default is 100
|
|
1346
|
-
(units)
|
|
1347
|
-
* `write_capacity` - is used at table or indices creation. Default is 20
|
|
1348
|
-
(units)
|
|
1349
|
-
* `warn_on_scan` - log warnings when scan table. Default is `true`
|
|
1350
|
-
* `error_on_scan` - raises an error when scan table. Default is `false`
|
|
1351
|
-
* `endpoint` - if provided, it communicates with the DynamoDB listening
|
|
1352
|
-
at the endpoint. This is useful for testing with
|
|
1353
|
-
[DynamoDB Local](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html)
|
|
1354
|
-
* `identity_map` - ensures that each object gets loaded only once by
|
|
1355
|
-
keeping every loaded object in a map. Looks up objects using the map
|
|
1356
|
-
when referring to them. Isn't thread safe. Default is `false`.
|
|
1357
|
-
`Use Dynamoid::Middleware::IdentityMap` to clear identity map for each HTTP request
|
|
1358
|
-
* `timestamps` - by default Dynamoid sets `created_at` and `updated_at`
|
|
1359
|
-
fields at model creation and updating. You can disable this
|
|
1360
|
-
behavior by setting `false` value
|
|
1361
|
-
* `sync_retry_max_times` - when Dynamoid creates or deletes table
|
|
1362
|
-
synchronously it checks for completion specified times. Default is 60
|
|
1363
|
-
(times). It's a bit over 2 minutes by default
|
|
1364
|
-
* `sync_retry_wait_seconds` - time to wait between retries. Default is 2
|
|
1365
|
-
(seconds)
|
|
1366
|
-
* `convert_big_decimal` - if `true` then Dynamoid converts numbers
|
|
1367
|
-
stored in `Hash` in `raw` field to float. Default is `false`
|
|
1368
|
-
* `store_attribute_with_nil_value` - if `true` Dynamoid keeps attribute
|
|
1369
|
-
with `nil` value in a document. Otherwise Dynamoid removes it while
|
|
1370
|
-
saving a document. Default is `nil` which equals behaviour with `false`
|
|
1371
|
-
value.
|
|
1372
|
-
* `models_dir` - `dynamoid:create_tables` rake task loads DynamoDb
|
|
1373
|
-
models from this directory. Default is `./app/models`.
|
|
1374
|
-
* `application_timezone` - Dynamoid converts all `datetime` fields to
|
|
1375
|
-
specified time zone when loads data from the storage.
|
|
1376
|
-
Acceptable values - `:utc`, `:local` (to use system time zone) and
|
|
1377
|
-
time zone name e.g. `Eastern Time (US & Canada)`. Default is `utc`
|
|
1378
|
-
* `dynamodb_timezone` - When a datetime field is stored in string format
|
|
1379
|
-
Dynamoid converts it to specified time zone when saves a value to the
|
|
1380
|
-
storage. Acceptable values - `:utc`, `:local` (to use system time
|
|
1381
|
-
zone) and time zone name e.g. `Eastern Time (US & Canada)`. Default is
|
|
1382
|
-
`utc`
|
|
1383
|
-
* `store_datetime_as_string` - if `true` then Dynamoid stores :datetime
|
|
1384
|
-
fields in ISO 8601 string format. Default is `false`
|
|
1385
|
-
* `store_date_as_string` - if `true` then Dynamoid stores :date fields
|
|
1386
|
-
in ISO 8601 string format. Default is `false`
|
|
1387
|
-
* `store_empty_string_as_nil` - store attribute's empty String value as NULL. Default is `true`
|
|
1388
|
-
* `store_boolean_as_native` - if `true` Dynamoid stores boolean fields
|
|
1389
|
-
as native DynamoDB boolean values. Otherwise boolean fields are stored
|
|
1390
|
-
as string values `'t'` and `'f'`. Default is `true`
|
|
1391
|
-
* `store_binary_as_native` - if `true` Dynamoid stores binary fields
|
|
1392
|
-
as native DynamoDB binary values. Otherwise binary fields are stored
|
|
1393
|
-
as Base64 encoded string values. Default is `false`
|
|
1394
|
-
* `backoff` - is a hash: key is a backoff strategy (symbol), value is
|
|
1395
|
-
parameters for the strategy. Is used in batch operations. Default id
|
|
1396
|
-
`nil`
|
|
1397
|
-
* `backoff_strategies`: is a hash and contains all available strategies.
|
|
1398
|
-
Default is `{ constant: ..., exponential: ...}`
|
|
1399
|
-
* `log_formatter`: overrides default AWS SDK formatter. There are
|
|
1400
|
-
several canned formatters: `Aws::Log::Formatter.default`,
|
|
1401
|
-
`Aws::Log::Formatter.colored` and `Aws::Log::Formatter.short`. Please
|
|
1402
|
-
look into `Aws::Log::Formatter` AWS SDK documentation in order to
|
|
1403
|
-
provide own formatter.
|
|
1404
|
-
* `http_continue_timeout`: The number of seconds to wait for a
|
|
1405
|
-
100-continue HTTP response before sending the request body. Default
|
|
1406
|
-
option value is `nil`. If not specified effected value is `1`
|
|
1407
|
-
* `http_idle_timeout`: The number of seconds an HTTP connection is
|
|
1408
|
-
allowed to sit idle before it is considered stale. Default option
|
|
1409
|
-
value is `nil`. If not specified effected value is `5`
|
|
1410
|
-
* `http_open_timeout`: The number of seconds to wait when opening a HTTP
|
|
1411
|
-
session. Default option value is `nil`. If not specified effected
|
|
1412
|
-
value is `15`
|
|
1413
|
-
* `http_read_timeout`:The number of seconds to wait for HTTP response
|
|
1414
|
-
data. Default option value is `nil`. If not specified effected value
|
|
1415
|
-
is `60`
|
|
1416
|
-
* `create_table_on_save`: if `true` then Dynamoid creates a
|
|
1417
|
-
corresponding table in DynamoDB at model persisting if the table
|
|
1418
|
-
doesn't exist yet. Default is `true`
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
## Concurrency
|
|
1422
|
-
|
|
1423
|
-
Dynamoid supports basic, ActiveRecord-like optimistic locking on save
|
|
1424
|
-
operations. Simply add a `lock_version` column to your table like so:
|
|
72
|
+
Once defined, you can interact with your models using a familiar API:
|
|
1425
73
|
|
|
1426
74
|
```ruby
|
|
1427
|
-
|
|
1428
|
-
|
|
75
|
+
# Create and Save
|
|
76
|
+
user = User.create(name: 'Josh', email: 'josh@example.com')
|
|
1429
77
|
|
|
1430
|
-
|
|
78
|
+
# Find and Update
|
|
79
|
+
user = User.where(email: 'josh@example.com').first
|
|
80
|
+
user.update_attributes(age: 30)
|
|
1431
81
|
|
|
1432
|
-
|
|
1433
|
-
|
|
82
|
+
# Querying
|
|
83
|
+
users = User.where(active: true).all.to_a
|
|
1434
84
|
```
|
|
1435
85
|
|
|
1436
|
-
In this example, all saves to `MyTable` will raise an
|
|
1437
|
-
`Dynamoid::Errors::StaleObjectError` if a concurrent process loaded,
|
|
1438
|
-
edited, and saved the same row. Your code should trap this exception,
|
|
1439
|
-
reload the row (so that it will pick up the newest values), and try the
|
|
1440
|
-
save again.
|
|
1441
|
-
|
|
1442
|
-
Calls to `update` and `update!` also increment the `lock_version`,
|
|
1443
|
-
however, they do not check the existing value. This guarantees that a
|
|
1444
|
-
update operation will raise an exception in a concurrent save operation,
|
|
1445
|
-
however a save operation will never cause an update to fail. Thus,
|
|
1446
|
-
`update` is useful & safe only for doing atomic operations (e.g.
|
|
1447
|
-
increment a value, add/remove from a set, etc), but should not be used
|
|
1448
|
-
in a read-modify-write pattern.
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
### Backoff strategies
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
You can use several methods that run efficiently in batch mode like
|
|
1455
|
-
`.find_all` and `.import`. It affects `Query` and `Scan` operations as
|
|
1456
|
-
well.
|
|
1457
|
-
|
|
1458
|
-
The backoff strategy will be used when, for any reason, some items could
|
|
1459
|
-
not be processed as part of a batch mode command. Operations will be
|
|
1460
|
-
re-run to process these items.
|
|
1461
|
-
|
|
1462
|
-
Exponential backoff is the recommended way to handle throughput limits
|
|
1463
|
-
exceeding and throttling on the table.
|
|
1464
|
-
|
|
1465
|
-
There are two built-in strategies - constant delay and truncated binary
|
|
1466
|
-
exponential backoff. By default no backoff is used but you can specify
|
|
1467
|
-
one of the built-in ones:
|
|
1468
|
-
|
|
1469
|
-
```ruby
|
|
1470
|
-
Dynamoid.configure do |config|
|
|
1471
|
-
config.backoff = { constant: 2.second }
|
|
1472
|
-
end
|
|
1473
|
-
|
|
1474
|
-
Dynamoid.configure do |config|
|
|
1475
|
-
config.backoff = { exponential: { base_backoff: 0.2.seconds, ceiling: 10 } }
|
|
1476
|
-
end
|
|
1477
86
|
|
|
1478
|
-
|
|
87
|
+
### Essential Configuration
|
|
1479
88
|
|
|
1480
|
-
You can
|
|
1481
|
-
presets:
|
|
89
|
+
Minimal connection settings are required. You can configure Dynamoid in several ways, such as in `config/initializers/dynamoid.rb` (for Rails) or directly in your setup:
|
|
1482
90
|
|
|
1483
91
|
```ruby
|
|
1484
|
-
|
|
1485
|
-
config.backoff = :constant
|
|
1486
|
-
end
|
|
1487
|
-
```
|
|
1488
|
-
|
|
1489
|
-
You can use your own strategy in the following way:
|
|
92
|
+
require 'dynamoid'
|
|
1490
93
|
|
|
1491
|
-
```ruby
|
|
1492
94
|
Dynamoid.configure do |config|
|
|
1493
|
-
config.
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
config.backoff = { custom: 10 }
|
|
95
|
+
config.access_key = 'REPLACE_WITH_ACCESS_KEY_ID'
|
|
96
|
+
config.secret_key = 'REPLACE_WITH_SECRET_ACCESS_KEY'
|
|
97
|
+
config.region = 'REPLACE_WITH_REGION' # e.g. 'us-west-2'
|
|
1498
98
|
end
|
|
1499
99
|
```
|
|
1500
100
|
|
|
1501
101
|
|
|
1502
|
-
##
|
|
1503
|
-
|
|
1504
|
-
There are a few Rake tasks available out of the box:
|
|
1505
|
-
|
|
1506
|
-
* `rake dynamoid:create_tables`
|
|
1507
|
-
* `rake dynamoid:ping`
|
|
1508
|
-
|
|
1509
|
-
In order to use them in non-Rails application they should be required
|
|
1510
|
-
explicitly:
|
|
1511
|
-
|
|
1512
|
-
```ruby
|
|
1513
|
-
# Rakefile
|
|
1514
|
-
|
|
1515
|
-
Rake::Task.define_task(:environment)
|
|
1516
|
-
require 'dynamoid/tasks'
|
|
1517
|
-
```
|
|
1518
|
-
|
|
1519
|
-
The Rake tasks depend on `:environment` task so it should be declared as
|
|
1520
|
-
well.
|
|
1521
|
-
|
|
1522
|
-
## Test Environment
|
|
1523
|
-
|
|
1524
|
-
In test environment you will most likely want to clean the database
|
|
1525
|
-
between test runs to keep tests completely isolated. This can be
|
|
1526
|
-
achieved like so
|
|
102
|
+
## Documentation
|
|
1527
103
|
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
def self.all
|
|
1531
|
-
Dynamoid.adapter.list_tables.each do |table|
|
|
1532
|
-
# Only delete tables in our namespace
|
|
1533
|
-
if table =~ /^#{Dynamoid::Config.namespace}/
|
|
1534
|
-
Dynamoid.adapter.delete_table(table)
|
|
1535
|
-
end
|
|
1536
|
-
end
|
|
1537
|
-
Dynamoid.adapter.tables.clear
|
|
1538
|
-
# Recreate all tables to avoid unexpected errors
|
|
1539
|
-
Dynamoid.included_models.each { |m| m.create_table(sync: true) }
|
|
1540
|
-
end
|
|
1541
|
-
end
|
|
1542
|
-
|
|
1543
|
-
# Reduce noise in test output
|
|
1544
|
-
Dynamoid.logger.level = Logger::FATAL
|
|
1545
|
-
```
|
|
104
|
+
* **API Reference:** Comprehensive documentation for all classes and methods is available on [API Reference](https://dynamoid.github.io/dynamoid/reference/).
|
|
105
|
+
* **User Guides:** For detailed overviews and usage examples of specific features, see the online [User Guides](https://dynamoid.github.io/dynamoid/guides/).
|
|
1546
106
|
|
|
1547
|
-
If you're using RSpec you can invoke the above like so:
|
|
1548
|
-
|
|
1549
|
-
```ruby
|
|
1550
|
-
RSpec.configure do |config|
|
|
1551
|
-
config.before(:each) do
|
|
1552
|
-
DynamoidReset.all
|
|
1553
|
-
end
|
|
1554
|
-
end
|
|
1555
|
-
```
|
|
1556
107
|
|
|
1557
|
-
|
|
1558
|
-
```ruby
|
|
1559
|
-
Dir[File.join(Dynamoid::Config.models_dir, '**/*.rb')].sort.each { |file| require file }
|
|
1560
|
-
```
|
|
1561
|
-
Note that this will require _all_ models in your models folder - you can also explicitly require only certain models if you would prefer to.
|
|
108
|
+
## Compatibility
|
|
1562
109
|
|
|
1563
|
-
|
|
1564
|
-
accidentally by adding the following to your test environment setup:
|
|
110
|
+
Dynamoid relies on `aws-sdk-dynamodb` (AWS SDK v3) and `activemodel` (>= 4.2). It officially supports Ruby >= 2.3 and Rails >= 4.2.
|
|
1565
111
|
|
|
1566
|
-
|
|
1567
|
-
|
|
112
|
+
Compatibility is tested against the following versions:
|
|
113
|
+
* Ruby: 2.3 - 4.0 (including JRuby 10.x)
|
|
114
|
+
* Rails: 4.2 - 8.1
|
|
1568
115
|
|
|
1569
|
-
Dynamoid.configure do |config|
|
|
1570
|
-
config.namespace = "#{Rails.application.railtie_name}_#{Rails.env}"
|
|
1571
|
-
end
|
|
1572
|
-
```
|
|
1573
116
|
|
|
1574
|
-
##
|
|
117
|
+
## Contributing
|
|
1575
118
|
|
|
1576
|
-
|
|
1577
|
-
responses to DynamoDB using this logger on the `debug` level. So in
|
|
1578
|
-
order to troubleshoot and debug issues just set it:
|
|
119
|
+
We welcome contributions to Dynamoid! Please see our [CONTRIBUTING.md][contributing] guide for details on how to get started. Join us!
|
|
1579
120
|
|
|
1580
|
-
```ruby
|
|
1581
|
-
class User
|
|
1582
|
-
include Dynamoid::Document
|
|
1583
121
|
|
|
1584
|
-
|
|
1585
|
-
end
|
|
122
|
+
## Security
|
|
1586
123
|
|
|
1587
|
-
|
|
1588
|
-
Dynamoid.config.endpoint = 'http://localhost:8000'
|
|
124
|
+
See [SECURITY.md][security].
|
|
1589
125
|
|
|
1590
|
-
User.create(name: 'Alex')
|
|
1591
126
|
|
|
1592
|
-
|
|
127
|
+
## License
|
|
1593
128
|
|
|
1594
|
-
|
|
1595
|
-
|
|
129
|
+
The gem is available as open source under the terms of
|
|
130
|
+
the [MIT License][license] [][license-ref].
|
|
131
|
+
See [LICENSE][license] for the official [Copyright Notice][copyright-notice-explainer].
|
|
1596
132
|
|
|
1597
|
-
The first line is a body of HTTP request and response. The second line -
|
|
1598
|
-
Dynamoid internal logging of API call (`PUT ITEM` in our case) with
|
|
1599
|
-
timing (231.28 ms).
|
|
1600
133
|
|
|
1601
134
|
## Credits
|
|
1602
135
|
|
|
1603
136
|
Dynamoid borrows code, structure, and even its name very liberally from
|
|
1604
137
|
the truly amazing [Mongoid](https://github.com/mongoid/mongoid). Without
|
|
1605
138
|
Mongoid to crib from none of this would have been possible, and I hope
|
|
1606
|
-
they don't mind me reusing their very awesome ideas to make DynamoDB
|
|
139
|
+
they don't mind me reusing their very awesome ideas to make Amazon DynamoDB
|
|
1607
140
|
just as accessible to the Ruby world as MongoDB.
|
|
1608
141
|
|
|
1609
142
|
Also, without contributors the project wouldn't be nearly as awesome. So
|
|
@@ -1627,70 +160,13 @@ many thanks to:
|
|
|
1627
160
|
|
|
1628
161
|
\* Current Maintainers
|
|
1629
162
|
|
|
1630
|
-
## Running the tests
|
|
1631
|
-
|
|
1632
|
-
Running the tests is fairly simple. You should have an instance of
|
|
1633
|
-
DynamoDB running locally. Follow these steps to setup your test
|
|
1634
|
-
environment.
|
|
1635
|
-
|
|
1636
|
-
* First download and unpack the latest version of DynamoDB. We have a
|
|
1637
|
-
script that will do this for you if you use bash, and homebrew on a Mac.
|
|
1638
|
-
|
|
1639
|
-
```shell
|
|
1640
|
-
bin/setup
|
|
1641
|
-
```
|
|
1642
|
-
|
|
1643
|
-
* Start the local instance of DynamoDB to listen in ***8000*** port
|
|
1644
|
-
|
|
1645
|
-
```shell
|
|
1646
|
-
bin/start_dynamodblocal
|
|
1647
|
-
```
|
|
1648
|
-
|
|
1649
|
-
* and lastly, use `rake` to run the tests.
|
|
1650
|
-
|
|
1651
|
-
```shell
|
|
1652
|
-
rake
|
|
1653
|
-
```
|
|
1654
|
-
|
|
1655
|
-
* When you are done, remember to stop the local test instance of
|
|
1656
|
-
dynamodb
|
|
1657
|
-
|
|
1658
|
-
```shell
|
|
1659
|
-
bin/stop_dynamodblocal
|
|
1660
|
-
```
|
|
1661
|
-
|
|
1662
|
-
If you run into issues, please try these steps first.
|
|
1663
|
-
NOTE: You can use any version manager: rvm, rbenv, chruby, asdf-ruby
|
|
1664
|
-
```shell
|
|
1665
|
-
asdf install ruby 3.1.1
|
|
1666
|
-
asdf local ruby 3.1.1
|
|
1667
|
-
gem update --system
|
|
1668
|
-
bundle install
|
|
1669
|
-
```
|
|
1670
|
-
|
|
1671
|
-
## Security
|
|
1672
|
-
|
|
1673
|
-
See [SECURITY.md][security].
|
|
1674
|
-
|
|
1675
|
-
## Related links
|
|
1676
|
-
|
|
1677
|
-
This documentation may be useful for the contributors:
|
|
1678
|
-
- <https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/welcome.html>
|
|
1679
|
-
- <https://docs.aws.amazon.com/sdk-for-ruby/v3/api/index.html>
|
|
1680
|
-
|
|
1681
|
-
## License
|
|
1682
|
-
|
|
1683
|
-
The gem is available as open source under the terms of
|
|
1684
|
-
the [MIT License][license] [][license-ref].
|
|
1685
|
-
See [LICENSE][license] for the official [Copyright Notice][copyright-notice-explainer].
|
|
1686
|
-
|
|
1687
|
-
[copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
|
1688
163
|
|
|
164
|
+
[contributing]:
|
|
165
|
+
https://github.com/Dynamoid/dynamoid/blob/master/CONTRIBUTING.md
|
|
166
|
+
[security]: https://github.com/Dynamoid/dynamoid/blob/master/SECURITY.md
|
|
1689
167
|
[license]: https://github.com/Dynamoid/dynamoid/blob/master/LICENSE.txt
|
|
1690
|
-
|
|
1691
168
|
[license-ref]: https://opensource.org/licenses/MIT
|
|
1692
|
-
|
|
1693
|
-
[security]: https://github.com/Dynamoid/dynamoid/blob/master/SECURITY.md
|
|
169
|
+
[copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
|
1694
170
|
|
|
1695
171
|
[⛳️gem]: https://rubygems.org/gems/dynamoid
|
|
1696
172
|
[⛳️version-img]: http://img.shields.io/gem/v/dynamoid.svg
|