Class: Dynomite::Item

Inherits:
Object
  • Object
show all
Includes:
DbConfig, Errors, Log
Defined in:
lib/dynomite/item.rb

Class Method Summary collapse

Instance Method Summary collapse

Methods included from DbConfig

#db, included, #namespaced_table_name

Methods included from Log

included, #log

Constructor Details

#initialize(attrs = {}) ⇒ Item

Returns a new instance of Item.



46
47
48
# File 'lib/dynomite/item.rb', line 46

def initialize(attrs={})
  @attrs = attrs
end

Class Method Details

.add_column(name) ⇒ Object

See Also:



333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/dynomite/item.rb', line 333

def self.add_column(name)
  if Dynomite::RESERVED_WORDS.include?(name)
    raise ReservedWordError, "'#{name}' is a reserved word"
  end

  define_method(name) do
    @attrs[name.to_s]
  end

  define_method("#{name}=") do |value|
    @attrs[name.to_s] = value
  end
end

.column(*names) ⇒ Object

Defines column. Defined column can be accessed by getter and setter methods of the same name (e.g. [model.my_column]). Attributes with undefined columns can be accessed by

model.attrs

method.



328
329
330
# File 'lib/dynomite/item.rb', line 328

def self.column(*names)
  names.each(&method(:add_column))
end

.countObject



321
322
323
# File 'lib/dynomite/item.rb', line 321

def self.count
  table.item_count
end

.delete(key_object, options = {}) ⇒ Object

Two ways to use the delete method:

  1. Specify the key as a String. In this case the key will is the partition_key

set on the model.

MyModel.delete("728e7b5df40b93c3ea6407da8ac3e520e00d7351")
  1. Specify the key as a Hash, you can arbitrarily specific the key structure this way

MyModel.delete(“728e7b5df40b93c3ea6407da8ac3e520e00d7351”)

options is provided in case you want to specific condition_expression or expression_attribute_values.



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/dynomite/item.rb', line 266

def self.delete(key_object, options={})
  if key_object.is_a?(String)
    key = {
      partition_key => key_object
    }
  else # it should be a Hash
    key = key_object
  end

  params = {
    table_name: table_name,
    key: key
  }
  # In case you want to specify condition_expression or expression_attribute_values
  params = params.merge(options)

  resp = db.delete_item(params)
end

.find(id) ⇒ Object



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/dynomite/item.rb', line 238

def self.find(id)
  params =
    case id
    when String
      { partition_key => id }
    when Hash
      id
    end

  resp = db.get_item(
    table_name: table_name,
    key: params
  )
  attributes = resp.item # unwraps the item's attributes
  self.new(attributes) if attributes
end

.get_table_nameObject



308
309
310
311
# File 'lib/dynomite/item.rb', line 308

def self.get_table_name
  @table_name ||= self.name.pluralize.gsub('::','-').underscore.dasherize
  [table_namespace, @table_name].reject {|s| s.nil? || s.empty?}.join('-')
end

.partition_key(*args) ⇒ Object

When called with an argument we’ll set the internal @partition_key value When called without an argument just retun it. class Comment < Dynomite::Item

partition_key "post_id"

end



290
291
292
293
294
295
296
297
# File 'lib/dynomite/item.rb', line 290

def self.partition_key(*args)
  case args.size
  when 0
    @partition_key || "id" # defaults to id
  when 1
    @partition_key = args[0].to_s
  end
end

.query(params = {}) ⇒ Object

Adds very little wrapper logic to query.

  • Automatically add table_name to options for convenience.

  • Decorates return value. Returns Array of [MyModel.new] instead of the dynamodb client response.

Other than that, usage is same was using the dynamodb client query method directly. Example:

MyModel.query(
  index_name: 'category-index',
  expression_attribute_names: { "#category_name" => "category" },
  expression_attribute_values: { ":category_value" => "Entertainment" },
  key_condition_expression: "#category_name = :category_value",
)

AWS Docs examples: docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.Ruby.04.html



167
168
169
170
171
# File 'lib/dynomite/item.rb', line 167

def self.query(params={})
  params = { table_name: table_name }.merge(params)
  resp = db.query(params)
  resp.items.map {|i| self.new(i) }
end

.replace(attrs) ⇒ Object



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/dynomite/item.rb', line 216

def self.replace(attrs)
  # Automatically adds some attributes:
  #   partition key unique id
  #   created_at and updated_at timestamps. Timestamp format from AWS docs: http://amzn.to/2z98Bdc
  defaults = {
    partition_key => Digest::SHA1.hexdigest([Time.now, rand].join)
  }
  item = defaults.merge(attrs)
  item["created_at"] ||= Time.now.utc.strftime('%Y-%m-%dT%TZ')
  item["updated_at"] = Time.now.utc.strftime('%Y-%m-%dT%TZ')

  # put_item full replaces the item
  resp = db.put_item(
    table_name: table_name,
    item: item
  )

  # The resp does not contain the attrs. So might as well return
  # the original item with the generated partition_key value
  item
end

.scan(params = {}) ⇒ Object

Adds very little wrapper logic to scan.

  • Automatically add table_name to options for convenience.

  • Decorates return value. Returns Array of [MyModel.new] instead of the dynamodb client response.

Other than that, usage is same was using the dynamodb client scan method directly. Example:

