Module: Dynamoid::Persistence

Extended by:
ActiveSupport::Concern
Included in:
Components
Defined in:
lib/dynamoid/persistence.rb,
lib/dynamoid/persistence/inc.rb,
lib/dynamoid/persistence/save.rb,
lib/dynamoid/persistence/import.rb,
lib/dynamoid/persistence/upsert.rb,
lib/dynamoid/persistence/update_fields.rb,
lib/dynamoid/persistence/update_validations.rb,
lib/dynamoid/persistence/item_updater_with_dumping.rb,
lib/dynamoid/persistence/item_updater_with_casting_and_dumping.rb

Overview

Persistence is responsible for dumping objects to and marshalling objects from the data store. It tries to reserialize values to be of the same type as when they were passed in, based on the fields in the class.

Defined Under Namespace

Modules: ClassMethods, UpdateValidations Classes: Import, Inc, ItemUpdaterWithCastingAndDumping, ItemUpdaterWithDumping, Save, UpdateFields, Upsert

Constant Summary collapse

UNIX_EPOCH_DATE =
Date.new(1970, 1, 1).freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#destroyedObject Also known as: destroyed?

Returns the value of attribute destroyed.



22
23
24
# File 'lib/dynamoid/persistence.rb', line 22

def destroyed
  @destroyed
end

#new_recordObject Also known as: new_record?

Returns the value of attribute new_record.



22
23
24
# File 'lib/dynamoid/persistence.rb', line 22

def new_record
  @new_record
end

Instance Method Details

#decrement(attribute, by = 1) ⇒ Dynamoid::Document

Change numeric attribute value.

Initializes attribute to zero if nil and subtracts the specified value (by default is 1). Only makes sense for number-based attributes.

user.decrement(:followers_count)
user.decrement(:followers_count, 2)

Parameters:

  • attribute (Symbol)

    attribute name

  • by (Numeric) (defaults to: 1)

    value to subtract (optional)

Returns:



972
973
974
# File 'lib/dynamoid/persistence.rb', line 972

def decrement(attribute, by = 1)
  increment(attribute, -by)
end

#decrement!(attribute, by = 1, touch: nil) ⇒ Dynamoid::Document

Change numeric attribute value and save a model.

Initializes attribute to zero if nil and subtracts the specified value (by default is 1). Only makes sense for number-based attributes.

user.decrement!(:followers_count)
user.decrement!(:followers_count, 2)

Only ‘attribute` is saved. The model itself is not saved. So any other modified attributes will still be dirty. Validations and callbacks are skipped.

When ‘:touch` option is passed the timestamp columns are updating. If attribute names are passed, they are updated along with updated_at attribute:

user.decrement!(:followers_count, touch: true)
user.decrement!(:followers_count, touch: :viewed_at)
user.decrement!(:followers_count, touch: [:viewed_at, :accessed_at])

Parameters:

  • attribute (Symbol)

    attribute name

  • by (Numeric) (defaults to: 1)

    value to subtract (optional)

  • touch (true | Symbol | Array<Symbol>) (defaults to: nil)

    to update update_at attribute and optionally the specified ones

Returns:



1000
1001
1002
# File 'lib/dynamoid/persistence.rb', line 1000

def decrement!(attribute, by = 1, touch: nil)
  increment!(attribute, -by, touch: touch)
end

#deleteDynamoid::Document

Delete a model.

Supports optimistic locking with the lock_version attribute and doesn’t delete a model if it’s already changed.

Raises Dynamoid::Errors::StaleObjectError exception if cannot delete a model.

Raises Dynamoid::Errors::MissingHashKey if a partition key has value nil and raises Dynamoid::Errors::MissingRangeKey if a sort key is required but has value nil.

Returns:

Since:

  • 0.2.0



1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
# File 'lib/dynamoid/persistence.rb', line 1060

def delete
  raise Dynamoid::Errors::MissingHashKey if hash_key.nil?
  raise Dynamoid::Errors::MissingRangeKey if self.class.range_key? && range_value.nil?

  options = range_key ? { range_key: Dumping.dump_field(read_attribute(range_key), self.class.attributes[range_key]) } : {}
  partition_key_dumped = Dumping.dump_field(hash_key, self.class.attributes[self.class.hash_key])

  # Add an optimistic locking check if the lock_version column exists
  if self.class.attributes[:lock_version]
    conditions = { if: {} }
    conditions[:if][:lock_version] =
      if changes[:lock_version].nil?
        lock_version
      else
        changes[:lock_version][0]
      end
    options[:conditions] = conditions
  end

  @destroyed = true

  Dynamoid.adapter.delete(self.class.table_name, partition_key_dumped, options)

  self.class.associations.each_key do |name|
    send(name).disassociate_source
  end

  self
