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