Module: Dynamoid::Persistence::ClassMethods

Defined in:
lib/dynamoid/persistence.rb

Instance Method Summary collapse

Instance Method Details

#create(attrs = {}, &block) ⇒ Dynamoid::Document

Create a model.

Initializes a new model and immediately saves it into DynamoDB.

User.create(first_name: 'Mark', last_name: 'Tyler')

Accepts both Hash and Array of Hashes and can create several models.

User.create([{ first_name: 'Alice' }, { first_name: 'Bob' }])

Instantiates a model and pass it into an optional block to set other attributes.

User.create(first_name: 'Mark') do |u|
  u.age = 21
end

Validates model and runs callbacks.

Raises Dynamoid::Errors::MissingRangeKey if a sort key is required but not specified or has value nil.

Parameters:

  • attrs (Hash|Array<Hash>) (defaults to: {})

    Attributes of a model

  • block (Proc)

    Block to process a document after initialization

Returns:

Since:

  • 0.2.0



202
203
204
205
206
207
208
# File 'lib/dynamoid/persistence.rb', line 202

def create(attrs = {}, &block)
  if attrs.is_a?(Array)
    attrs.map { |attr| create(attr, &block) }
  else
    build(attrs, &block).tap(&:save)
  end
end

#create!(attrs = {}, &block) ⇒ Dynamoid::Document

Create a model.

Initializes a new object and immediately saves it into DynamoDB.

User.create!(first_name: 'Mark', last_name: 'Tyler')

Raises an exception Dynamoid::Errors::DocumentNotValid if validation failed.

If any of the before_* callbacks throws :abort the creation is cancelled and create! raises Dynamoid::Errors::RecordNotSaved.

Accepts both Hash and Array of Hashes and can create several models.

User.create!([{ first_name: 'Alice' }, { first_name: 'Bob' }])

Instantiates a model and pass it into an optional block to set other attributes.

User.create!(first_name: 'Mark') do |u|
  u.age = 21
end

Validates model and runs callbacks.

Raises Dynamoid::Errors::MissingRangeKey if a sort key is required but not specified or has value nil.

Parameters:

  • attrs (Hash|Array<Hash>) (defaults to: {})

    Attributes with which to create the object.

  • block (Proc)

    Block to process a document after initialization

Returns:

Since:

  • 0.2.0



243
244
245
246
247
248
249
# File 'lib/dynamoid/persistence.rb', line 243

def create!(attrs = {}, &block)
  if attrs.is_a?(Array)
    attrs.map { |attr| create!(attr, &block) }
  else
    build(attrs, &block).tap(&:save!)
  end
end

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

Create a table.

Uses a configuration specified in a model class (with the table method) e.g. table name, schema (hash and range keys), global and local secondary indexes, billing mode and write/read capacity.

For instance here

class User
  include Dynamoid::Document

  table key: :uuid
  range :last_name

  field :first_name
  field :last_name
end

User.create_table

create_table method call will create a table dynamoid_users with hash key uuid and range key name, DynamoDB default billing mode and Dynamoid default read/write capacity units (100/20).

All the configuration can be overridden with options argument.

User.create_table(table_name: 'users', read_capacity: 200, write_capacity: 40)

Dynamoid creates a table synchronously by default. DynamoDB table creation is an asynchronous operation and a client should wait until a table status changes to ACTIVE and a table becomes available. That’s why Dynamoid is polling a table status and returns results only when a table becomes available.

Polling is configured with Dynamoid::Config.sync_retry_max_times and Dynamoid::Config.sync_retry_wait_seconds configuration options. If table creation takes more time than configured waiting time then Dynamoid stops polling and returns true.

In order to return back asynchronous behaviour and not to wait until a table is created the sync: false option should be specified.

User.create_table(sync: false)

Subsequent method calls for the same table will be ignored.

Parameters:

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

