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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -4
  3. data/README.md +60 -1584
  4. data/lib/dynamoid/associations/association.rb +10 -2
  5. data/lib/dynamoid/associations/belongs_to.rb +3 -1
  6. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +0 -1
  7. data/lib/dynamoid/associations/has_many.rb +0 -1
  8. data/lib/dynamoid/associations/has_one.rb +1 -2
  9. data/lib/dynamoid/associations/many_association.rb +1 -0
  10. data/lib/dynamoid/associations.rb +64 -48
  11. data/lib/dynamoid/components.rb +1 -0
  12. data/lib/dynamoid/criteria/chain.rb +43 -30
  13. data/lib/dynamoid/dirty.rb +13 -3
  14. data/lib/dynamoid/document.rb +29 -21
  15. data/lib/dynamoid/fields.rb +55 -41
  16. data/lib/dynamoid/finders.rb +1 -1
  17. data/lib/dynamoid/indexes.rb +29 -21
  18. data/lib/dynamoid/persistence/inc.rb +15 -7
  19. data/lib/dynamoid/persistence/save.rb +21 -14
  20. data/lib/dynamoid/persistence.rb +88 -54
  21. data/lib/dynamoid/transactions/mutation/base.rb +76 -0
  22. data/lib/dynamoid/transactions/mutation/builders/delete_request_builder.rb +44 -0
  23. data/lib/dynamoid/transactions/mutation/builders/update_request_builder.rb +139 -0
  24. data/lib/dynamoid/transactions/mutation/create.rb +52 -0
  25. data/lib/dynamoid/transactions/mutation/delete_with_instance.rb +80 -0
  26. data/lib/dynamoid/transactions/mutation/delete_with_primary_key.rb +62 -0
  27. data/lib/dynamoid/transactions/mutation/destroy.rb +96 -0
  28. data/lib/dynamoid/transactions/mutation/import.rb +84 -0
  29. data/lib/dynamoid/transactions/mutation/inc.rb +101 -0
  30. data/lib/dynamoid/transactions/mutation/increment.rb +63 -0
  31. data/lib/dynamoid/transactions/mutation/save.rb +194 -0
  32. data/lib/dynamoid/transactions/mutation/touch.rb +99 -0
  33. data/lib/dynamoid/transactions/mutation/update_attributes.rb +49 -0
  34. data/lib/dynamoid/transactions/mutation/update_fields.rb +185 -0
  35. data/lib/dynamoid/transactions/mutation/upsert.rb +94 -0
  36. data/lib/dynamoid/transactions/mutation.rb +935 -0
  37. data/lib/dynamoid/transactions/retrieval/find.rb +140 -0
  38. data/lib/dynamoid/transactions/retrieval.rb +150 -0
  39. data/lib/dynamoid/transactions.rb +63 -0
  40. data/lib/dynamoid/version.rb +1 -1
  41. data/lib/dynamoid.rb +1 -2
  42. metadata +23 -17
  43. data/lib/dynamoid/transaction_read/find.rb +0 -137
  44. data/lib/dynamoid/transaction_read.rb +0 -146
  45. data/lib/dynamoid/transaction_write/base.rb +0 -59
  46. data/lib/dynamoid/transaction_write/create.rb +0 -49
  47. data/lib/dynamoid/transaction_write/delete_with_instance.rb +0 -65
  48. data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +0 -64
  49. data/lib/dynamoid/transaction_write/destroy.rb +0 -84
  50. data/lib/dynamoid/transaction_write/item_updater.rb +0 -60
  51. data/lib/dynamoid/transaction_write/save.rb +0 -177
  52. data/lib/dynamoid/transaction_write/update_attributes.rb +0 -46
  53. data/lib/dynamoid/transaction_write/update_fields.rb +0 -247
  54. data/lib/dynamoid/transaction_write/upsert.rb +0 -113
  55. 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 is not like other document-based databases you might know, and
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
- ## Installation
21
+ ## Key Features
36
22
 
37
- Installing Dynamoid is pretty simple. First include the Gem in your
38
- Gemfile:
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
- At times it can be useful to rely on DynamoDB [low-level
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
- ```ruby
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
- Returned models with have filled specified fields only.
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
- Address.find(id).update_attributes(city: 'Chicago')
997
- Address.find(id).update_attribute(:city, 'Chicago')
998
- Address.update(id, city: 'Chicago')
38
+ gem 'dynamoid'
999
39
  ```
1000
40
 
1001
- There are also some low level methods `#update`, `.update_fields` and
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
- ```ruby
1008
- Address.find(id).update do |i|
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
- By default, `#upsert` will update all attributes of the document if it already exists.
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
- ```ruby
1027
- Address.upsert(id, { city: 'Chicago' }, { unless_exists: [:id] })
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
- ### Global Secondary Indexes
54
+ ### Usage
1040
55
 
1041
- You can define index with `global_secondary_index`:
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 :age, :number
1049
-
1050
- global_secondary_index hash_key: :age # Must come after field definitions.
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
- ```ruby
1298
- users = Dynamoid::TransactionRead.execute do |t|
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
- ### PartiQL
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
- class MyTable
1428
- # ...
75
+ # Create and Save
76
+ user = User.create(name: 'Josh', email: 'josh@example.com')
1429
77
 
1430
- field :lock_version, :integer
78
+ # Find and Update
79
+ user = User.where(email: 'josh@example.com').first
80
+ user.update_attributes(age: 30)
1431
81
 
1432
- # ...
1433
- end
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 just specify strategy without any arguments to use default
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
- Dynamoid.configure do |config|
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.backoff_strategies[:custom] = lambda do |n|
1494
- -> { sleep rand(n) }
1495
- end
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
- ## Rake Tasks
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
- ```ruby
1529
- module DynamoidReset
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
- In addition, the first test for each model may fail if the relevant models are not included in `included_models`. This can be fixed by adding this line before the `DynamoidReset` module:
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
- In Rails, you may also want to ensure you do not delete non-test data
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
- ```ruby
1567
- raise "Tests should be run in 'test' environment only" if Rails.env != 'test'
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
- ## Logging
117
+ ## Contributing
1575
118
 
1576
- There is a config option `logger`. Dynamoid writes requests and
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
- field name
1585
- end
122
+ ## Security
1586
123
 
1587
- Dynamoid.config.logger.level = :debug
1588
- Dynamoid.config.endpoint = 'http://localhost:8000'
124
+ See [SECURITY.md][security].
1589
125
 
1590
- User.create(name: 'Alex')
1591
126
 
1592
- # => D, [2019-05-12T20:01:07.840051 #75059] DEBUG -- : put_item | Request "{\"TableName\":\"dynamoid_users\",\"Item\":{\"created_at\":{\"N\":\"1557680467.608749\"},\"updated_at\":{\"N\":\"1557680467.608809\"},\"id\":{\"S\":\"1227eea7-2c96-4b8a-90d9-77b38eb85cd0\"}},\"Expected\":{\"id\":{\"Exists\":false}}}" | Response "{}"
127
+ ## License
1593
128
 
1594
- # => D, [2019-05-12T20:01:07.842397 #75059] DEBUG -- : (231.28 ms) PUT ITEM - ["dynamoid_users", {:created_at=>0.1557680467608749e10, :updated_at=>0.1557680467608809e10, :id=>"1227eea7-2c96-4b8a-90d9-77b38eb85cd0", :User=>nil}, {}]
1595
- ```
129
+ The gem is available as open source under the terms of
130
+ the [MIT License][license] [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)][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: MIT](https://img.shields.io/badge/License-MIT-green.svg)][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