Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Added :touch option to belongs_to associations that will touch the pa…
…rent record when the current record is saved or destroyed [DHH]
  • Loading branch information
dhh committed Apr 16, 2009
1 parent fdb61f0 commit abb899c
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 29 deletions.
4 changes: 3 additions & 1 deletion activerecord/CHANGELOG
@@ -1,6 +1,8 @@
*Edge*

* Added ActiveRecord::Base#touch to update the updated_at/on attributes with the current time [DHH]
* Added :touch option to belongs_to associations that will touch the parent record when the current record is saved or destroyed [DHH]

* Added ActiveRecord::Base#touch to update the updated_at/on attributes (or another specified timestamp) with the current time [DHH]


*2.3.2 [Final] (March 15, 2009)*
Expand Down
68 changes: 45 additions & 23 deletions activerecord/lib/active_record/associations.rb
Expand Up @@ -981,6 +981,9 @@ def has_one(association_id, options = {})
# If false, don't validate the associated objects when saving the parent object. +false+ by default.
# [:autosave]
# If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default.
# [:touch]
# If true, the associated object will be touched (the updated_at/on attributes set to now) when this record is either saved or
# destroyed. If you specify a symbol, that attribute will be updated with the current time instead of the updated_at/on attribute.
#
# Option examples:
# belongs_to :firm, :foreign_key => "client_of"
Expand All @@ -990,6 +993,8 @@ def has_one(association_id, options = {})
# belongs_to :attachable, :polymorphic => true
# belongs_to :project, :readonly => true
# belongs_to :post, :counter_cache => true
# belongs_to :company, :touch => true
# belongs_to :company, :touch => :employees_last_updated_at
def belongs_to(association_id, options = {})
reflection = create_belongs_to_reflection(association_id, options)

Expand All @@ -1001,28 +1006,8 @@ def belongs_to(association_id, options = {})
association_constructor_method(:create, reflection, BelongsToAssociation)
end

# Create the callbacks to update counter cache
if options[:counter_cache]
cache_column = reflection.counter_cache_column

method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
define_method(method_name) do
association = send(reflection.name)
association.class.increment_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
end
after_create method_name

method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym
define_method(method_name) do
association = send(reflection.name)
association.class.decrement_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
end
before_destroy method_name

module_eval(
"#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)"
)
end
add_counter_cache_callbacks(reflection) if options[:counter_cache]
add_touch_callbacks(reflection, options[:touch]) if options[:touch]

configure_dependency_for_belongs_to(reflection)
end
Expand Down Expand Up @@ -1329,6 +1314,43 @@ def association_constructor_method(constructor, reflection, association_proxy_cl
end
end

def add_counter_cache_callbacks(reflection)
cache_column = reflection.counter_cache_column

method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
define_method(method_name) do
association = send(reflection.name)
association.class.increment_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
end
after_create(method_name)

method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym
define_method(method_name) do
association = send(reflection.name)
association.class.decrement_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
end
before_destroy(method_name)

module_eval(
"#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)"
)
end

def add_touch_callbacks(reflection, touch_attribute)
method_name = "belongs_to_touch_after_save_or_destroy_for_#{reflection.name}".to_sym
define_method(method_name) do
association = send(reflection.name)

if touch_attribute == true
association.touch unless association.nil?
else
association.touch(touch_attribute) unless association.nil?
end
end
after_save(method_name)
after_destroy(method_name)
end

def find_with_associations(options = {})
catch :invalid_query do
join_dependency = JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
Expand Down Expand Up @@ -1499,7 +1521,7 @@ def create_has_one_through_reflection(association_id, options)
@@valid_keys_for_belongs_to_association = [
:class_name, :foreign_key, :foreign_type, :remote, :select, :conditions,
:include, :dependent, :counter_cache, :extend, :polymorphic, :readonly,
:validate
:validate, :touch
]

def create_belongs_to_reflection(association_id, options)
Expand Down
16 changes: 13 additions & 3 deletions activerecord/lib/active_record/timestamp.rb
Expand Up @@ -18,11 +18,21 @@ def self.included(base) #:nodoc:

# Saves the record with the updated_at/on attributes set to the current time.
# If the save fails because of validation errors, an ActiveRecord::RecordInvalid exception is raised.
def touch
# If an attribute name is passed, that attribute is used for the touch instead of the updated_at/on attributes.
#
# Examples:
#
# product.touch # updates updated_at
# product.touch(:designed_at) # updates the designed_at attribute
def touch(attribute = nil)
current_time = current_time_from_proper_timezone

write_attribute('updated_at', current_time) if respond_to?(:updated_at)
write_attribute('updated_on', current_time) if respond_to?(:updated_on)
if attribute
write_attribute(attribute, current_time)
else
write_attribute('updated_at', current_time) if respond_to?(:updated_at)
write_attribute('updated_on', current_time) if respond_to?(:updated_on)
end

save!
end
Expand Down
47 changes: 46 additions & 1 deletion activerecord/test/cases/timestamp_test.rb
@@ -1,8 +1,10 @@
require 'cases/helper'
require 'models/developer'
require 'models/owner'
require 'models/pet'

class TimestampTest < ActiveRecord::TestCase
fixtures :developers
fixtures :developers, :owners, :pets

def setup
@developer = Developer.first
Expand All @@ -27,4 +29,47 @@ def test_touching_a_record_updates_its_timestamp

assert @previously_updated_at != @developer.updated_at
end

def test_touching_a_different_attribute
previously_created_at = @developer.created_at
@developer.touch(:created_at)

assert previously_created_at != @developer.created_at
end

def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at
pet = Pet.first
owner = pet.owner
previously_owner_updated_at = owner.updated_at

pet.name = "Fluffy the Third"
pet.save

assert previously_owner_updated_at != pet.owner.updated_at
end

def test_destroying_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at
pet = Pet.first
owner = pet.owner
previously_owner_updated_at = owner.updated_at

pet.destroy

assert previously_owner_updated_at != pet.owner.updated_at
end

def test_saving_a_record_with_a_belongs_to_that_specifies_touching_a_specific_attribute_the_parent_should_update_that_attribute
Pet.belongs_to :owner, :touch => :happy_at

pet = Pet.first
owner = pet.owner
previously_owner_happy_at = owner.happy_at

pet.name = "Fluffy the Third"
pet.save

assert previously_owner_happy_at != pet.owner.happy_at
ensure
Pet.belongs_to :owner, :touch => true
end
end
2 changes: 1 addition & 1 deletion activerecord/test/models/pet.rb
@@ -1,5 +1,5 @@
class Pet < ActiveRecord::Base
set_primary_key :pet_id
belongs_to :owner
belongs_to :owner, :touch => true
has_many :toys
end
2 changes: 2 additions & 0 deletions activerecord/test/schema/schema.rb
Expand Up @@ -281,6 +281,8 @@ def create_table(*args, &block)

create_table :owners, :primary_key => :owner_id ,:force => true do |t|
t.string :name
t.column :updated_at, :datetime
t.column :happy_at, :datetime
end


Expand Down

3 comments on commit abb899c

@drnic
Copy link
Contributor

@drnic drnic commented on abb899c Apr 17, 2009

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice touch indeed

@actsasflinn
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very nice, I’ve been doing something like this for a while so this is a welcome addition

@atnan
Copy link
Contributor

@atnan atnan commented on abb899c Apr 17, 2009

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice change, but it seems a little counter-intuitive to call AR::Base#save! from AR::Base#touch. Any reason for not sticking to the usual convention?

Please sign in to comment.