Options Hash (options):

  • :table_name (Symbol)

    name of the table

  • :id (Symbol)

    hash key name of the table

  • :hash_key_type (Symbol)

    Dynamoid type of the hash key - :string, :integer or any other scalar type

  • :range_key (Hash)

    a Hash with range key name and type in format { <name> => <type> } e.g. { last_name: :string }

  • :billing_mode (String)

    billing mode of a table - either PROVISIONED (default) or PAY_PER_REQUEST (for On-Demand Mode)

  • :read_capacity (Integer)

    read capacity units for the table; does not work on existing tables and is ignored when billing mode is PAY_PER_REQUEST

  • :write_capacity (Integer)

    write capacity units for the table; does not work on existing tables and is ignored when billing mode is PAY_PER_REQUEST

  • :local_secondary_indexes (Hash)
  • :global_secondary_indexes (Hash)
  • :sync (true|false)

    specifies should the method call be synchronous and wait until a table is completely created

Returns:

  • (true|false)

    Whether a table created successfully

Since:

  • 0.4.0



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/dynamoid/persistence.rb', line 104

def create_table(options = {})
  range_key_hash = if range_key
                     { range_key => PrimaryKeyTypeMapping.dynamodb_type(attributes[range_key][:type], attributes[range_key]) }
                   end

  options = {
    id: hash_key,
    table_name: table_name,
    billing_mode: capacity_mode,
    write_capacity: write_capacity,
    read_capacity: read_capacity,
    range_key: range_key_hash,
    hash_key_type: PrimaryKeyTypeMapping.dynamodb_type(attributes[hash_key][:type], attributes[hash_key]),
    local_secondary_indexes: local_secondary_indexes.values,
    global_secondary_indexes: global_secondary_indexes.values
  }.merge(options)

  created_successfuly = Dynamoid.adapter.create_table(options[:table_name], options[:id], options)

  if created_successfuly && self.options[:expires]
    attribute = self.options[:expires][:field]
    Dynamoid.adapter.update_time_to_live(options[:table_name], attribute)
  end

  self
end

#delete(*ids) ⇒ Object

Delete a model by a given primary key.

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 or is missing.

Examples:

Delete a model by given partition key:

User.delete(user_id)

Delete a model by given partition and sort keys:

User.delete(user_id, sort_key)

Delete multiple models by given partition keys:

User.delete([id1, id2, id3])

Delete multiple models by given partition and sort keys:

User.delete([[id1, sk1], [id2, sk2], [id3, sk3]])

Parameters:

  • ids (String|Array)

    primary key or an array of primary keys

Returns:

  • nil



482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
# File 'lib/dynamoid/persistence.rb', line 482

def delete(*ids)
  if ids.empty?
    raise Dynamoid::Errors::MissingHashKey
  end

  if ids[0].is_a?(Array)
    # given multiple keys
    #   Model.delete([id1, id2, id3])
    keys = ids[0] # ignore other arguments

    ids = []
    if self.range_key
      # compound primary key
      # expect [hash key, range key] pairs

      range_key = []

      # assume all elements are pairs, that's arrays
      keys.each do |pk, sk|
        raise Dynamoid::Errors::MissingHashKey if pk.nil?
        raise Dynamoid::Errors::MissingRangeKey if self.range_key && sk.nil?

        partition_key_dumped = cast_and_dump(hash_key, pk)
        sort_key_dumped = cast_and_dump(self.range_key, sk)

        ids << partition_key_dumped
        range_key << sort_key_dumped
      end
    else
      # simple primary key

      range_key = nil

      keys.each do |pk|
        raise Dynamoid::Errors::MissingHashKey if pk.nil?

        partition_key_dumped = cast_and_dump(hash_key, pk)
        ids << partition_key_dumped
      end
    end

    options = range_key ? { range_key: range_key } : {}
    Dynamoid.adapter.delete(table_name, ids, options)
  else
    # given single primary key:
    #   Model.delete(partition_key)
    #   Model.delete(partition_key, sort_key)

    partition_key, sort_key = ids

    raise Dynamoid::Errors::MissingHashKey if partition_key.nil?
    raise Dynamoid::Errors::MissingRangeKey if range_key? && sort_key.nil?

    options = sort_key ? { range_key: cast_and_dump(self.range_key, sort_key) } : {}
    partition_key_dumped = cast_and_dump(hash_key, partition_key)

    Dynamoid.adapter.delete(table_name, partition_key_dumped, options)
  end

  nil
end

#delete_tableModel class

Deletes the table for the model.

Dynamoid deletes a table asynchronously and doesn’t wait until a table is deleted completely.

Subsequent method calls for the same table will be ignored.

Returns:

  • (Model class)

    self