rescue Dynamoid::Errors::ConditionalCheckFailedException
  raise Dynamoid::Errors::StaleObjectError.new(self, 'delete')
end

#destroyDynamoid::Document|false

Delete a model.

Runs callbacks.

Supports optimistic locking with the lock_version attribute and doesn’t delete a model if it’s already changed.

Returns self if deleted successfully and false otherwise.

Raises Dynamoid::Errors::MissingHashKey if a partition key has value nil and raises Dynamoid::Errors::MissingRangeKey if a sort key is required but has value nil.

Returns:

Since:

  • 0.2.0



1019
1020
1021
1022
1023
1024
1025
1026
1027
# File 'lib/dynamoid/persistence.rb', line 1019

def destroy
  ret = run_callbacks(:destroy) do
    delete
  end

  @destroyed = true

  ret == false ? false : self
end

#destroy!Object

Delete a model.

Runs callbacks.

Supports optimistic locking with the lock_version attribute and doesn’t delete a model if it’s already changed.

Raises Dynamoid::Errors::RecordNotDestroyed exception if model deleting failed.

Raises Dynamoid::Errors::MissingHashKey if a partition key has value nil and raises Dynamoid::Errors::MissingRangeKey if a sort key is required but has value nil.



1042
1043
1044
# File 'lib/dynamoid/persistence.rb', line 1042

def destroy!
  destroy || (raise Dynamoid::Errors::RecordNotDestroyed, self)
end

#increment(attribute, by = 1) ⇒ Dynamoid::Document

Change numeric attribute value.

Initializes attribute to zero if nil and adds the specified value (by default is 1). Only makes sense for number-based attributes.

user.increment(:followers_count)
user.increment(:followers_count, 2)

Parameters:

  • attribute (Symbol)

    attribute name

  • by (Numeric) (defaults to: 1)

    value to add (optional)

Returns:



919
920
921
922
923
# File 'lib/dynamoid/persistence.rb', line 919

def increment(attribute, by = 1)
  self[attribute] ||= 0
  self[attribute] += by
  self
end

#increment!(attribute, by = 1, touch: nil) ⇒ Dynamoid::Document

Change numeric attribute value and save a model.

Initializes attribute to zero if nil and adds the specified value (by default is 1). Only makes sense for number-based attributes.

user.increment!(:followers_count)
user.increment!(:followers_count, 2)

Only ‘attribute` is saved. The model itself is not saved. So any other modified attributes will still be dirty. Validations and callbacks are skipped.

When ‘:touch` option is passed the timestamp columns are updating. If attribute names are passed, they are updated along with updated_at attribute:

user.increment!(:followers_count, touch: true)
user.increment!(:followers_count, touch: :viewed_at)
user.increment!(:followers_count, touch: [:viewed_at, :accessed_at])

Parameters:

  • attribute (Symbol)

    attribute name

  • by (Numeric) (defaults to: 1)

    value to add (optional)

  • touch (true | Symbol | Array<Symbol>) (defaults to: nil)

    to update update_at attribute and optionally the specified ones

Returns:



949
950
951
952
953
954
955
956
957
958
959
# File 'lib/dynamoid/persistence.rb', line 949

def increment!(attribute, by = 1, touch: nil)
  increment(attribute, by)
  change = read_attribute(attribute) - (attribute_was(attribute) || 0)

  run_callbacks :touch do
    self.class.inc(hash_key, range_value, attribute => change, touch: touch)
    clear_attribute_changes(attribute)
  end

  self
end

#persisted?true|false

Is this object persisted in DynamoDB?

user = User.new
user.persisted? # => false

user.save
user.persisted? # => true

Returns:

  • (true|false)

Since:

  • 0.2.0



509
510
511
# File 'lib/dynamoid/persistence.rb', line 509

def persisted?
  !(new_record? || @destroyed)
end

#save(options = {}) ⇒ true|false

Create new model or persist changes.

Run the validation and callbacks. Returns true if saving is successful and false otherwise.

user = User.new
user.save # => true

