Skip to content

Commit 29b6fcd

Browse files
ckhspongeandrykonchin
authored andcommitted
TransactionWrite supports update_fields() with a block for set, add and delete operations on fields
1 parent 41f015a commit 29b6fcd

File tree

5 files changed

+357
-7
lines changed

5 files changed

+357
-7
lines changed

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ DynamoDB is not like other document-based databases you might know, and
2424
is very different indeed from relational databases. It sacrifices
2525
anything beyond the simplest relational queries and transactional
2626
support to provide a fast, cost-efficient, and highly durable storage
27-
solution. If your database requires complicated relational queries and
28-
transaction support, then this modest Gem cannot provide them for you,
27+
solution. If your database requires complicated relational queries
28+
then this modest Gem cannot provide them for you
2929
and neither can DynamoDB. In those cases you would do better to look
3030
elsewhere for your database needs.
3131

@@ -1187,6 +1187,19 @@ Dynamoid::TransactionWrite.execute do |txn|
11871187
# sets the name and title for a user
11881188
# The user is found by id (that equals 1)
11891189
txn.update_fields(User, '1', name: 'bob', title: 'mister')
1190+
1191+
# sets the name, increments a count and deletes a field
1192+
txn.update_fields(User, 1) do |u1| # a User instance is provided
1193+
u1.set(name: 'bob')
1194+
u1.add(article_count: 1)
1195+
u1.delete(:title)
1196+
end
1197+
1198+
# adds to a set of integers and deletes from a set of strings
1199+
txn.update_fields(User, 2) do |u2|
1200+
u2.add(friend_ids: [1, 2])
1201+
u2.delete(child_names: ['bebe'])
1202+
end
11901203
end
11911204
```
11921205

lib/dynamoid/transaction_write.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
require 'dynamoid/transaction_write/update_fields'
99
require 'dynamoid/transaction_write/update_attributes'
1010
require 'dynamoid/transaction_write/upsert'
11+
require 'dynamoid/transaction_write/item_updater'
1112

1213
module Dynamoid
1314
# The class +TransactionWrite+ provides means to perform multiple modifying
@@ -345,6 +346,16 @@ def upsert(model_class, hash_key, range_key = nil, attributes) # rubocop:disable
345346
# t.update_fields(User, '1', 'Tylor', age: 26)
346347
# end
347348
#
349+
# Updates can also be performed in a block.
350+
# Dynamoid::TransactionWrite.execute do |t|
351+
# t.update_fields(User, 1) do |u|
352+
# u.set age: 27, last_name: 'Tylor'
353+
# u.add article_count: 1 # increment a counter
354+
# u.delete favorite_colors: 'green' # remove from a set
355+
# u.delete :first_name # clear a field
356+
# end
357+
# end
358+
#
348359
# Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
349360
# attributes is not declared in the model class.
350361
#
@@ -353,8 +364,14 @@ def upsert(model_class, hash_key, range_key = nil, attributes) # rubocop:disable
353364
# @param range_key [Scalar value] range key value (optional)
354365
# @param attributes [Hash]
355366
# @return [nil]
356-
def update_fields(model_class, hash_key, range_key = nil, attributes) # rubocop:disable Style/OptionalArguments
367+
def update_fields(model_class, hash_key, range_key = nil, attributes = nil)
368+
if attributes.nil? && range_key.is_a?(Hash)
369+
attributes = range_key
370+
range_key = nil
371+
end
357372
action = Dynamoid::TransactionWrite::UpdateFields.new(model_class, hash_key, range_key, attributes)
373+
# cannot pass in &block due to code climate max-args limit so instead yield it here
374+
yield(action) if block_given?
358375
register_action action
359376
end
360377

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# frozen_string_literal: true
2+
3+
module Dynamoid
4+
class TransactionWrite
5+
class ItemUpdater
6+
def initialize
7+
@additions = {}
8+
@deletions = {}
9+
@removals = []
10+
end
11+
12+
def empty?
13+
@additions.empty? && @deletions.empty? && @removals.empty?
14+
end
15+
16+
# adds to array of fields for use in REMOVE update expression
17+
def remove(field)
18+
@removals << field
19+
end
20+
21+
# increments a number or adds to a set, starts at 0 or [] if it doesn't yet exist
22+
def add(values)
23+
@additions.merge!(values)
24+
end
25+
26+
# deletes a value or values from a set type or simply sets a field to nil
27+
def delete(field_or_values)
28+
if field_or_values.is_a?(Hash)
29+
@deletions.merge!(field_or_values)
30+
else
31+
remove(field_or_values)
32+
end
33+
end
34+
35+
def merge_update_expression(expression_attribute_names, expression_attribute_values, update_expression)
36+
update_expression = set_additions(expression_attribute_values, update_expression)
37+
update_expression = set_deletions(expression_attribute_values, update_expression)
38+
expression_attribute_names, update_expression = set_removals(expression_attribute_names, update_expression)
39+
[expression_attribute_names, update_expression]
40+
end
41+
42+
# adds all of the ADD statements to the update_expression and returns it
43+
def set_additions(expression_attribute_values, update_expression)
44+
return update_expression unless @additions.present?
45+
46+
# ADD statements can be used to increment a counter:
47+
# txn.update!(UserCount, "UserCount#Red", {}, options: {add: {record_count: 1}})
48+
add_keys = @additions.keys
49+
add_values = transform_add_values(@additions)
50+
51+
update_expression += build_add_expression(add_keys)
52+
add_keys.each_with_index { |k, i| expression_attribute_values[":_a#{i}"] = add_values[k] }
53+
update_expression
54+
end
55+
56+
def transform_add_values(additions)
57+
additions.transform_values do |v|
58+
!v.is_a?(Set) && v.is_a?(Enumerable) ? Set.new(v) : v
59+
end
60+
end
61+
62+
def build_add_expression(add_keys)
63+
" ADD #{add_keys.each_with_index.map { |k, i| "#{k} :_a#{i}" }.join(', ')}"
64+
end
65+
66+
# adds all of the DELETE statements to the update_expression and returns it
67+
def set_deletions(expression_attribute_values, update_expression)
68+
return update_expression unless @deletions.present?
69+
70+
update_expression += build_delete_expression(@deletions.keys)
71+
delete_values = prepare_delete_values(@deletions)
72+
73+
@deletions.keys.each_with_index do |key, index|
74+
expression_attribute_values[":_d#{index}"] = delete_values[key]
75+
end
76+
77+
update_expression
78+
end
79+
80+
def build_delete_expression(delete_keys)
81+
" DELETE #{delete_keys.each_with_index.map { |key, index| "#{key} :_d#{index}" }.join(', ')}"
82+
end
83+
84+
def prepare_delete_values(deletions)
85+
deletions.transform_values(&method(:prepare_delete_values_transform))
86+
end
87+
88+
def prepare_delete_values_transform(value)
89+
if value.is_a?(Set)
90+
value
91+
else
92+
Set.new(value.is_a?(Enumerable) ? value : [value])
93+
end
94+
end
95+
96+
# adds all of the removals as a REMOVE clause
97+
def set_removals(expression_attribute_names, update_expression)
98+
return expression_attribute_names, update_expression unless @removals.present?
99+
100+
update_expression += " REMOVE #{@removals.each_with_index.map { |_k, i| "#_r#{i}" }.join(', ')}"
101+
expression_attribute_names = expression_attribute_names.merge(
102+
@removals.each_with_index.map { |k, i| ["#_r#{i}", k.to_s] }.to_h
103+
)
104+
[expression_attribute_names, update_expression]
105+
end
106+
end
107+
end
108+
end

lib/dynamoid/transaction_write/update_fields.rb

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ def initialize(model_class, hash_key, range_key, attributes)
1212
@model_class = model_class
1313
@hash_key = hash_key
1414
@range_key = range_key
15-
@attributes = attributes
15+
@attributes = attributes || {}
16+
17+
@item_updater = ItemUpdater.new
18+
19+
yield(self) if block_given?
1620
end
1721

1822
def on_registration
@@ -29,13 +33,33 @@ def aborted?
2933
end
3034

3135
def skipped?
32-
@attributes.empty?
36+
@attributes.empty? && @item_updater.empty?
3337
end
3438

3539
def observable_by_user_result
3640
nil
3741
end
3842

43+
# sets a value in the attributes
44+
def set(attributes)
45+
@attributes.merge!(attributes)
46+
end
47+
48+
# adds to array of fields for use in REMOVE update expression
49+
def remove(field)
50+
@item_updater.remove(field)
51+
end
52+
53+
# increments a number or adds to a set, starts at 0 or [] if it doesn't yet exist
54+
def add(values)
55+
@item_updater.add(values)
56+
end
57+
58+
# deletes a value or values from a set type or simply sets a field to nil
59+
def delete(field_or_values)
60+
@item_updater.delete(field_or_values)
61+
end
62+
3963
def action_request
4064
# changed attributes to persist
4165
changes = @attributes.dup
@@ -63,6 +87,10 @@ def action_request
6387

6488
update_expression = "SET #{update_expression_statements.join(', ')}"
6589

90+
expression_attribute_names, update_expression = @item_updater.merge_update_expression(
91+
expression_attribute_names, expression_attribute_values, update_expression
92+
)
93+
6694
# require primary key to exist
6795
condition_expression = "attribute_exists(#{@model_class.hash_key})"
6896
if @model_class.range_key?

0 commit comments

Comments
 (0)