diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 6a6485f35e..e2f2508ae8 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -61,6 +61,7 @@ module ActiveRecord autoload :Base autoload :Callbacks + autoload :CounterCache autoload :DynamicFinderMatch autoload :DynamicScopeMatch autoload :Migration @@ -68,6 +69,7 @@ module ActiveRecord autoload :NamedScope autoload :NestedAttributes autoload :Observer + autoload :Persistence autoload :QueryCache autoload :Reflection autoload :Schema diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 08389907ef..45aaea062d 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -253,6 +253,7 @@ module ActiveRecord raise ArgumentError, 'Converter must be a symbol denoting the converter method to call or a Proc to be invoked.' end end + mapping.each { |pair| self[pair.first] = part.send(pair.last) } instance_variable_set("@#{name}", part.freeze) end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 6c64210c92..0a3c7c6a60 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1304,14 +1304,14 @@ module ActiveRecord # Don't use a before_destroy callback since users' before_destroy # callbacks will be executed after the association is wiped out. - old_method = "destroy_without_habtm_shim_for_#{reflection.name}" - class_eval <<-end_eval unless method_defined?(old_method) - alias_method :#{old_method}, :destroy_without_callbacks # alias_method :destroy_without_habtm_shim_for_posts, :destroy_without_callbacks - def destroy_without_callbacks # def destroy_without_callbacks - #{reflection.name}.clear # posts.clear - #{old_method} # destroy_without_habtm_shim_for_posts - end # end - end_eval + include Module.new { + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def destroy # def destroy + super # super + #{reflection.name}.clear # posts.clear + end # end + RUBY + } add_association_callbacks(reflection.name, options) end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 3a9a67e3a2..c117271c71 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -18,10 +18,19 @@ module ActiveRecord def instance_method_already_implemented?(method_name) method_name = method_name.to_s @_defined_class_methods ||= ancestors.first(ancestors.index(ActiveRecord::Base)).sum([]) { |m| m.public_instance_methods(false) | m.private_instance_methods(false) | m.protected_instance_methods(false) }.map {|m| m.to_s }.to_set - @@_defined_activerecord_methods ||= (ActiveRecord::Base.public_instance_methods(false) | ActiveRecord::Base.private_instance_methods(false) | ActiveRecord::Base.protected_instance_methods(false)).map{|m| m.to_s }.to_set + @@_defined_activerecord_methods ||= defined_activerecord_methods raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name) @_defined_class_methods.include?(method_name) end + + def defined_activerecord_methods + active_record = ActiveRecord::Base + super_klass = ActiveRecord::Base.superclass + methods = active_record.public_instance_methods - super_klass.public_instance_methods + methods += active_record.private_instance_methods - super_klass.private_instance_methods + methods += active_record.protected_instance_methods - super_klass.protected_instance_methods + methods.map {|m| m.to_s }.to_set + end end def method_missing(method_id, *args, &block) diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 36f2a9777c..dd44bd8d51 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -5,20 +5,20 @@ module ActiveRecord module Dirty extend ActiveSupport::Concern include ActiveModel::Dirty + include AttributeMethods::Write included do - alias_method_chain :save, :dirty - alias_method_chain :save!, :dirty - alias_method_chain :update, :dirty - alias_method_chain :reload, :dirty + if self < Timestamp + raise "You cannot include Dirty after Timestamp" + end superclass_delegating_accessor :partial_updates self.partial_updates = true end # Attempts to +save+ the record and clears changed attributes if successful. - def save_with_dirty(*args) #:nodoc: - if status = save_without_dirty(*args) + def save(*) #:nodoc: + if status = super @previously_changed = changes @changed_attributes.clear end @@ -26,70 +26,70 @@ module ActiveRecord end # Attempts to save! the record and clears changed attributes if successful. - def save_with_dirty!(*args) #:nodoc: - save_without_dirty!(*args).tap do + def save!(*) #:nodoc: + super.tap do @previously_changed = changes @changed_attributes.clear end end # reload the record and clears changed attributes. - def reload_with_dirty(*args) #:nodoc: - reload_without_dirty(*args).tap do + def reload(*) #:nodoc: + super.tap do @previously_changed.clear @changed_attributes.clear end end - private - # Wrap write_attribute to remember original attribute value. - def write_attribute(attr, value) - attr = attr.to_s + private + # Wrap write_attribute to remember original attribute value. + def write_attribute(attr, value) + attr = attr.to_s - # The attribute already has an unsaved change. - if attribute_changed?(attr) - old = @changed_attributes[attr] - @changed_attributes.delete(attr) unless field_changed?(attr, old, value) + # The attribute already has an unsaved change. + if attribute_changed?(attr) + old = @changed_attributes[attr] + @changed_attributes.delete(attr) unless field_changed?(attr, old, value) + else + old = clone_attribute_value(:read_attribute, attr) + # Save Time objects as TimeWithZone if time_zone_aware_attributes == true + old = old.in_time_zone if clone_with_time_zone_conversion_attribute?(attr, old) + @changed_attributes[attr] = old if field_changed?(attr, old, value) + end + + # Carry on. + super(attr, value) + end + + def update(*) + if partial_updates? + # Serialized attributes should always be written in case they've been + # changed in place. + super(changed | (attributes.keys & self.class.serialized_attributes.keys)) + else + super + end + end + + def field_changed?(attr, old, value) + if column = column_for_attribute(attr) + if column.number? && column.null && (old.nil? || old == 0) && value.blank? + # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values. + # Hence we don't record it as a change if the value changes from nil to ''. + # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll + # be typecast back to 0 (''.to_i => 0) + value = nil else - old = clone_attribute_value(:read_attribute, attr) - # Save Time objects as TimeWithZone if time_zone_aware_attributes == true - old = old.in_time_zone if clone_with_time_zone_conversion_attribute?(attr, old) - @changed_attributes[attr] = old if field_changed?(attr, old, value) - end - - # Carry on. - super(attr, value) - end - - def update_with_dirty - if partial_updates? - # Serialized attributes should always be written in case they've been - # changed in place. - update_without_dirty(changed | (attributes.keys & self.class.serialized_attributes.keys)) - else - update_without_dirty + value = column.type_cast(value) end end - def field_changed?(attr, old, value) - if column = column_for_attribute(attr) - if column.number? && column.null && (old.nil? || old == 0) && value.blank? - # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values. - # Hence we don't record it as a change if the value changes from nil to ''. - # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll - # be typecast back to 0 (''.to_i => 0) - value = nil - else - value = column.type_cast(value) - end - end + old != value + end - old != value - end - - def clone_with_time_zone_conversion_attribute?(attr, old) - old.class.name == "Time" && time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(attr.to_sym) - end + def clone_with_time_zone_conversion_attribute?(attr, old) + old.class.name == "Time" && time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(attr.to_sym) + end end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 325a8aa7ec..fd1082a268 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -130,8 +130,6 @@ module ActiveRecord ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many } included do - alias_method_chain :reload, :autosave_associations - ASSOCIATION_TYPES.each do |type| send("valid_keys_for_#{type}_association") << :autosave end @@ -196,9 +194,9 @@ module ActiveRecord end # Reloads the attributes of the object as usual and removes a mark for destruction. - def reload_with_autosave_associations(options = nil) + def reload(options = nil) @marked_for_destruction = false - reload_without_autosave_associations(options) + super end # Marks this record to be destroyed as part of the parents save transaction. diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 9ed53cc4af..650a91b385 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -480,110 +480,6 @@ module ActiveRecord #:nodoc: connection.select_value(sql, "#{name} Count").to_i end - # Resets one or more counter caches to their correct value using an SQL - # count query. This is useful when adding new counter caches, or if the - # counter has been corrupted or modified directly by SQL. - # - # ==== Parameters - # - # * +id+ - The id of the object you wish to reset a counter on. - # * +counters+ - One or more counter names to reset - # - # ==== Examples - # - # # For Post with id #1 records reset the comments_count - # Post.reset_counters(1, :comments) - def reset_counters(id, *counters) - object = find(id) - counters.each do |association| - child_class = reflect_on_association(association).klass - counter_name = child_class.reflect_on_association(self.name.downcase.to_sym).counter_cache_column - - connection.update("UPDATE #{quoted_table_name} SET #{connection.quote_column_name(counter_name)} = #{object.send(association).count} WHERE #{connection.quote_column_name(primary_key)} = #{quote_value(object.id)}", "#{name} UPDATE") - end - end - - # A generic "counter updater" implementation, intended primarily to be - # used by increment_counter and decrement_counter, but which may also - # be useful on its own. It simply does a direct SQL update for the record - # with the given ID, altering the given hash of counters by the amount - # given by the corresponding value: - # - # ==== Parameters - # - # * +id+ - The id of the object you wish to update a counter on or an Array of ids. - # * +counters+ - An Array of Hashes containing the names of the fields - # to update as keys and the amount to update the field by as values. - # - # ==== Examples - # - # # For the Post with id of 5, decrement the comment_count by 1, and - # # increment the action_count by 1 - # Post.update_counters 5, :comment_count => -1, :action_count => 1 - # # Executes the following SQL: - # # UPDATE posts - # # SET comment_count = comment_count - 1, - # # action_count = action_count + 1 - # # WHERE id = 5 - # - # # For the Posts with id of 10 and 15, increment the comment_count by 1 - # Post.update_counters [10, 15], :comment_count => 1 - # # Executes the following SQL: - # # UPDATE posts - # # SET comment_count = comment_count + 1, - # # WHERE id IN (10, 15) - def update_counters(id, counters) - updates = counters.inject([]) { |list, (counter_name, increment)| - sign = increment < 0 ? "-" : "+" - list << "#{connection.quote_column_name(counter_name)} = COALESCE(#{connection.quote_column_name(counter_name)}, 0) #{sign} #{increment.abs}" - }.join(", ") - - if id.is_a?(Array) - ids_list = id.map {|i| quote_value(i)}.join(', ') - condition = "IN (#{ids_list})" - else - condition = "= #{quote_value(id)}" - end - - update_all(updates, "#{connection.quote_column_name(primary_key)} #{condition}") - end - - # Increment a number field by one, usually representing a count. - # - # This is used for caching aggregate values, so that they don't need to be computed every time. - # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is - # shown it would have to run an SQL query to find how many posts and comments there are. - # - # ==== Parameters - # - # * +counter_name+ - The name of the field that should be incremented. - # * +id+ - The id of the object that should be incremented. - # - # ==== Examples - # - # # Increment the post_count column for the record with an id of 5 - # DiscussionBoard.increment_counter(:post_count, 5) - def increment_counter(counter_name, id) - update_counters(id, counter_name => 1) - end - - # Decrement a number field by one, usually representing a count. - # - # This works the same as increment_counter but reduces the column value by 1 instead of increasing it. - # - # ==== Parameters - # - # * +counter_name+ - The name of the field that should be decremented. - # * +id+ - The id of the object that should be decremented. - # - # ==== Examples - # - # # Decrement the post_count column for the record with an id of 5 - # DiscussionBoard.decrement_counter(:post_count, 5) - def decrement_counter(counter_name, id) - update_counters(id, counter_name => -1) - end - # Attributes named in this macro are protected from mass-assignment, # such as new(attributes), # update_attributes(attributes), or @@ -1623,186 +1519,6 @@ module ActiveRecord #:nodoc: quote_value(id, column_for_attribute(self.class.primary_key)) end - # Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet; otherwise, returns false. - def new_record? - @new_record - end - - # Returns true if this object has been destroyed, otherwise returns false. - def destroyed? - @destroyed - end - - # Returns if the record is persisted, i.e. it's not a new record and it was not destroyed. - def persisted? - !(new_record? || destroyed?) - end - - # :call-seq: - # save(options) - # - # Saves the model. - # - # If the model is new a record gets created in the database, otherwise - # the existing record gets updated. - # - # By default, save always run validations. If any of them fail the action - # is cancelled and +save+ returns +false+. However, if you supply - # :validate => false, validations are bypassed altogether. See - # ActiveRecord::Validations for more information. - # - # There's a series of callbacks associated with +save+. If any of the - # before_* callbacks return +false+ the action is cancelled and - # +save+ returns +false+. See ActiveRecord::Callbacks for further - # details. - def save - create_or_update - end - - # Saves the model. - # - # If the model is new a record gets created in the database, otherwise - # the existing record gets updated. - # - # With save! validations always run. If any of them fail - # ActiveRecord::RecordInvalid gets raised. See ActiveRecord::Validations - # for more information. - # - # There's a series of callbacks associated with save!. If any of - # the before_* callbacks return +false+ the action is cancelled - # and save! raises ActiveRecord::RecordNotSaved. See - # ActiveRecord::Callbacks for further details. - def save! - create_or_update || raise(RecordNotSaved) - end - - # Deletes the record in the database and freezes this instance to - # reflect that no changes should be made (since they can't be - # persisted). Returns the frozen instance. - # - # The row is simply removed with a SQL +DELETE+ statement on the - # record's primary key, and no callbacks are executed. - # - # To enforce the object's +before_destroy+ and +after_destroy+ - # callbacks, Observer methods, or any :dependent association - # options, use #destroy. - def delete - self.class.delete(id) if persisted? - @destroyed = true - freeze - end - - # Deletes the record in the database and freezes this instance to reflect that no changes should - # be made (since they can't be persisted). - def destroy - if persisted? - self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).delete_all - end - - @destroyed = true - freeze - end - - # Returns an instance of the specified +klass+ with the attributes of the current record. This is mostly useful in relation to - # single-table inheritance structures where you want a subclass to appear as the superclass. This can be used along with record - # identification in Action Pack to allow, say, Client < Company to do something like render :partial => @client.becomes(Company) - # to render that instance using the companies/company partial instead of clients/client. - # - # Note: The new instance will share a link to the same attributes as the original class. So any change to the attributes in either - # instance will affect the other. - def becomes(klass) - became = klass.new - became.instance_variable_set("@attributes", @attributes) - became.instance_variable_set("@attributes_cache", @attributes_cache) - became.instance_variable_set("@new_record", new_record?) - became.instance_variable_set("@destroyed", destroyed?) - became - end - - # Updates a single attribute and saves the record without going through the normal validation procedure. - # This is especially useful for boolean flags on existing records. The regular +update_attribute+ method - # in Base is replaced with this when the validations module is mixed in, which it is by default. - def update_attribute(name, value) - send("#{name}=", value) - save(:validate => false) - end - - # Updates all the attributes from the passed-in Hash and saves the record. If the object is invalid, the saving will - # fail and false will be returned. - def update_attributes(attributes) - self.attributes = attributes - save - end - - # Updates an object just like Base.update_attributes but calls save! instead of save so an exception is raised if the record is invalid. - def update_attributes!(attributes) - self.attributes = attributes - save! - end - - # Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1). - # The increment is performed directly on the underlying attribute, no setter is invoked. - # Only makes sense for number-based attributes. Returns +self+. - def increment(attribute, by = 1) - self[attribute] ||= 0 - self[attribute] += by - self - end - - # Wrapper around +increment+ that saves the record. This method differs from - # its non-bang version in that it passes through the attribute setter. - # Saving is not subjected to validation checks. Returns +true+ if the - # record could be saved. - def increment!(attribute, by = 1) - increment(attribute, by).update_attribute(attribute, self[attribute]) - end - - # Initializes +attribute+ to zero if +nil+ and subtracts the value passed as +by+ (default is 1). - # The decrement is performed directly on the underlying attribute, no setter is invoked. - # Only makes sense for number-based attributes. Returns +self+. - def decrement(attribute, by = 1) - self[attribute] ||= 0 - self[attribute] -= by - self - end - - # Wrapper around +decrement+ that saves the record. This method differs from - # its non-bang version in that it passes through the attribute setter. - # Saving is not subjected to validation checks. Returns +true+ if the - # record could be saved. - def decrement!(attribute, by = 1) - decrement(attribute, by).update_attribute(attribute, self[attribute]) - end - - # Assigns to +attribute+ the boolean opposite of attribute?. So - # if the predicate returns +true+ the attribute will become +false+. This - # method toggles directly the underlying value without calling any setter. - # Returns +self+. - def toggle(attribute) - self[attribute] = !send("#{attribute}?") - self - end - - # Wrapper around +toggle+ that saves the record. This method differs from - # its non-bang version in that it passes through the attribute setter. - # Saving is not subjected to validation checks. Returns +true+ if the - # record could be saved. - def toggle!(attribute) - toggle(attribute).update_attribute(attribute, self[attribute]) - end - - # Reloads the attributes of this object from the database. - # The optional options argument is passed to find when reloading so you - # may do e.g. record.reload(:lock => true) to reload the same record with - # an exclusive row lock. - def reload(options = nil) - clear_aggregation_cache - clear_association_cache - @attributes.update(self.class.send(:with_exclusive_scope) { self.class.find(self.id, options) }.instance_variable_get('@attributes')) - @attributes_cache = {} - self - end - # Returns true if the given attribute is in the attributes hash def has_attribute?(attr_name) @attributes.has_key?(attr_name.to_s) @@ -1980,40 +1696,6 @@ module ActiveRecord #:nodoc: end private - def create_or_update - raise ReadOnlyRecord if readonly? - result = new_record? ? create : update - result != false - end - - # Updates the associated record with values matching those of the instance attributes. - # Returns the number of affected rows. - def update(attribute_names = @attributes.keys) - attributes_with_values = arel_attributes_values(false, false, attribute_names) - return 0 if attributes_with_values.empty? - self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).arel.update(attributes_with_values) - end - - # Creates a record with values matching those of the instance attributes - # and returns its id. - def create - if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name) - self.id = connection.next_sequence_value(self.class.sequence_name) - end - - attributes_values = arel_attributes_values - - new_id = if attributes_values.empty? - self.class.unscoped.insert connection.empty_insert_statement_value - else - self.class.unscoped.insert attributes_values - end - - self.id ||= new_id - - @new_record = false - id - end # Sets the attribute used for single table inheritance to this class name if this is not the ActiveRecord::Base descendant. # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to do Reply.new without having to @@ -2099,17 +1781,6 @@ module ActiveRecord #:nodoc: instance_eval("%@#{sql.gsub('@', '\@')}@") end - # Initializes the attributes array with keys matching the columns from the linked table and - # the values matching the corresponding default value of that column, so - # that a new instance, or one populated from a passed-in Hash, still has all the attributes - # that instances loaded from the database would. - def attributes_from_column_definition - self.class.columns.inject({}) do |attributes, column| - attributes[column.name] = column.default unless column.name == self.class.primary_key - attributes - end - end - # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done # by calling new on the column type or aggregation type (through composed_of) object with these parameters. # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate @@ -2225,12 +1896,14 @@ module ActiveRecord #:nodoc: end Base.class_eval do + include ActiveRecord::Persistence extend ActiveModel::Naming extend QueryCache::ClassMethods extend ActiveSupport::Benchmarkable include ActiveModel::Conversion include Validations + extend CounterCache include Locking::Optimistic, Locking::Pessimistic include AttributeMethods include AttributeMethods::Read, AttributeMethods::Write, AttributeMethods::BeforeTypeCast, AttributeMethods::Query diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 7ebeb6079e..498836aca4 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -233,10 +233,6 @@ module ActiveRecord ] included do - [:create_or_update, :valid?, :create, :update, :destroy].each do |method| - alias_method_chain method, :callbacks - end - extend ActiveModel::Callbacks define_callbacks :validation, :terminator => "result == false", :scope => [:kind, :name] @@ -273,38 +269,13 @@ module ActiveRecord end end - def create_or_update_with_callbacks #:nodoc: - _run_save_callbacks do - create_or_update_without_callbacks - end - end - private :create_or_update_with_callbacks - - def create_with_callbacks #:nodoc: - _run_create_callbacks do - create_without_callbacks - end - end - private :create_with_callbacks - - def update_with_callbacks(*args) #:nodoc: - _run_update_callbacks do - update_without_callbacks(*args) - end - end - private :update_with_callbacks - - def valid_with_callbacks? #:nodoc: + def valid?(*) #:nodoc: @_on_validate = new_record? ? :create : :update - _run_validation_callbacks do - valid_without_callbacks? - end + _run_validation_callbacks { super } end - def destroy_with_callbacks #:nodoc: - _run_destroy_callbacks do - destroy_without_callbacks - end + def destroy #:nodoc: + _run_destroy_callbacks { super } end def deprecated_callback_method(symbol) #:nodoc: @@ -313,5 +284,18 @@ module ActiveRecord send(symbol) end end + + private + def create_or_update #:nodoc: + _run_save_callbacks { super } + end + + def create #:nodoc: + _run_create_callbacks { super } + end + + def update(*) #:nodoc: + _run_update_callbacks { super } + end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 533a7bb8e6..78fffaff6e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -5,23 +5,16 @@ module ActiveRecord module QueryCache class << self def included(base) - base.class_eval do - alias_method_chain :columns, :query_cache - alias_method_chain :select_all, :query_cache - end - dirties_query_cache base, :insert, :update, :delete end def dirties_query_cache(base, *method_names) method_names.each do |method_name| base.class_eval <<-end_code, __FILE__, __LINE__ + 1 - def #{method_name}_with_query_dirty(*args) # def update_with_query_dirty(*args) - clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled - #{method_name}_without_query_dirty(*args) # update_without_query_dirty(*args) - end # end - # - alias_method_chain :#{method_name}, :query_dirty # alias_method_chain :update, :query_dirty + def #{method_name}(*) # def update_with_query_dirty(*args) + clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled + super # update_without_query_dirty(*args) + end # end end_code end end @@ -56,19 +49,19 @@ module ActiveRecord @query_cache.clear end - def select_all_with_query_cache(*args) + def select_all(*args) if @query_cache_enabled - cache_sql(args.first) { select_all_without_query_cache(*args) } + cache_sql(args.first) { super } else - select_all_without_query_cache(*args) + super end end - def columns_with_query_cache(*args) + def columns(*) if @query_cache_enabled - @query_cache["SHOW FIELDS FROM #{args.first}"] ||= columns_without_query_cache(*args) + @query_cache["SHOW FIELDS FROM #{args.first}"] ||= super else - columns_without_query_cache(*args) + super end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb new file mode 100644 index 0000000000..cbebded995 --- /dev/null +++ b/activerecord/lib/active_record/counter_cache.rb @@ -0,0 +1,107 @@ +module ActiveRecord + module CounterCache + # Resets one or more counter caches to their correct value using an SQL + # count query. This is useful when adding new counter caches, or if the + # counter has been corrupted or modified directly by SQL. + # + # ==== Parameters + # + # * +id+ - The id of the object you wish to reset a counter on. + # * +counters+ - One or more counter names to reset + # + # ==== Examples + # + # # For Post with id #1 records reset the comments_count + # Post.reset_counters(1, :comments) + def reset_counters(id, *counters) + object = find(id) + counters.each do |association| + child_class = reflect_on_association(association).klass + counter_name = child_class.reflect_on_association(self.name.downcase.to_sym).counter_cache_column + + connection.update("UPDATE #{quoted_table_name} SET #{connection.quote_column_name(counter_name)} = #{object.send(association).count} WHERE #{connection.quote_column_name(primary_key)} = #{quote_value(object.id)}", "#{name} UPDATE") + end + end + + # A generic "counter updater" implementation, intended primarily to be + # used by increment_counter and decrement_counter, but which may also + # be useful on its own. It simply does a direct SQL update for the record + # with the given ID, altering the given hash of counters by the amount + # given by the corresponding value: + # + # ==== Parameters + # + # * +id+ - The id of the object you wish to update a counter on or an Array of ids. + # * +counters+ - An Array of Hashes containing the names of the fields + # to update as keys and the amount to update the field by as values. + # + # ==== Examples + # + # # For the Post with id of 5, decrement the comment_count by 1, and + # # increment the action_count by 1 + # Post.update_counters 5, :comment_count => -1, :action_count => 1 + # # Executes the following SQL: + # # UPDATE posts + # # SET comment_count = comment_count - 1, + # # action_count = action_count + 1 + # # WHERE id = 5 + # + # # For the Posts with id of 10 and 15, increment the comment_count by 1 + # Post.update_counters [10, 15], :comment_count => 1 + # # Executes the following SQL: + # # UPDATE posts + # # SET comment_count = comment_count + 1, + # # WHERE id IN (10, 15) + def update_counters(id, counters) + updates = counters.inject([]) { |list, (counter_name, increment)| + sign = increment < 0 ? "-" : "+" + list << "#{connection.quote_column_name(counter_name)} = COALESCE(#{connection.quote_column_name(counter_name)}, 0) #{sign} #{increment.abs}" + }.join(", ") + + if id.is_a?(Array) + ids_list = id.map {|i| quote_value(i)}.join(', ') + condition = "IN (#{ids_list})" + else + condition = "= #{quote_value(id)}" + end + + update_all(updates, "#{connection.quote_column_name(primary_key)} #{condition}") + end + + # Increment a number field by one, usually representing a count. + # + # This is used for caching aggregate values, so that they don't need to be computed every time. + # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is + # shown it would have to run an SQL query to find how many posts and comments there are. + # + # ==== Parameters + # + # * +counter_name+ - The name of the field that should be incremented. + # * +id+ - The id of the object that should be incremented. + # + # ==== Examples + # + # # Increment the post_count column for the record with an id of 5 + # DiscussionBoard.increment_counter(:post_count, 5) + def increment_counter(counter_name, id) + update_counters(id, counter_name => 1) + end + + # Decrement a number field by one, usually representing a count. + # + # This works the same as increment_counter but reduces the column value by 1 instead of increasing it. + # + # ==== Parameters + # + # * +counter_name+ - The name of the field that should be decremented. + # * +id+ - The id of the object that should be decremented. + # + # ==== Examples + # + # # Decrement the post_count column for the record with an id of 5 + # DiscussionBoard.decrement_counter(:post_count, 5) + def decrement_counter(counter_name, id) + update_counters(id, counter_name => -1) + end + end +end \ No newline at end of file diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 60ad23f38c..71057efa15 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -48,10 +48,6 @@ module ActiveRecord cattr_accessor :lock_optimistically, :instance_writer => false self.lock_optimistically = true - alias_method_chain :update, :lock - alias_method_chain :destroy, :lock - alias_method_chain :attributes_from_column_definition, :lock - class << self alias_method :locking_column=, :set_locking_column end @@ -62,8 +58,8 @@ module ActiveRecord end private - def attributes_from_column_definition_with_lock - result = attributes_from_column_definition_without_lock + def attributes_from_column_definition + result = super # If the locking column has no default value set, # start the lock version at zero. Note we can't use @@ -77,8 +73,8 @@ module ActiveRecord return result end - def update_with_lock(attribute_names = @attributes.keys) #:nodoc: - return update_without_lock(attribute_names) unless locking_enabled? + def update(attribute_names = @attributes.keys) #:nodoc: + return super unless locking_enabled? return 0 if attribute_names.empty? lock_col = self.class.locking_column @@ -97,7 +93,6 @@ module ActiveRecord ) ).arel.update(arel_attributes_values(false, false, attribute_names)) - unless affected_rows == 1 raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}" end @@ -111,8 +106,8 @@ module ActiveRecord end end - def destroy_with_lock #:nodoc: - return destroy_without_lock unless locking_enabled? + def destroy #:nodoc: + return super unless locking_enabled? unless new_record? lock_col = self.class.locking_column @@ -136,12 +131,6 @@ module ActiveRecord module ClassMethods DEFAULT_LOCKING_COLUMN = 'lock_version' - def self.extended(base) - class < 1) if locking_enabled? - update_counters_without_lock(id, counters) + super end end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb new file mode 100644 index 0000000000..10788630a5 --- /dev/null +++ b/activerecord/lib/active_record/persistence.rb @@ -0,0 +1,230 @@ +module ActiveRecord + module Persistence + # Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet; otherwise, returns false. + def new_record? + @new_record + end + + # Returns true if this object has been destroyed, otherwise returns false. + def destroyed? + @destroyed + end + + # Returns if the record is persisted, i.e. it's not a new record and it was not destroyed. + def persisted? + !(new_record? || destroyed?) + end + + # :call-seq: + # save(options) + # + # Saves the model. + # + # If the model is new a record gets created in the database, otherwise + # the existing record gets updated. + # + # By default, save always run validations. If any of them fail the action + # is cancelled and +save+ returns +false+. However, if you supply + # :validate => false, validations are bypassed altogether. See + # ActiveRecord::Validations for more information. + # + # There's a series of callbacks associated with +save+. If any of the + # before_* callbacks return +false+ the action is cancelled and + # +save+ returns +false+. See ActiveRecord::Callbacks for further + # details. + def save(*) + create_or_update + end + + # Saves the model. + # + # If the model is new a record gets created in the database, otherwise + # the existing record gets updated. + # + # With save! validations always run. If any of them fail + # ActiveRecord::RecordInvalid gets raised. See ActiveRecord::Validations + # for more information. + # + # There's a series of callbacks associated with save!. If any of + # the before_* callbacks return +false+ the action is cancelled + # and save! raises ActiveRecord::RecordNotSaved. See + # ActiveRecord::Callbacks for further details. + def save!(*) + create_or_update || raise(RecordNotSaved) + end + + # Deletes the record in the database and freezes this instance to + # reflect that no changes should be made (since they can't be + # persisted). Returns the frozen instance. + # + # The row is simply removed with a SQL +DELETE+ statement on the + # record's primary key, and no callbacks are executed. + # + # To enforce the object's +before_destroy+ and +after_destroy+ + # callbacks, Observer methods, or any :dependent association + # options, use #destroy. + def delete + self.class.delete(id) if persisted? + @destroyed = true + freeze + end + + # Deletes the record in the database and freezes this instance to reflect that no changes should + # be made (since they can't be persisted). + def destroy + if persisted? + self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).delete_all + end + + @destroyed = true + freeze + end + + # Returns an instance of the specified +klass+ with the attributes of the current record. This is mostly useful in relation to + # single-table inheritance structures where you want a subclass to appear as the superclass. This can be used along with record + # identification in Action Pack to allow, say, Client < Company to do something like render :partial => @client.becomes(Company) + # to render that instance using the companies/company partial instead of clients/client. + # + # Note: The new instance will share a link to the same attributes as the original class. So any change to the attributes in either + # instance will affect the other. + def becomes(klass) + became = klass.new + became.instance_variable_set("@attributes", @attributes) + became.instance_variable_set("@attributes_cache", @attributes_cache) + became.instance_variable_set("@new_record", new_record?) + became.instance_variable_set("@destroyed", destroyed?) + became + end + + # Updates a single attribute and saves the record without going through the normal validation procedure. + # This is especially useful for boolean flags on existing records. The regular +update_attribute+ method + # in Base is replaced with this when the validations module is mixed in, which it is by default. + def update_attribute(name, value) + send("#{name}=", value) + save(:validate => false) + end + + # Updates all the attributes from the passed-in Hash and saves the record. If the object is invalid, the saving will + # fail and false will be returned. + def update_attributes(attributes) + self.attributes = attributes + save + end + + # Updates an object just like Base.update_attributes but calls save! instead of save so an exception is raised if the record is invalid. + def update_attributes!(attributes) + self.attributes = attributes + save! + end + + # Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1). + # The increment is performed directly on the underlying attribute, no setter is invoked. + # Only makes sense for number-based attributes. Returns +self+. + def increment(attribute, by = 1) + self[attribute] ||= 0 + self[attribute] += by + self + end + + # Wrapper around +increment+ that saves the record. This method differs from + # its non-bang version in that it passes through the attribute setter. + # Saving is not subjected to validation checks. Returns +true+ if the + # record could be saved. + def increment!(attribute, by = 1) + increment(attribute, by).update_attribute(attribute, self[attribute]) + end + + # Initializes +attribute+ to zero if +nil+ and subtracts the value passed as +by+ (default is 1). + # The decrement is performed directly on the underlying attribute, no setter is invoked. + # Only makes sense for number-based attributes. Returns +self+. + def decrement(attribute, by = 1) + self[attribute] ||= 0 + self[attribute] -= by + self + end + + # Wrapper around +decrement+ that saves the record. This method differs from + # its non-bang version in that it passes through the attribute setter. + # Saving is not subjected to validation checks. Returns +true+ if the + # record could be saved. + def decrement!(attribute, by = 1) + decrement(attribute, by).update_attribute(attribute, self[attribute]) + end + + # Assigns to +attribute+ the boolean opposite of attribute?. So + # if the predicate returns +true+ the attribute will become +false+. This + # method toggles directly the underlying value without calling any setter. + # Returns +self+. + def toggle(attribute) + self[attribute] = !send("#{attribute}?") + self + end + + # Wrapper around +toggle+ that saves the record. This method differs from + # its non-bang version in that it passes through the attribute setter. + # Saving is not subjected to validation checks. Returns +true+ if the + # record could be saved. + def toggle!(attribute) + toggle(attribute).update_attribute(attribute, self[attribute]) + end + + # Reloads the attributes of this object from the database. + # The optional options argument is passed to find when reloading so you + # may do e.g. record.reload(:lock => true) to reload the same record with + # an exclusive row lock. + def reload(options = nil) + clear_aggregation_cache + clear_association_cache + @attributes.update(self.class.send(:with_exclusive_scope) { self.class.find(self.id, options) }.instance_variable_get('@attributes')) + @attributes_cache = {} + self + end + + private + def create_or_update + raise ReadOnlyRecord if readonly? + result = new_record? ? create : update + result != false + end + + # Updates the associated record with values matching those of the instance attributes. + # Returns the number of affected rows. + def update(attribute_names = @attributes.keys) + attributes_with_values = arel_attributes_values(false, false, attribute_names) + return 0 if attributes_with_values.empty? + self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).arel.update(attributes_with_values) + end + + # Creates a record with values matching those of the instance attributes + # and returns its id. + def create + if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name) + self.id = connection.next_sequence_value(self.class.sequence_name) + end + + attributes_values = arel_attributes_values + + new_id = if attributes_values.empty? + self.class.unscoped.insert connection.empty_insert_statement_value + else + self.class.unscoped.insert attributes_values + end + + self.id ||= new_id + + @new_record = false + id + end + + # Initializes the attributes array with keys matching the columns from the linked table and + # the values matching the corresponding default value of that column, so + # that a new instance, or one populated from a passed-in Hash, still has all the attributes + # that instances loaded from the database would. + def attributes_from_column_definition + self.class.columns.inject({}) do |attributes, column| + attributes[column.name] = column.default unless column.name == self.class.primary_key + attributes + end + end + end +end \ No newline at end of file diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index da075dabd3..9fba8f0aca 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -11,9 +11,6 @@ module ActiveRecord extend ActiveSupport::Concern included do - alias_method_chain :create, :timestamps - alias_method_chain :update, :timestamps - class_inheritable_accessor :record_timestamps, :instance_writer => false self.record_timestamps = true end @@ -39,35 +36,34 @@ module ActiveRecord save! end + private + def create #:nodoc: + if record_timestamps + current_time = current_time_from_proper_timezone - private - def create_with_timestamps #:nodoc: - if record_timestamps - current_time = current_time_from_proper_timezone + write_attribute('created_at', current_time) if respond_to?(:created_at) && created_at.nil? + write_attribute('created_on', current_time) if respond_to?(:created_on) && created_on.nil? - write_attribute('created_at', current_time) if respond_to?(:created_at) && created_at.nil? - write_attribute('created_on', current_time) if respond_to?(:created_on) && created_on.nil? - - write_attribute('updated_at', current_time) if respond_to?(:updated_at) && updated_at.nil? - write_attribute('updated_on', current_time) if respond_to?(:updated_on) && updated_on.nil? - end - - create_without_timestamps + write_attribute('updated_at', current_time) if respond_to?(:updated_at) && updated_at.nil? + write_attribute('updated_on', current_time) if respond_to?(:updated_on) && updated_on.nil? end - def update_with_timestamps(*args) #:nodoc: - if record_timestamps && (!partial_updates? || changed?) - current_time = current_time_from_proper_timezone + super + end - write_attribute('updated_at', current_time) if respond_to?(:updated_at) - write_attribute('updated_on', current_time) if respond_to?(:updated_on) - end + def update(*args) #:nodoc: + if record_timestamps && (!partial_updates? || changed?) + current_time = current_time_from_proper_timezone - update_without_timestamps(*args) - end - - def current_time_from_proper_timezone - self.class.default_timezone == :utc ? Time.now.utc : Time.now + write_attribute('updated_at', current_time) if respond_to?(:updated_at) + write_attribute('updated_on', current_time) if respond_to?(:updated_on) end + + super + end + + def current_time_from_proper_timezone + self.class.default_timezone == :utc ? Time.now.utc : Time.now + end end end \ No newline at end of file diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 1a195fbb81..5a8e2ce880 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -9,10 +9,6 @@ module ActiveRecord end included do - [:destroy, :save, :save!].each do |method| - alias_method_chain method, :transactions - end - define_model_callbacks :commit, :commit_on_update, :commit_on_create, :commit_on_destroy, :only => :after define_model_callbacks :rollback, :rollback_on_update, :rollback_on_create, :rollback_on_destroy end @@ -213,16 +209,18 @@ module ActiveRecord self.class.transaction(&block) end - def destroy_with_transactions #:nodoc: - with_transaction_returning_status(:destroy_without_transactions) + def destroy #:nodoc: + with_transaction_returning_status { super } end - def save_with_transactions(*args) #:nodoc: - rollback_active_record_state! { with_transaction_returning_status(:save_without_transactions, *args) } + def save(*) #:nodoc: + rollback_active_record_state! do + with_transaction_returning_status { super } + end end - def save_with_transactions! #:nodoc: - with_transaction_returning_status(:save_without_transactions!) + def save!(*) #:nodoc: + with_transaction_returning_status { super } end # Reset id and @new_record if the transaction rolls back. @@ -279,11 +277,11 @@ module ActiveRecord # # This method is available within the context of an ActiveRecord::Base # instance. - def with_transaction_returning_status(method, *args) + def with_transaction_returning_status status = nil self.class.transaction do add_to_transaction - status = send(method, *args) + status = yield raise ActiveRecord::Rollback unless status end status diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index b2ee51fa51..55c4236874 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -19,11 +19,6 @@ module ActiveRecord extend ActiveSupport::Concern include ActiveModel::Validations - included do - alias_method_chain :save, :validation - alias_method_chain :save!, :validation - end - module ClassMethods # Creates an object just like Base.create but calls save! instead of save # so an exception is raised if the record is invalid. @@ -39,39 +34,37 @@ module ActiveRecord end end - module InstanceMethods - # The validation process on save can be skipped by passing false. The regular Base#save method is - # replaced with this when the validations module is mixed in, which it is by default. - def save_with_validation(options=nil) - perform_validation = case options - when NilClass - true - when Hash - options[:validate] != false - else - ActiveSupport::Deprecation.warn "save(#{options}) is deprecated, please give save(:validate => #{options}) instead", caller - options - end + # The validation process on save can be skipped by passing false. The regular Base#save method is + # replaced with this when the validations module is mixed in, which it is by default. + def save(options=nil) + return super if valid?(options) + false + end - if perform_validation && valid? || !perform_validation - save_without_validation + def save_without_validation! + save!(:validate => false) + end + + # Attempts to save the record just like Base#save but will raise a RecordInvalid exception instead of returning false + # if the record is not valid. + def save!(options = nil) + return super if valid?(options) + raise RecordInvalid.new(self) + end + + # Runs all the specified validations and returns true if no errors were added otherwise false. + def valid?(options = nil) + perform_validation = case options + when NilClass + true + when Hash + options[:validate] != false else - false - end + ActiveSupport::Deprecation.warn "save(#{options}) is deprecated, please give save(:validate => #{options}) instead", caller + options end - # Attempts to save the record just like Base#save but will raise a RecordInvalid exception instead of returning false - # if the record is not valid. - def save_with_validation! - if valid? - save_without_validation! - else - raise RecordInvalid.new(self) - end - end - - # Runs all the specified validations and returns true if no errors were added otherwise false. - def valid? + if perform_validation errors.clear self.validation_context = new_record? ? :create : :update @@ -86,16 +79,12 @@ module ActiveRecord end errors.empty? - end - - def invalid? - !valid? + else + true end end end end -Dir[File.dirname(__FILE__) + "/validations/*.rb"].sort.each do |path| - filename = File.basename(path) - require "active_record/validations/#{filename}" -end +require "active_record/validations/associated" +require "active_record/validations/uniqueness" diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index aa2d9527f9..66874cdad1 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -195,7 +195,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_raises(ActiveRecord::RecordNotFound) { Person.find(p1.id) } assert_raises(ActiveRecord::RecordNotFound) { LegacyThing.find(t.id) } end - + def test_quote_table_name ref = references(:michael_magician) ref.favourite = !ref.favourite @@ -206,8 +206,11 @@ class OptimisticLockingTest < ActiveRecord::TestCase # is nothing else being updated. def test_update_without_attributes_does_not_only_update_lock_version assert_nothing_raised do - p1 = Person.new(:first_name => 'anika') - p1.send(:update_with_lock, []) + p1 = Person.create!(:first_name => 'anika') + lock_version = p1.lock_version + p1.save + p1.reload + assert_equal lock_version, p1.lock_version end end