user.age = 26
user.save # => true

Validation can be skipped with validate: false option:

user = User.new(age: -1)
user.save(validate: false) # => true

save by default sets timestamps attributes - created_at and updated_at when creates new model and updates updated_at attribute when updates already existing one.

Changing updated_at attribute at updating a model can be skipped with touch: false option:

user.save(touch: false)

If a model is new and hash key (id by default) is not assigned yet it was assigned implicitly with random UUID value.

If lock_version attribute is declared it will be incremented. If it’s blank then it will be initialized with 1.

save method call raises Dynamoid::Errors::RecordNotUnique exception if primary key (hash key + optional range key) already exists in a table.

save method call raises Dynamoid::Errors::StaleObjectError exception if there is lock_version attribute and the document in a table was already changed concurrently and lock_version was consequently increased.

Raises Dynamoid::Errors::MissingHashKey if a model is already persisted and a partition key has value nil and raises Dynamoid::Errors::MissingRangeKey if a sort key is required but has value nil.

When a table is not created yet the first save method call will create a table. It’s useful in test environment to avoid explicit table creation.

Parameters:

  • options (Hash) (defaults to: {})

    (optional)

Options Hash (options):

  • :validate (true|false)

    validate a model or not - true by default (optional)

  • :touch (true|false)

    update tiemstamps fields or not - true by default (optional)

Returns:

  • (true|false)

    Whether saving successful or not

Since:

  • 0.2.0



567
568
569
570
571
572
573
574
575
576
577
578
579
# File 'lib/dynamoid/persistence.rb', line 567

def save(options = {})
  if Dynamoid.config.create_table_on_save
    self.class.create_table(sync: true)
  end

  create_or_update = new_record? ? :create : :update

  run_callbacks(:save) do
    run_callbacks(create_or_update) do
      Save.call(self, touch: options[:touch])
    end
  end
end

#save!(options = {}) ⇒ true|false

Create new model or persist changes.

Run the validation and callbacks. Raises Dynamoid::Errors::DocumentNotValid is validation fails.

user = User.create

user.age = 26
user.save! # => user

Validation can be skipped with validate: false option:

user = User.new(age: -1)
user.save!(validate: false) # => user

save! by default sets timestamps attributes - created_at and updated_at when creates new model and updates updated_at attribute when updates already existing one.

Changing updated_at attribute at updating a model can be skipped with touch: false option:

user.save!(touch: false)

If a model is new and hash key (id by default) is not assigned yet it was assigned implicitly with random UUID value.

If lock_version attribute is declared it will be incremented. If it’s blank then it will be initialized with 1.

save! method call raises Dynamoid::Errors::RecordNotUnique exception if primary key (hash key + optional range key) already exists in a table.

save! method call raises Dynamoid::Errors::StaleObjectError exception if there is lock_version attribute and the document in a table was already changed concurrently and lock_version was consequently increased.

save! method call raises Dynamoid::Errors::RecordNotSaved exception if some callback aborted execution.

Raises Dynamoid::Errors::MissingHashKey if a model is already persisted and a partition key has value nil and raises Dynamoid::Errors::MissingRangeKey if a sort key is required but has value nil.

When a table is not created yet the first save! method call will create a table. It’s useful in test environment to avoid explicit table creation.

Parameters:

  • options (Hash) (defaults to: {})

    (optional)

Options Hash (options):

  • :validate (true|false)

    validate a model or not - true by default (optional)

  • :touch (true|false)

    update tiemstamps fields or not - true by default (optional)

Returns:

  • (true|false)

    Whether saving successful or not



636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
# File 'lib/dynamoid/persistence.rb', line 636

def save!(options = {})
  # validation is handled in the Validation module

  if Dynamoid.config.create_table_on_save
    self.class.create_table(sync: true)
  end

  create_or_update = new_record? ? :create : :update
  aborted = true

  run_callbacks(:save) do
    run_callbacks(create_or_update) do
      aborted = false
      Save.call(self, touch: options[:touch])
    end
  end

  if aborted
    raise Dynamoid::Errors::RecordNotSaved, self
  end

  self
end

#touch(*names, time: nil) ⇒ Dynamoid::Document

Update document timestamps.

Set updated_at attribute to current DateTime.

post.touch

Can update other fields in addition with the same timestamp if their names passed as arguments.

user.touch(:last_login_at, :viewed_at)

Some specific value can be used to save:

