dynamoid 3.10.0 → 3.12.1
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 +37 -1
- data/README.md +268 -8
- data/dynamoid.gemspec +4 -4
- data/lib/dynamoid/adapter.rb +1 -1
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +53 -18
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +5 -4
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +9 -7
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +1 -1
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +1 -1
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/transact.rb +31 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +17 -5
- data/lib/dynamoid/components.rb +1 -0
- data/lib/dynamoid/config.rb +3 -0
- data/lib/dynamoid/criteria/chain.rb +74 -21
- data/lib/dynamoid/criteria/where_conditions.rb +13 -6
- data/lib/dynamoid/dirty.rb +97 -11
- data/lib/dynamoid/dumping.rb +39 -17
- data/lib/dynamoid/errors.rb +30 -3
- data/lib/dynamoid/fields.rb +13 -3
- data/lib/dynamoid/finders.rb +44 -23
- data/lib/dynamoid/loadable.rb +1 -0
- data/lib/dynamoid/persistence/inc.rb +35 -19
- data/lib/dynamoid/persistence/item_updater_with_casting_and_dumping.rb +36 -0
- data/lib/dynamoid/persistence/item_updater_with_dumping.rb +33 -0
- data/lib/dynamoid/persistence/save.rb +29 -14
- data/lib/dynamoid/persistence/update_fields.rb +23 -8
- data/lib/dynamoid/persistence/update_validations.rb +3 -3
- data/lib/dynamoid/persistence/upsert.rb +22 -8
- data/lib/dynamoid/persistence.rb +184 -28
- data/lib/dynamoid/transaction_read/find.rb +137 -0
- data/lib/dynamoid/transaction_read.rb +146 -0
- data/lib/dynamoid/transaction_write/base.rb +47 -0
- data/lib/dynamoid/transaction_write/create.rb +49 -0
- data/lib/dynamoid/transaction_write/delete_with_instance.rb +65 -0
- data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +64 -0
- data/lib/dynamoid/transaction_write/destroy.rb +84 -0
- data/lib/dynamoid/transaction_write/item_updater.rb +55 -0
- data/lib/dynamoid/transaction_write/save.rb +169 -0
- data/lib/dynamoid/transaction_write/update_attributes.rb +46 -0
- data/lib/dynamoid/transaction_write/update_fields.rb +239 -0
- data/lib/dynamoid/transaction_write/upsert.rb +106 -0
- data/lib/dynamoid/transaction_write.rb +673 -0
- data/lib/dynamoid/type_casting.rb +3 -1
- data/lib/dynamoid/undumping.rb +13 -2
- data/lib/dynamoid/validations.rb +8 -5
- data/lib/dynamoid/version.rb +1 -1
- data/lib/dynamoid.rb +8 -0
- metadata +21 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f3e330233bdd01c0bb962fb7253b48629fe9f0aeb1ecdcb6cfe7d3272ddddbe0
|
4
|
+
data.tar.gz: 7f6ee27e8d6dedb5fe21bce21a6c00397f42d85dfa40b8d30dae4e13a8294d15
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f84a868794dd6b812ccd6936eaa16c91a8d0f87b2ae959b89a8b01802537490168449bfa83f62d92f8643a6c47043cefeb9168da3301ca4f005400bc1086406f
|
7
|
+
data.tar.gz: 528587c73879f49d25e660974fa5832464eed5773da06535a76bfe3c9c96311a1877eb801e68362c94eadc479cf65a8df053a0f47574c78fbf5919859cb88e66
|
data/CHANGELOG.md
CHANGED
@@ -11,7 +11,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
11
11
|
### Changed
|
12
12
|
### Removed
|
13
13
|
|
14
|
-
## 3.
|
14
|
+
## 3.12.0
|
15
|
+
|
16
|
+
### Fixed
|
17
|
+
* [#849](https://github.com/Dynamoid/dynamoid/pull/849) Fixed saving a field of custom type when it implements both `.dynamoid_dump()` and `#dynamoid_dump` method and use the former one.
|
18
|
+
A proper behaviour is to use adapter's .dynamoid_dump method instead of a value's #dynamoid_dump method.
|
19
|
+
* [#851](https://github.com/Dynamoid/dynamoid/pull/851) Fixed Dirty API and don't require custom types to implement `#==` method. Add field option `:comparable`.
|
20
|
+
* [#913](https://github.com/Dynamoid/dynamoid/pull/913) Fixed saving partition keys of Dynamoid-specific types (e.g. `date` or `datetime`) and always convert them into a proper DynamoDB type
|
21
|
+
* [#917](https://github.com/Dynamoid/dynamoid/pull/917) Fixed transactional write operations when primary key is of non-native DynamoDB type
|
22
|
+
* [#927](https://github.com/Dynamoid/dynamoid/pull/927) Multiple fixes in transactional and non-transactional write methods and finders:
|
23
|
+
* Changed non-transactional method `#find` and raise `MissingHashKey` when a given partition key is `nil`
|
24
|
+
* Fixed non-transactional method `#save!` and raise `RecordNotSaved` when a callback throws `:abort` and terminates the process
|
25
|
+
* Check whether partition and sort key are specified in the non-transactional persistence methods and raise a proper exception (`MissingHashKey` or `MissingRangeKey`)
|
26
|
+
* Fixed methods `#inc`, `#increment!` and `#decrement!`, `#update` and `#update!` to not create a new item when an item with specified primary key is already deleted
|
27
|
+
* Fixed method `#update_attribute` to not raise `StaleObjectError` when an item with specified primary key is already deleted
|
28
|
+
* Fixed method `#where` with condition on a `:serialized` field to not raise an exception
|
29
|
+
### Added
|
30
|
+
* [#910](https://github.com/Dynamoid/dynamoid/pull/910) Added `key_type` option to the `table` method to change type of a partition key field (@ogidow)
|
31
|
+
* [#916](https://github.com/Dynamoid/dynamoid/pull/916) Added new `error_on_scan` config option to raises an error and prevent table scans (DynamoDB's `Scan` operation) (@aaronalberg)
|
32
|
+
* [#926](https://github.com/Dynamoid/dynamoid/pull/926) Implemented read transactions (using DynamoDB's `TransactGetItems` operation)
|
33
|
+
### Changed
|
34
|
+
* [#846](https://github.com/Dynamoid/dynamoid/pull/846) Changed transactional `update_fields()` method to accept a block and support low-level operations `set`, `add`, `delete`, `remove` (similar to the non-transactional `#update()` method) (@ckhsponge)
|
35
|
+
### Removed
|
36
|
+
|
37
|
+
## 3.11.0 / 2025-01-12
|
38
|
+
### Fixed
|
39
|
+
* [#829](https://github.com/Dynamoid/dynamoid/pull/829) Fixed saving of in-place field changes
|
40
|
+
* [#812](https://github.com/Dynamoid/dynamoid/pull/812) Restored sanitizing of attribute names in `#where` conditions
|
41
|
+
* [#721](https://github.com/Dynamoid/dynamoid/pull/721) Fixed code examples in README.md (@ndjndj)
|
42
|
+
### Added
|
43
|
+
* [#688](https://github.com/Dynamoid/dynamoid/pull/688) Implemented write transactions (using DynamoDB's `TransactWriteItems` operation) (@ckhsponge)
|
44
|
+
* [#794](https://github.com/Dynamoid/dynamoid/pull/794) Added new config option `store_empty_string_as_nil`
|
45
|
+
* [#828](https://github.com/Dynamoid/dynamoid/pull/828) Added Ruby 3.4, Rails 8.0 and Rails 7.2 in CI
|
46
|
+
### Changed
|
47
|
+
* [#832](https://github.com/Dynamoid/dynamoid/pull/832) Support String condition expressions with `#where`
|
48
|
+
* [#822](https://github.com/Dynamoid/dynamoid/pull/822) Support binary type natively. Also added new config option `store_binary_as_native` (@dalibor)
|
49
|
+
|
50
|
+
## 3.10.0 / 2024-02-10
|
15
51
|
### Fixed
|
16
52
|
* [#681](https://github.com/Dynamoid/dynamoid/pull/681) Fixed saving persisted model and deleting attributes with `nil` value if `config.store_attribute_with_nil_value` is `false`
|
17
53
|
* [#716](https://github.com/Dynamoid/dynamoid/pull/716), [#691](https://github.com/Dynamoid/dynamoid/pull/691), [#687](https://github.com/Dynamoid/dynamoid/pull/687), [#660](https://github.com/Dynamoid/dynamoid/pull/660) Numerous fixes in README.md and RDoc documentation (@ndjndj, @kiharito, @dunkOnIT)
|
data/README.md
CHANGED
@@ -24,8 +24,8 @@ DynamoDB is not like other document-based databases you might know, and
|
|
24
24
|
is very different indeed from relational databases. It sacrifices
|
25
25
|
anything beyond the simplest relational queries and transactional
|
26
26
|
support to provide a fast, cost-efficient, and highly durable storage
|
27
|
-
solution. If your database requires complicated relational queries
|
28
|
-
|
27
|
+
solution. If your database requires complicated relational queries
|
28
|
+
then this modest Gem cannot provide them for you
|
29
29
|
and neither can DynamoDB. In those cases you would do better to look
|
30
30
|
elsewhere for your database needs.
|
31
31
|
|
@@ -102,8 +102,8 @@ credentials = Aws::AssumeRoleCredentials.new(
|
|
102
102
|
)
|
103
103
|
|
104
104
|
Dynamoid.configure do |config|
|
105
|
-
config.region = 'us-west-2'
|
106
|
-
|
105
|
+
config.region = 'us-west-2'
|
106
|
+
config.credentials = credentials
|
107
107
|
end
|
108
108
|
```
|
109
109
|
|
@@ -132,8 +132,8 @@ end
|
|
132
132
|
Dynamoid supports Ruby >= 2.3 and Rails >= 4.2.
|
133
133
|
|
134
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 and 3.
|
136
|
-
5.2, 6.0, 6.1, 7.0
|
135
|
+
2.5, 2.6, 2.7, 3.0, 3.1, 3.2, 3.3, and 3.4, 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, and 8.0.
|
137
137
|
|
138
138
|
## Setup
|
139
139
|
|
@@ -342,6 +342,24 @@ class Document
|
|
342
342
|
end
|
343
343
|
```
|
344
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
|
+
|
345
363
|
#### Magic Columns
|
346
364
|
|
347
365
|
You get magic columns of `id` (`string`), `created_at` (`datetime`), and
|
@@ -453,6 +471,27 @@ method, which would return either `:string` or `:number`.
|
|
453
471
|
DynamoDB may support some other attribute types that are not yet
|
454
472
|
supported by Dynamoid.
|
455
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
|
+
|
456
495
|
### Sort key
|
457
496
|
|
458
497
|
Along with partition key table may have a sort key. In order to declare
|
@@ -602,6 +641,7 @@ with `inheritance_field` table option:
|
|
602
641
|
```ruby
|
603
642
|
class Car
|
604
643
|
include Dynamoid::Document
|
644
|
+
|
605
645
|
table inheritance_field: :my_new_type
|
606
646
|
|
607
647
|
field :my_new_type
|
@@ -701,7 +741,7 @@ users = User.import([{ name: 'Josh' }, { name: 'Nick' }])
|
|
701
741
|
|
702
742
|
### Querying
|
703
743
|
|
704
|
-
Querying can be done in one of
|
744
|
+
Querying can be done in one of the following ways:
|
705
745
|
|
706
746
|
```ruby
|
707
747
|
Address.find(address.id) # Find directly by ID.
|
@@ -710,6 +750,27 @@ Address.where(city: 'Chicago').all # Find by any number of matching criteria.
|
|
710
750
|
Address.find_by_city('Chicago') # The same as above, but using ActiveRecord's older syntax.
|
711
751
|
```
|
712
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
|
+
|
713
774
|
And you can also query on associations:
|
714
775
|
|
715
776
|
```ruby
|
@@ -875,6 +936,7 @@ It could be done with `project` method:
|
|
875
936
|
```ruby
|
876
937
|
class User
|
877
938
|
include Dynamoid::Document
|
939
|
+
|
878
940
|
field :name
|
879
941
|
end
|
880
942
|
|
@@ -934,7 +996,6 @@ validation and callbacks.
|
|
934
996
|
Address.find(id).update_attributes(city: 'Chicago')
|
935
997
|
Address.find(id).update_attribute(:city, 'Chicago')
|
936
998
|
Address.update(id, city: 'Chicago')
|
937
|
-
Address.update(id, { city: 'Chicago' }, if: { deliverable: true })
|
938
999
|
```
|
939
1000
|
|
940
1001
|
There are also some low level methods `#update`, `.update_fields` and
|
@@ -1047,6 +1108,198 @@ resolving the fields with a second query against the table since a query
|
|
1047
1108
|
against GSI then a query on base table is still likely faster than scan
|
1048
1109
|
on the base table*
|
1049
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 existig
|
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:
|
1296
|
+
|
1297
|
+
```ruby
|
1298
|
+
users = Dynamoid::TransactionRead.execute do |t|
|
1299
|
+
t.find(User, [id1, id2, id3])
|
1300
|
+
end
|
1301
|
+
```
|
1302
|
+
|
1050
1303
|
### PartiQL
|
1051
1304
|
|
1052
1305
|
To run PartiQL statements `Dynamoid.adapter.execute` method should be
|
@@ -1094,6 +1347,7 @@ Listed below are all configuration options.
|
|
1094
1347
|
* `write_capacity` - is used at table or indices creation. Default is 20
|
1095
1348
|
(units)
|
1096
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`
|
1097
1351
|
* `endpoint` - if provided, it communicates with the DynamoDB listening
|
1098
1352
|
at the endpoint. This is useful for testing with
|
1099
1353
|
[DynamoDB Local](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html)
|
@@ -1130,9 +1384,13 @@ Listed below are all configuration options.
|
|
1130
1384
|
fields in ISO 8601 string format. Default is `false`
|
1131
1385
|
* `store_date_as_string` - if `true` then Dynamoid stores :date fields
|
1132
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`
|
1133
1388
|
* `store_boolean_as_native` - if `true` Dynamoid stores boolean fields
|
1134
1389
|
as native DynamoDB boolean values. Otherwise boolean fields are stored
|
1135
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`
|
1136
1394
|
* `backoff` - is a hash: key is a backoff strategy (symbol), value is
|
1137
1395
|
parameters for the strategy. Is used in batch operations. Default id
|
1138
1396
|
`nil`
|
@@ -1322,6 +1580,7 @@ order to troubleshoot and debug issues just set it:
|
|
1322
1580
|
```ruby
|
1323
1581
|
class User
|
1324
1582
|
include Dynamoid::Document
|
1583
|
+
|
1325
1584
|
field name
|
1326
1585
|
end
|
1327
1586
|
|
@@ -1350,6 +1609,7 @@ just as accessible to the Ruby world as MongoDB.
|
|
1350
1609
|
Also, without contributors the project wouldn't be nearly as awesome. So
|
1351
1610
|
many thanks to:
|
1352
1611
|
|
1612
|
+
* [Chris Hobbs](https://github.com/ckhsponge)
|
1353
1613
|
* [Logan Bowers](https://github.com/loganb)
|
1354
1614
|
* [Lane LaRue](https://github.com/luxx)
|
1355
1615
|
* [Craig Heneveld](https://github.com/cheneveld)
|
data/dynamoid.gemspec
CHANGED
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
|
|
29
29
|
|
30
30
|
spec.description = "Dynamoid is an ORM for Amazon's DynamoDB that supports offline development, associations, querying, and everything else you'd expect from an ActiveRecord-style replacement."
|
31
31
|
spec.summary = "Dynamoid is an ORM for Amazon's DynamoDB"
|
32
|
-
# Ignore not
|
32
|
+
# Ignore not committed files
|
33
33
|
spec.files = Dir[
|
34
34
|
'CHANGELOG.md',
|
35
35
|
'dynamoid.gemspec',
|
@@ -51,9 +51,9 @@ Gem::Specification.new do |spec|
|
|
51
51
|
spec.metadata['wiki_uri'] = 'https://github.com/Dynamoid/dynamoid/wiki'
|
52
52
|
spec.metadata['rubygems_mfa_required'] = 'true'
|
53
53
|
|
54
|
-
spec.
|
55
|
-
spec.
|
56
|
-
spec.
|
54
|
+
spec.add_dependency 'activemodel', '>=4'
|
55
|
+
spec.add_dependency 'aws-sdk-dynamodb', '~> 1.0'
|
56
|
+
spec.add_dependency 'concurrent-ruby', '>= 1.0'
|
57
57
|
|
58
58
|
spec.add_development_dependency 'appraisal'
|
59
59
|
spec.add_development_dependency 'bundler'
|
data/lib/dynamoid/adapter.rb
CHANGED
@@ -118,7 +118,7 @@ module Dynamoid
|
|
118
118
|
# @param [Hash] query a hash of attributes: matching records will be returned by the scan
|
119
119
|
#
|
120
120
|
# @since 0.2.0
|
121
|
-
def scan(table, query =
|
121
|
+
def scan(table, query = [], opts = {})
|
122
122
|
benchmark('Scan', table, query) { adapter.scan(table, query, opts) }
|
123
123
|
end
|
124
124
|
|
@@ -20,48 +20,83 @@ module Dynamoid
|
|
20
20
|
private
|
21
21
|
|
22
22
|
def build
|
23
|
-
clauses =
|
23
|
+
clauses = []
|
24
|
+
|
25
|
+
@conditions.each do |conditions|
|
26
|
+
if conditions.is_a? Hash
|
27
|
+
clauses << build_for_hash(conditions) unless conditions.empty?
|
28
|
+
elsif conditions.is_a? Array
|
29
|
+
query, placeholders = conditions
|
30
|
+
clauses << build_for_string(query, placeholders)
|
31
|
+
else
|
32
|
+
raise ArgumentError, "expected Hash or Array but actual value is #{conditions}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
@expression = clauses.join(' AND ')
|
37
|
+
end
|
38
|
+
|
39
|
+
def build_for_hash(hash)
|
40
|
+
clauses = hash.map do |name, attribute_conditions|
|
24
41
|
attribute_conditions.map do |operator, value|
|
25
|
-
|
42
|
+
# replace attribute names with placeholders unconditionally to support
|
43
|
+
# - special characters (e.g. '.', ':', and '#') and
|
44
|
+
# - leading '_'
|
45
|
+
# See
|
46
|
+
# - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.NamingRules
|
47
|
+
# - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ExpressionAttributeNames.html#Expressions.ExpressionAttributeNames.AttributeNamesContainingSpecialCharacters
|
48
|
+
name_placeholder = name_placeholder_for(name)
|
26
49
|
|
27
50
|
case operator
|
28
51
|
when :eq
|
29
|
-
"#{
|
52
|
+
"#{name_placeholder} = #{value_placeholder_for(value)}"
|
30
53
|
when :ne
|
31
|
-
"#{
|
54
|
+
"#{name_placeholder} <> #{value_placeholder_for(value)}"
|
32
55
|
when :gt
|
33
|
-
"#{
|
56
|
+
"#{name_placeholder} > #{value_placeholder_for(value)}"
|
34
57
|
when :lt
|
35
|
-
"#{
|
58
|
+
"#{name_placeholder} < #{value_placeholder_for(value)}"
|
36
59
|
when :gte
|
37
|
-
"#{
|
60
|
+
"#{name_placeholder} >= #{value_placeholder_for(value)}"
|
38
61
|
when :lte
|
39
|
-
"#{
|
62
|
+
"#{name_placeholder} <= #{value_placeholder_for(value)}"
|
40
63
|
when :between
|
41
|
-
"#{
|
64
|
+
"#{name_placeholder} BETWEEN #{value_placeholder_for(value[0])} AND #{value_placeholder_for(value[1])}"
|
42
65
|
when :begins_with
|
43
|
-
"begins_with (#{
|
66
|
+
"begins_with (#{name_placeholder}, #{value_placeholder_for(value)})"
|
44
67
|
when :in
|
45
68
|
list = value.map(&method(:value_placeholder_for)).join(' , ')
|
46
|
-
"#{
|
69
|
+
"#{name_placeholder} IN (#{list})"
|
47
70
|
when :contains
|
48
|
-
"contains (#{
|
71
|
+
"contains (#{name_placeholder}, #{value_placeholder_for(value)})"
|
49
72
|
when :not_contains
|
50
|
-
"NOT contains (#{
|
73
|
+
"NOT contains (#{name_placeholder}, #{value_placeholder_for(value)})"
|
51
74
|
when :null
|
52
|
-
"attribute_not_exists (#{
|
75
|
+
"attribute_not_exists (#{name_placeholder})"
|
53
76
|
when :not_null
|
54
|
-
"attribute_exists (#{
|
77
|
+
"attribute_exists (#{name_placeholder})"
|
55
78
|
end
|
56
79
|
end
|
57
80
|
end.flatten
|
58
81
|
|
59
|
-
|
82
|
+
if clauses.empty?
|
83
|
+
nil
|
84
|
+
else
|
85
|
+
clauses.join(' AND ')
|
86
|
+
end
|
60
87
|
end
|
61
88
|
|
62
|
-
def
|
63
|
-
|
89
|
+
def build_for_string(query, placeholders)
|
90
|
+
placeholders.each do |(k, v)|
|
91
|
+
k = k.to_s
|
92
|
+
k = ":#{k}" unless k.start_with?(':')
|
93
|
+
@value_placeholders[k] = v
|
94
|
+
end
|
95
|
+
|
96
|
+
"(#{query})"
|
97
|
+
end
|
64
98
|
|
99
|
+
def name_placeholder_for(name)
|
65
100
|
placeholder = @name_placeholder_sequence.call
|
66
101
|
@name_placeholders[placeholder] = name
|
67
102
|
placeholder
|
@@ -97,10 +97,11 @@ module Dynamoid
|
|
97
97
|
|
98
98
|
private
|
99
99
|
|
100
|
-
#
|
100
|
+
# It's a single low level component available in a public API (with
|
101
|
+
# Document#update/#update! methods). So duplicate sanitizing to some
|
102
|
+
# degree.
|
101
103
|
#
|
102
|
-
#
|
103
|
-
# attribute value is nil or not.
|
104
|
+
# Keep in sync with AwsSdkV3.sanitize_item.
|
104
105
|
def sanitize_attributes(attributes)
|
105
106
|
# rubocop:disable Lint/DuplicateBranch
|
106
107
|
attributes.transform_values do |v|
|
@@ -108,7 +109,7 @@ module Dynamoid
|
|
108
109
|
v.stringify_keys
|
109
110
|
elsif v.is_a?(Set) && v.empty?
|
110
111
|
nil
|
111
|
-
elsif v.is_a?(String) && v.empty?
|
112
|
+
elsif v.is_a?(String) && v.empty? && Config.store_empty_string_as_nil
|
112
113
|
nil
|
113
114
|
else
|
114
115
|
v
|
@@ -21,13 +21,15 @@ module Dynamoid
|
|
21
21
|
return if @names.nil? || @names.empty?
|
22
22
|
|
23
23
|
clauses = @names.map do |name|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
24
|
+
# replace attribute names with placeholders unconditionally to support
|
25
|
+
# - special characters (e.g. '.', ':', and '#') and
|
26
|
+
# - leading '_'
|
27
|
+
# See
|
28
|
+
# - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.NamingRules
|
29
|
+
# - https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ExpressionAttributeNames.html#Expressions.ExpressionAttributeNames.AttributeNamesContainingSpecialCharacters
|
30
|
+
placeholder = @name_placeholder_sequence.call
|
31
|
+
@name_placeholders[placeholder] = name
|
32
|
+
placeholder
|
31
33
|
end
|
32
34
|
|
33
35
|
@expression = clauses.join(' , ')
|
@@ -69,7 +69,7 @@ module Dynamoid
|
|
69
69
|
limit = [record_limit, scan_limit, batch_size].compact.min
|
70
70
|
|
71
71
|
# key condition expression
|
72
|
-
convertor = FilterExpressionConvertor.new(@key_conditions, name_placeholders, value_placeholders, name_placeholder_sequence, value_placeholder_sequence)
|
72
|
+
convertor = FilterExpressionConvertor.new([@key_conditions], name_placeholders, value_placeholders, name_placeholder_sequence, value_placeholder_sequence)
|
73
73
|
key_condition_expression = convertor.expression
|
74
74
|
value_placeholders = convertor.value_placeholders
|
75
75
|
name_placeholders = convertor.name_placeholders
|
@@ -13,7 +13,7 @@ module Dynamoid
|
|
13
13
|
class Scan
|
14
14
|
attr_reader :client, :table, :conditions, :options
|
15
15
|
|
16
|
-
def initialize(client, table, conditions =
|
16
|
+
def initialize(client, table, conditions = [], options = {})
|
17
17
|
@client = client
|
18
18
|
@table = table
|
19
19
|
@conditions = conditions
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Prepare all the actions of the transaction for sending to the AWS SDK.
|
4
|
+
module Dynamoid
|
5
|
+
module AdapterPlugin
|
6
|
+
class AwsSdkV3
|
7
|
+
class Transact
|
8
|
+
attr_reader :client
|
9
|
+
|
10
|
+
def initialize(client)
|
11
|
+
@client = client
|
12
|
+
end
|
13
|
+
|
14
|
+
# Perform all of the item actions in a single transaction.
|
15
|
+
#
|
16
|
+
# @param [Array] items of type Dynamoid::Transaction::Action or
|
17
|
+
# any other object whose to_h is a transact_item hash
|
18
|
+
#
|
19
|
+
def transact_write_items(items)
|
20
|
+
transact_items = items.map(&:to_h)
|
21
|
+
params = {
|
22
|
+
transact_items: transact_items,
|
23
|
+
return_consumed_capacity: 'TOTAL',
|
24
|
+
return_item_collection_metrics: 'SIZE'
|
25
|
+
}
|
26
|
+
client.transact_write_items(params) # returns this
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|