lib/dynamoid/document.rb in dynamoid-2.2.0 vs lib/dynamoid/document.rb in dynamoid-3.0.0
- old
+ new
@@ -1,8 +1,8 @@
-# encoding: utf-8
-module Dynamoid #:nodoc:
+# frozen_string_literal: true
+module Dynamoid #:nodoc:
# This is the base module for all domain objects that need to be persisted to
# the database as documents.
module Document
extend ActiveSupport::Concern
include Dynamoid::Components
@@ -112,82 +112,152 @@
# @return [Boolean] true/false
#
# @since 0.2.0
def exists?(id_or_conditions = {})
case id_or_conditions
- when Hash then where(id_or_conditions).first.present?
- else !! find_by_id(id_or_conditions)
+ when Hash then where(id_or_conditions).first.present?
+ else
+ begin
+ find(id_or_conditions)
+ true
+ rescue Dynamoid::Errors::RecordNotFound
+ false
+ end
end
end
- def update(hash_key, range_key_value=nil, attrs)
- if range_key.present?
- range_key_value = dump_field(range_key_value, attributes[self.range_key])
- else
- range_key_value = nil
- end
-
+ # Update document with provided values.
+ # Instantiates document and saves changes. Runs validations and callbacks.
+ #
+ # @param [Scalar value] partition key
+ # @param [Scalar value] sort key, optional
+ # @param [Hash] attributes
+ #
+ # @return [Dynamoid::Doument] updated document
+ #
+ # @example Update document
+ # Post.update(101, read: true)
+ 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
- def update_fields(hash_key_value, range_key_value=nil, attrs={}, conditions={})
+ # Update document.
+ # Uses efficient low-level `UpdateItem` API call.
+ # Changes attibutes and loads new document version with one API call.
+ # Doesn't run validations and callbacks. Can make conditional update.
+ # If a document doesn't exist or specified conditions failed - returns `nil`
+ #
+ # @param [Scalar value] partition key
+ # @param [Scalar value] sort key (optional)
+ # @param [Hash] attributes
+ # @param [Hash] conditions
+ #
+ # @return [Dynamoid::Document/nil] updated document
+ #
+ # @example Update document
+ # Post.update_fields(101, read: true)
+ #
+ # @example Update document with condition
+ # Post.update_fields(101, { read: true }, if: { version: 1 })
+ 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]
+ attrs, conditions = optional_params[0..1]
else
range_key_value = optional_params.first
- attrs, conditions = optional_params[1 .. 2]
+ attrs, conditions = optional_params[1..2]
end
options = if range_key
- { range_key: dump_field(range_key_value, attributes[range_key]) }
+ value_casted = TypeCasting.cast_field(range_key_value, attributes[range_key])
+ value_dumped = Dumping.dump_field(value_casted, attributes[range_key])
+ { range_key: value_dumped }
else
{}
end
(conditions[:if_exists] ||= {})[hash_key] = hash_key_value
options[:conditions] = conditions
+ attrs = attrs.symbolize_keys
+ if Dynamoid::Config.timestamps
+ attrs[:updated_at] ||= DateTime.now.in_time_zone(Time.zone)
+ end
+
begin
new_attrs = Dynamoid.adapter.update_item(table_name, hash_key_value, options) do |t|
- attrs.symbolize_keys.each do |k, v|
- t.set k => dump_field(v, attributes[k])
+ attrs.each do |k, v|
+ value_casted = TypeCasting.cast_field(v, attributes[k])
+ value_dumped = Dumping.dump_field(value_casted, attributes[k])
+ t.set(k => value_dumped)
end
end
- new(new_attrs)
+ attrs_undumped = Undumping.undump_attributes(new_attrs, attributes)
+ new(attrs_undumped)
rescue Dynamoid::Errors::ConditionalCheckFailedException
end
end
- def upsert(hash_key_value, range_key_value=nil, attrs={}, conditions={})
+
+ # Update existing document or create new one.
+ # Similar to `.update_fields`. The only diffirence is creating new document.
+ #
+ # Uses efficient low-level `UpdateItem` API call.
+ # Changes attibutes and loads new document version with one API call.
+ # Doesn't run validations and callbacks. Can make conditional update.
+ # If specified conditions failed - returns `nil`
+ #
+ # @param [Scalar value] partition key
+ # @param [Scalar value] sort key (optional)
+ # @param [Hash] attributes
+ # @param [Hash] conditions
+ #
+ # @return [Dynamoid::Document/nil] updated document
+ #
+ # @example Update document
+ # Post.update(101, read: true)
+ #
+ # @example Update document
+ # Post.upsert(101, read: true)
+ 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]
+ attrs, conditions = optional_params[0..1]
else
range_key_value = optional_params.first
- attrs, conditions = optional_params[1 .. 2]
+ attrs, conditions = optional_params[1..2]
end
options = if range_key
- { range_key: dump_field(range_key_value, attributes[range_key]) }
+ value_casted = TypeCasting.cast_field(range_key_value, attributes[range_key])
+ value_dumped = Dumping.dump_field(value_casted, attributes[range_key])
+ { range_key: value_dumped }
else
{}
end
options[:conditions] = conditions
+ attrs = attrs.symbolize_keys
+ if Dynamoid::Config.timestamps
+ attrs[:updated_at] ||= DateTime.now.in_time_zone(Time.zone)
+ end
+
begin
new_attrs = Dynamoid.adapter.update_item(table_name, hash_key_value, options) do |t|
- attrs.symbolize_keys.each do |k, v|
- t.set k => dump_field(v, attributes[k])
+ attrs.each do |k, v|
+ value_casted = TypeCasting.cast_field(v, attributes[k])
+ value_dumped = Dumping.dump_field(value_casted, attributes[k])
+ t.set(k => value_dumped)
end
end
- new(new_attrs)
+ attrs_undumped = Undumping.undump_attributes(new_attrs, attributes)
+ new(attrs_undumped)
rescue Dynamoid::Errors::ConditionalCheckFailedException
end
end
def deep_subclasses
@@ -210,17 +280,34 @@
run_callbacks :initialize do
@new_record = true
@attributes ||= {}
@associations ||= {}
- load(attrs)
+ self.class.attributes.each do |_, options|
+ if options[:type].is_a?(Class) && options[:default]
+ raise 'Dynamoid class-type fields do not support default values'
+ end
+ end
+
+ attrs_with_defaults = {}
+ self.class.attributes.each do |attribute, options|
+ attrs_with_defaults[attribute] = if attrs.key?(attribute)
+ attrs[attribute]
+ elsif options.key?(:default)
+ evaluate_default_value(options[:default])
+ end
+ end
+
+ attrs_virtual = attrs.slice(*(attrs.keys - self.class.attributes.keys))
+
+ load(attrs_with_defaults.merge(attrs_virtual))
end
end
def load(attrs)
- self.class.undump(attrs).each do |key, value|
- send("#{key}=", value) if self.respond_to?("#{key}=")
+ attrs.each do |key, value|
+ send("#{key}=", value) if respond_to?("#{key}=")
end
end
# An object is equal to another object if their ids are equal.
#
@@ -228,11 +315,11 @@
def ==(other)
if self.class.identity_map_on?
super
else
return false if other.nil?
- other.is_a?(Dynamoid::Document) && self.hash_key == other.hash_key && self.range_value == other.range_value
+ other.is_a?(Dynamoid::Document) && hash_key == other.hash_key && range_value == other.range_value
end
end
def eql?(other)
self == other
@@ -247,42 +334,61 @@
#
# @return [Dynamoid::Document] the document this method was called on
#
# @since 0.2.0
def reload
- range_key_value = range_value ? dumped_range_value : nil
- self.attributes = self.class.find(hash_key, range_key: range_key_value, consistent_read: true).attributes
+ options = { consistent_read: true }
+
+ if self.class.range_key
+ options[:range_key] = range_value
+ end
+
+ self.attributes = self.class.find(hash_key, options).attributes
@associations.values.each(&:reset)
self
end
# Return an object's hash key, regardless of what it might be called to the object.
#
# @since 0.4.0
def hash_key
- self.send(self.class.hash_key)
+ send(self.class.hash_key)
end
# Assign an object's hash key, regardless of what it might be called to the object.
#
# @since 0.4.0
def hash_key=(value)
- self.send("#{self.class.hash_key}=", value)
+ send("#{self.class.hash_key}=", value)
end
def range_value
if range_key = self.class.range_key
- self.send(range_key)
+ send(range_key)
end
end
def range_value=(value)
- self.send("#{self.class.range_key}=", value)
+ send("#{self.class.range_key}=", value)
end
private
def dumped_range_value
- dump_field(range_value, self.class.attributes[self.class.range_key])
+ Dumping.dump_field(range_value, self.class.attributes[self.class.range_key])
+ end
+
+ # Evaluates the default value given, this is used by undump
+ # when determining the value of the default given for a field options.
+ #
+ # @param [Object] :value the attribute's default value
+ def evaluate_default_value(val)
+ if val.respond_to?(:call)
+ val.call
+ elsif val.duplicable?
+ val.dup
+ else
+ val
+ end
end
end
end