user.touch(time: 1.hour.ago)

No validation is performed and only after_touch callback is called.

The method must be used on a persisted object, otherwise Dynamoid::Errors::Error will be thrown.

Parameters:

  • names (*Symbol)

    a list of attribute names to update (optional)

  • time (Time) (defaults to: nil)

    datetime value that can be used instead of the current time (optional)

Returns:



476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
# File 'lib/dynamoid/persistence.rb', line 476

def touch(*names, time: nil)
  if new_record?
    raise Dynamoid::Errors::Error, 'cannot touch on a new or destroyed record object'
  end

  time_to_assign = time || DateTime.now

  self.updated_at = time_to_assign
  names.each do |name|
    attributes[name] = time_to_assign
  end

  attribute_names = names.map(&:to_sym) + [:updated_at]
  attributes_with_values = attributes.slice(*attribute_names)

  run_callbacks :touch do
    self.class.update_fields(hash_key, range_value, attributes_with_values)
    clear_attribute_changes(attribute_names.map(&:to_s))
  end

  self
end

#update(conditions = {}, &block) ⇒ true|false

Update a model.

Doesn’t run validation. Runs only update callbacks. Reloads all attribute values.

Accepts mandatory block in order to specify operations which will modify attributes. Supports following operations: add, delete and set.

Operation add just adds a value for numeric attributes and join collections if attribute is a set.

user.update do |t|
  t.add(age: 1, followers_count: 5)
  t.add(hobbies: ['skying', 'climbing'])
end

Operation delete is applied to collection attribute types and substructs one collection from another.

user.update do |t|
  t.delete(hobbies: ['skying'])
end

If it’s applied to a scalar attribute then the item’s attribute is removed at all:

user.update do |t|
  t.delete(age: nil)
end

or even without useless value at all:

user.update do |t|
  t.delete(:age)
end

Operation set just changes an attribute value:

user.update do |t|
  t.set(age: 21)
end

All the operations works like ADD, DELETE and PUT actions supported by AttributeUpdates parameter of UpdateItem operation.

Can update a model conditionaly:

user.update(if: { age: 20 }) do |t|
  t.add(age: 1)
end

To check if some attribute (or attributes) isn’t stored in a DynamoDB item (e.g. it wasn’t set explicitly) there is another condition - unless_exists:

user = User.create(name: 'Tylor')
user.update(unless_exists: [:age]) do |t|
  t.set(age: 18)
end

If a document doesn’t meet conditions it just returns false. Otherwise it returns true.

It will increment the lock_version attribute if a table has the column, but will not check it. Thus, a concurrent save call will never cause an update! to fail, but an update! may cause a concurrent save to fail.

Parameters:

  • conditions (Hash) (defaults to: {})

    Conditions on model attributes to make a conditional update (optional)

Returns:

  • (true|false)
    • whether conditions are met and updating is successful



901
902
903
904
905
906
# File 'lib/dynamoid/persistence.rb', line 901

def update(conditions = {}, &block)
  update!(conditions, &block)
  true
rescue Dynamoid::Errors::StaleObjectError
  false
end

#update!(conditions = {}) ⇒ Dynamoid::Document

Update a model.

Doesn’t run validation. Runs only update callbacks. Reloads all attribute values.

Accepts mandatory block in order to specify operations which will modify attributes. Supports following operations: add, delete and set.

Operation add just adds a value for numeric attributes and join collections if attribute is a set.

user.update! do |t|
  t.add(age: 1, followers_count: 5)
  t.add(hobbies: ['skying', 'climbing'])
end

Operation delete is applied to collection attribute types and substructs one collection from another.

user.update! do |t|
  t.delete(hobbies: ['skying'])
end

Operation set just changes an attribute value:

user.update! do |t|
  t.set(age: 21)
end

All the operations work like ADD, DELETE and PUT actions supported by AttributeUpdates parameter of UpdateItem operation.

It’s atomic operations. So adding or deleting elements in a collection or incrementing or decrementing a numeric field is atomic and does not interfere with other write requests.

Can update a model conditionaly:

user.update!(if: { age: 20 }) do |t|
  t.add(age: 1)
end

To check if some attribute (or attributes) isn’t stored in a DynamoDB item (e.g. it wasn’t set explicitly) there is another condition - unless_exists:

user = User.create(name: 'Tylor')
user.update!(unless_exists: [:age]) do |t|
  t.set(age: 18)