138
139
140
141
# File 'lib/dynamoid/persistence.rb', line 138

def delete_table
  Dynamoid.adapter.delete_table(table_name)
  self
end

#from_database(attrs = {}) ⇒ Object



144
145
146
147
148
# File 'lib/dynamoid/persistence.rb', line 144

def from_database(attrs = {})
  klass = choose_right_class(attrs)
  attrs_undumped = Undumping.undump_attributes(attrs, klass.attributes)
  klass.new(attrs_undumped).tap { |r| r.new_record = false }
end

#import(array_of_attributes) ⇒ Array

Create several models at once.

users = User.import([{ name: 'a' }, { name: 'b' }])

import is a relatively low-level method and bypasses some mechanisms like callbacks and validation.

It sets timestamp fields created_at and updated_at if they are blank. It sets a hash key field as well if it’s blank. It expects that the hash key field is string and sets a random UUID value if the field value is blank. All the field values are type casted to the declared types.

It works efficiently and uses the BatchWriteItem operation. In order to cope with throttling it uses a backoff strategy if it’s specified with Dynamoid::Config.backoff configuration option.

Because of the nature of DynamoDB and its limits only 25 models can be saved at once. So multiple HTTP requests can be sent to DynamoDB.

Parameters:

  • array_of_attributes (Array<Hash>)

Returns:

  • (Array)

    Created models



172
173
174
# File 'lib/dynamoid/persistence.rb', line 172

def import(array_of_attributes)
  Import.call(self, array_of_attributes)
end

#inc(hash_key_value, range_key_value = nil, counters) ⇒ Model class

Increase a numeric field by specified value.

User.inc('1', age: 2)

Can update several fields at once.

User.inc('1', age: 2, version: 1)

If range key is declared for a model it should be passed as well:

User.inc('1', 'Tylor', age: 2)

It’s an atomic operation it does not interfere with other write requests.

Uses efficient low-level UpdateItem operation and does only one HTTP request.

Doesn’t run validations and callbacks. Doesn’t update created_at and updated_at as well.

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

User.inc('1', age: 2, touch: true)
User.inc('1', age: 2, touch: :viewed_at)
User.inc('1', age: 2, touch: [:viewed_at, :accessed_at])

Parameters:

  • hash_key_value (Scalar value)

    hash key

  • range_key_value (Scalar value) (defaults to: nil)

    range key (optional)

  • counters (Hash)

    value to increase by

Options Hash (counters):

  • :touch (true | Symbol | Array<Symbol>)

    to update update_at attribute and optionally the specified ones

Returns:

  • (Model class)

    self



456
457
458
459
460
# File 'lib/dynamoid/persistence.rb', line 456

def inc(hash_key_value, range_key_value = nil, counters)
  # It's similar to Rails' #update_counters.
  Inc.call(self, hash_key_value, range_key_value, counters)
  self
end

#table_nameObject



30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/dynamoid/persistence.rb', line 30

def table_name
  return @table_name if @table_name

  if options[:arn]
    @table_name = options[:arn]
    return @table_name
  end

  base_name = options[:name] || base_class.name.split('::').last.downcase.pluralize
  namespace = Dynamoid::Config.namespace.to_s
  @table_name = [namespace, base_name].reject(&:empty?).join('_')
end

#update(hash_key, range_key_value = nil, attrs) ⇒ Dynamoid::Document

Update document with provided attributes.

Instantiates document and saves changes. Runs validations and callbacks. Don’t save changes if validation fails.

User.update('1', age: 26)

If range key is declared for a model it should be passed as well:

User.update('1', 'Tylor', age: 26)

Parameters:

  • hash_key (Scalar value)

    hash key

  • range_key_value (Scalar value) (defaults to: nil)

    range key (optional)

  • attrs (Hash)

Returns:



266
267
268
269
270
# File 'lib/dynamoid/persistence.rb', line 266

def update(hash_key, range_key_value = nil, attrs)
  model = find(hash_key, range_key: range_key_value, consistent_read: true)
  model.update_attributes(attrs)
  model
end

#update!(hash_key, range_key_value = nil, attrs) ⇒ Dynamoid::Document

Update document with provided attributes.

Instantiates document and saves changes. Runs validations and callbacks.

User.update!('1', age: 26)