MyModel.scan(
  expression_attribute_names: {"#updated_at"=>"updated_at"},
  filter_expression: "#updated_at between :start_time and :end_time",
  expression_attribute_values: {
    ":start_time" => "2010-01-01T00:00:00",
    ":end_time" => "2020-01-01T00:00:00"
  }
)

AWS Docs examples: docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.Ruby.04.html



142
143
144
145
146
147
148
# File 'lib/dynomite/item.rb', line 142

def self.scan(params={})
  log("It's recommended to not use scan for production. It can be slow and expensive. You can a LSI or GSI and query the index instead.")
  log("Scanning table: #{table_name}")
  params = { table_name: table_name }.merge(params)
  resp = db.scan(params)
  resp.items.map {|i| self.new(i) }
end

.set_table_name(value) ⇒ Object



313
314
315
# File 'lib/dynomite/item.rb', line 313

def self.set_table_name(value)
  @table_name = value
end

.tableObject



317
318
319
# File 'lib/dynomite/item.rb', line 317

def self.table
  Aws::DynamoDB::Table.new(name: table_name, client: db)
end

.table_name(*args) ⇒ Object



299
300
301
302
303
304
305
306
# File 'lib/dynomite/item.rb', line 299

def self.table_name(*args)
  case args.size
  when 0
    get_table_name
  when 1
    set_table_name(args[0])
  end
end

.where(attributes, options = {}) ⇒ Object

Translates simple query searches:

Post.where({category: "Drama"}, index_name: "category-index")

translates to

resp = db.query(
  table_name: "demo-dev-post",
  index_name: 'category-index',
  expression_attribute_names: { "#category_name" => "category" },
  expression_attribute_values: { ":category_value" => category },
  key_condition_expression: "#category_name = :category_value",
)

TODO: Implement nicer where syntax with index_name as a chained method.

Post.where({category: "Drama"}, {index_name: "category-index"})
  VS
Post.where(category: "Drama").index_name("category-index")


192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/dynomite/item.rb', line 192

def self.where(attributes, options={})
  raise "attributes.size == 1 only supported for now" if attributes.size != 1

  attr_name = attributes.keys.first
  attr_value = attributes[attr_name]

  # params = {
  #   expression_attribute_names: { "#category_name" => "category" },
  #   expression_attribute_values: { ":category_value" => "Entertainment" },
  #   key_condition_expression: "#category_name = :category_value",
  # }
  name_key, value_key = "##{attr_name}_name", ":#{attr_name}_value"
  params = {
    expression_attribute_names: { name_key => attr_name },
    expression_attribute_values: { value_key => attr_value },
    key_condition_expression: "#{name_key} = #{value_key}",
  }
  # Allow direct access to override params passed to dynamodb query options.
  # This is is how index_name is passed:
  params = params.merge(options)

  query(params)
end

Instance Method Details

#as_json(options = {}) ⇒ Object

For render json: item



109
110
111
# File 'lib/dynomite/item.rb', line 109

def as_json(options={})
  @attrs
end

#attributesObject



119
120
121
# File 'lib/dynomite/item.rb', line 119

def attributes
  @attributes
end

#attributes=(attributes) ⇒ Object

Longer hand methods for completeness. Internallly encourage the shorter attrs method.



115
116
117
# File 'lib/dynomite/item.rb', line 115

def attributes=(attributes)
  @attributes = attributes
end

#attrs(*args) ⇒ Object

Defining our own reader so we can do a deep merge if user passes in attrs



51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/dynomite/item.rb', line 51

def attrs(*args)
  case args.size
  when 0
    ActiveSupport::HashWithIndifferentAccess.new(@attrs)
  when 1
    attributes = args[0] # Hash
    if attributes.empty?
      ActiveSupport::HashWithIndifferentAccess.new
    else
      @attrs = attrs.deep_merge!(attributes)
    end
  end
end

#deleteObject



96
97
98
# File 'lib/dynomite/item.rb', line 96

def delete
  self.class.delete(@attrs[:id]) if @attrs[:id]
end

#find(id) ⇒ Object



92
93
94
# File 'lib/dynomite/item.rb', line 92

def find(id)
  self.class.find(id)
end

#partition_keyObject



104
105
106
# File 'lib/dynomite/item.rb', line 104

def partition_key
  self.class.partition_key
end

#replace(hash = {}) ⇒ Object

The method is named replace to clearly indicate that the item is fully replaced.



72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/dynomite/item.rb', line 72

def replace(hash={})
  @attrs = @attrs.deep_merge(hash)

  # valid? method comes from ActiveModel::Validations
  if respond_to? :valid?
    return false unless valid?
  end

  attrs = self.class.replace(@attrs)

  @attrs = attrs # refresh attrs because it now has the id
  self
end

#replace!(hash = {}) ⇒ Object

Similar to replace, but raises an error on failed validation. Works that way only if ActiveModel::Validations are included

Raises:



88
89
90
# File 'lib/dynomite/item.rb', line 88

def replace!(hash={})
  raise ValidationError, "Validation failed: #{errors.full_messages.join(', ')}" unless replace(hash)
end

#table_nameObject



100
101
102
# File 'lib/dynomite/item.rb', line 100

def table_name
  self.class.table_name
end