end

If a document doesn’t meet conditions it raises Dynamoid::Errors::StaleObjectError exception.

It will increment the lock_version attribute if a table has the column, but will not check it. Thus, a concurrent save call will never cause an update! to fail, but an update! may cause a concurrent save to fail.

Parameters:

  • conditions (Hash) (defaults to: {})

    Conditions on model attributes to make a conditional update (optional)

Returns:



786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
# File 'lib/dynamoid/persistence.rb', line 786

def update!(conditions = {})
  run_callbacks(:update) do
    options = {}
    if range_key
      value = read_attribute(range_key)
      attribute_options = self.class.attributes[range_key]
      options[:range_key] = Dumping.dump_field(value, attribute_options)
    end

    begin
      table_name = self.class.table_name
      partition_key_dumped = Dumping.dump_field(hash_key, self.class.attributes[self.class.hash_key])
      conditions = conditions.dup
      conditions[:if] ||= {}
      conditions[:if][self.class.hash_key] = partition_key_dumped
      if self.class.range_key
        sort_key_dumped = Dumping.dump_field(range_value, self.class.attributes[self.class.range_key])
        conditions[:if][self.class.range_key] = sort_key_dumped
      end

      update_item_options = options.merge(conditions: conditions)

      new_attrs = Dynamoid.adapter.update_item(table_name, partition_key_dumped, update_item_options) do |t|
        item_updater = ItemUpdaterWithDumping.new(self.class, t)

        item_updater.add(lock_version: 1) if self.class.attributes[:lock_version]

        if self.class.timestamps_enabled?
          item_updater.set(updated_at: DateTime.now.in_time_zone(Time.zone))
        end

        yield t
      end
      load(Undumping.undump_attributes(new_attrs, self.class.attributes))
    rescue Dynamoid::Errors::ConditionalCheckFailedException
      # exception may be raised either because of failed user provided conditions
      # or because of conditions on partition and sort keys. We cannot
      # distinguish these two cases.
      raise Dynamoid::Errors::StaleObjectError.new(self, 'update')
    end
  end

  self
end

#update_attribute(attribute, value) ⇒ Dynamoid::Document

Update a single attribute, saving the object afterwards.

Returns true if saving is successful and false otherwise.

user.update_attribute(:last_name, 'Tylor')

Validation is skipped.

Raises a Dynamoid::Errors::UnknownAttribute exception if any of the attributes is not on the model

Parameters:

  • attribute (Symbol)

    attribute name to update

  • value (Object)

    the value to assign it

Returns:

Since:

  • 0.2.0



720
721
722
# File 'lib/dynamoid/persistence.rb', line 720

def update_attribute(attribute, value)
  # final implementation is in the Dynamoid::Validation module
end

#update_attributes(attributes) ⇒ true|false

Update multiple attributes at once, saving the object once the updates are complete.

Returns true if saving is successful and false otherwise.

user.update_attributes(age: 27, last_name: 'Tylor')

Raises a Dynamoid::Errors::UnknownAttribute exception if any of the attributes is not on the model

Raises Dynamoid::Errors::MissingHashKey if a partition key has value nil and raises Dynamoid::Errors::MissingRangeKey if a sort key is required but has value nil.

Parameters:

  • attributes (Hash)

    a hash of attributes to update

Returns:

  • (true|false)

    Whether updating successful or not

Since:

  • 0.2.0



678
679
680
681
# File 'lib/dynamoid/persistence.rb', line 678

def update_attributes(attributes)
  attributes.each { |attribute, value| write_attribute(attribute, value) }
  save
end

#update_attributes!(attributes) ⇒ Object

Update multiple attributes at once, saving the object once the updates are complete.

user.update_attributes!(age: 27, last_name: 'Tylor')

Raises a Dynamoid::Errors::DocumentNotValid exception if some vaidation fails.

Raises a Dynamoid::Errors::UnknownAttribute exception if any of the attributes is not on the model

Raises Dynamoid::Errors::MissingHashKey if a partition key has value nil and raises Dynamoid::Errors::MissingRangeKey if a sort key is required but has value nil.

Parameters:

  • attributes (Hash)

    a hash of attributes to update



699
700
701
702
# File 'lib/dynamoid/persistence.rb', line 699

def update_attributes!(attributes)
  attributes.each { |attribute, value| write_attribute(attribute, value) }
  save!
end