Skip to content

Commit

Permalink
Send only changed attributes when save a persisted model
Browse files Browse the repository at this point in the history
  • Loading branch information
andrykonchin committed Feb 4, 2023
1 parent 80b07c4 commit 915d294
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 12 deletions.
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ GEM

PLATFORMS
x86_64-darwin-19
x86_64-darwin-21
x86_64-darwin-22
x86_64-linux

Expand Down
42 changes: 40 additions & 2 deletions lib/dynamoid/persistence/save.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,19 @@ def call
@model.lock_version = (@model.lock_version || 0) + 1
end

attributes_dumped = Dumping.dump_attributes(@model.attributes, @model.class.attributes)
Dynamoid.adapter.write(@model.class.table_name, attributes_dumped, conditions_for_write)
if @model.new_record?
attributes_dumped = Dumping.dump_attributes(@model.attributes, @model.class.attributes)
Dynamoid.adapter.write(@model.class.table_name, attributes_dumped, conditions_for_write)
else
attributes_to_persist = @model.attributes.slice(*@model.changed.map(&:to_sym))

Dynamoid.adapter.update_item(@model.class.table_name, @model.hash_key, options_to_update_item) do |t|
attributes_to_persist.each do |name, value|
value_dumped = Dumping.dump_field(value, @model.class.attributes[name])
t.set(name => value_dumped)
end
end
end

@model.new_record = false
true
Expand Down Expand Up @@ -68,6 +79,33 @@ def conditions_for_write

conditions
end

def options_to_update_item
options = {}

if @model.class.range_key
value_dumped = Dumping.dump_field(@model.range_value, @model.class.attributes[@model.class.range_key])
options[:range_key] = value_dumped
end

conditions = {}
conditions[:if_exists] ||= {}
conditions[:if_exists][@model.class.hash_key] = @model.hash_key

# Add an optimistic locking check if the lock_version column exists
if @model.class.attributes[:lock_version]
# Uses the original lock_version value from Dirty API
# in case user changed 'lock_version' manually
if @model.changes[:lock_version][0]
conditions[:if] ||= {}
conditions[:if][:lock_version] = @model.changes[:lock_version][0]
end
end

options[:conditions] = conditions

options
end
end
end
end
111 changes: 101 additions & 10 deletions spec/dynamoid/persistence_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1910,15 +1910,59 @@ def log_message
end
end

it 'saves model' do
let(:klass_with_range_key) do
new_class do
field :name
range :age, :integer
end
end

let(:klass_with_range_key_and_custom_type) do
new_class do
field :name
range :tags, :serialized
end
end

it 'persists new model' do
obj = klass.new(name: 'Alex')
obj.save

expect(klass.exists?(obj.id)).to eq true
expect(klass.find(obj.id).name).to eq 'Alex'
end

it 'marks it as persisted' do
it 'saves changes of already persisted model' do
obj = klass.create!(name: 'Alex')

obj.name = 'Michael'
obj.save

obj_loaded = klass.find(obj.id)
expect(obj_loaded.name).to eql 'Michael'
end

it 'saves changes of already persisted model if range key is declared' do
obj = klass_with_range_key.create!(name: 'Alex', age: 21)

obj.name = 'Michael'
obj.save

obj_loaded = klass_with_range_key.find(obj.id, range_key: obj.age)
expect(obj_loaded.name).to eql 'Michael'
end

it 'saves changes of already persisted model if range key is declared and its type is not supported by DynamoDB natively' do
obj = klass_with_range_key_and_custom_type.create!(name: 'Alex', tags: %w[a b])

obj.name = 'Michael'
obj.save

obj_loaded = klass_with_range_key_and_custom_type.find(obj.id, range_key: obj.tags)
expect(obj_loaded.name).to eql 'Michael'
end

it 'marks persisted new model as persisted' do
obj = klass.new(name: 'Alex')
expect { obj.save }.to change { obj.persisted? }.from(false).to(true)
end
Expand Down Expand Up @@ -1970,10 +2014,6 @@ def log_message
end

it 'does not make a request to persist a model if there is no any changed attribute' do
klass = new_class do
field :name
end

expect(Dynamoid.adapter).to receive(:write).and_call_original
obj = klass.create(name: 'Alex')

Expand All @@ -1986,17 +2026,68 @@ def log_message
end

it 'returns true if there is no any changed attribute' do
klass = new_class do
field :name
end

obj = klass.create(name: 'Alex')
obj_loaded = klass.find(obj.id)

expect(obj.save).to eql(true)
expect(obj_loaded.save).to eql(true)
end

it 'calls PutItem for a new record' do
expect(Dynamoid.adapter).to receive(:write).and_call_original
klass.create(name: 'Alex')
end

it 'calls UpdateItem for already persisted record' do
klass = new_class do
field :name
field :age, :integer
end

obj = klass.create!(name: 'Alex', age: 21)
obj.age = 31

expect(Dynamoid.adapter).to receive(:update_item).and_call_original
obj.save
end

it 'does not persist changes if a model was deleted' do
obj = klass.create!(name: 'Alex')
Dynamoid.adapter.delete_item(klass.table_name, obj.id)

obj.name = 'Michael'

expect do
expect { obj.save }.to raise_error(Dynamoid::Errors::StaleObjectError)
end.not_to change(klass, :count)
end

it 'does not persist changes if a model was deleted and range key is declared' do
obj = klass_with_range_key.create!(name: 'Alex', age: 21)
Dynamoid.adapter.delete_item(klass_with_range_key.table_name, obj.id, range_key: obj.age)

obj.name = 'Michael'

expect do
expect { obj.save }.to raise_error(Dynamoid::Errors::StaleObjectError)
end.not_to change(klass_with_range_key, :count)
end

it 'does not persist changes if a model was deleted, range key is declared and its type is not supported by DynamoDB natively' do
obj = klass_with_range_key_and_custom_type.create!(name: 'Alex', tags: %w[a b])
Dynamoid.adapter.delete_item(
obj.class.table_name,
obj.id,
range_key: Dynamoid::Dumping.dump_field(obj.tags, klass_with_range_key_and_custom_type.attributes[:tags])
)

obj.name = 'Michael'

expect do
expect { obj.save }.to raise_error(Dynamoid::Errors::StaleObjectError)
end.not_to change { obj.class.count }
end

describe 'partition key value' do
it 'generates "id" for new model' do
obj = klass.new
Expand Down

0 comments on commit 915d294

Please sign in to comment.