If range key is declared for a model it should be passed as well:

User.update('1', 'Tylor', age: 26)

Raises Dynamoid::Errors::DocumentNotValid exception if validation fails.

Parameters:

  • hash_key (Scalar value)

    hash key

  • range_key_value (Scalar value) (defaults to: nil)

    range key (optional)

  • attrs (Hash)

Returns:



289
290
291
292
293
# File 'lib/dynamoid/persistence.rb', line 289

def update!(hash_key, range_key_value = nil, attrs)
  model = find(hash_key, range_key: range_key_value, consistent_read: true)
  model.update_attributes!(attrs)
  model
end

#update_fields(hash_key_value, range_key_value = nil, attrs = {}, conditions = {}) ⇒ Dynamoid::Document|nil

Update document.

Doesn’t run validations and callbacks.

User.update_fields('1', age: 26)

If range key is declared for a model it should be passed as well:

User.update_fields('1', 'Tylor', age: 26)

Can make a conditional update so a document will be updated only if it meets the specified conditions. Conditions can be specified as a Hash with :if key:

User.update_fields('1', { age: 26 }, { if: { version: 1 } })

Here User model has an integer version field and the document will be updated only if the version attribute currently has value 1.

If a document with specified hash and range keys doesn’t exist or conditions were specified and failed the method call returns nil.

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_fields(user.id, { age: 18 }, { unless_exists: [:age] })

update_fields uses the UpdateItem operation so it saves changes and loads an updated document back with one HTTP request.

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 Dynamoid::Errors::MissingRangeKey if a sort key is required but has value nil.

Parameters:

  • hash_key_value (Scalar value)

    hash key

  • range_key_value (Scalar value) (defaults to: nil)

    range key (optional)

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

    (optional)

Options Hash (conditions):

  • :if (Hash)

    conditions on attribute values

  • :unless_exists (Hash)

    conditions on attributes presence

Returns:



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/dynamoid/persistence.rb', line 341

def update_fields(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
  optional_params = [range_key_value, attrs, conditions].compact
  if optional_params.first.is_a?(Hash)
    range_key_value = nil
    attrs, conditions = optional_params[0..1]
  else
    range_key_value = optional_params.first
    attrs, conditions = optional_params[1..2]
  end

  UpdateFields.call(self,
                    partition_key: hash_key_value,
                    sort_key: range_key_value,
                    attributes: attrs,
                    conditions: conditions)
end

#upsert(hash_key_value, range_key_value = nil, attrs = {}, conditions = {}) ⇒ Dynamoid::Document|nil

Update an existing document or create a new one.

If a document with specified hash and range keys doesn’t exist it creates a new document with specified attributes. Doesn’t run validations and callbacks.

User.upsert('1', age: 26)

If range key is declared for a model it should be passed as well:

User.upsert('1', 'Tylor', age: 26)

Can make a conditional update so a document will be updated only if it meets the specified conditions. Conditions can be specified as a Hash with :if key:

User.upsert('1', { age: 26 }, { if: { version: 1 } })

Here User model has an integer version field and the document will be updated only if the version attribute currently has value 1.

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.upsert(user.id, { age: 18 }, { unless_exists: [:age] })

If conditions were specified and failed the method call returns nil.

upsert uses the UpdateItem operation so it saves changes and loads an updated document back with one HTTP request.

Raises a Dynamoid::Errors::UnknownAttribute exception if any of the attributes is not declared in the model class.

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:

  • hash_key_value (Scalar value)

    hash key

  • range_key_value (Scalar value) (defaults to: nil)

    range key (optional)

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

    (optional)

Options Hash (conditions):

  • :if (Hash)

    conditions on attribute values

  • :unless_exists (Hash)

    conditions on attributes presence

Returns:



405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# File 'lib/dynamoid/persistence.rb', line 405

def upsert(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
  optional_params = [range_key_value, attrs, conditions].compact
  if optional_params.first.is_a?(Hash)
    range_key_value = nil
    attrs, conditions = optional_params[0..1]
  else
    range_key_value = optional_params.first
    attrs, conditions = optional_params[1..2]
  end

  Upsert.call(self,
              partition_key: hash_key_value,
              sort_key: range_key_value,
              attributes: attrs,
              conditions: conditions)
end