Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Add Touch method to ActiveRelation #8343

Open
wants to merge 2 commits into from

10 participants

@duggiefresh

This allows for more efficient touching of many records

activerecord/lib/active_record/relation.rb
((13 lines not shown))
+ # Person.all.touch(:created_at)
+ # # => "UPDATE \"persons\" SET \"updated_at\" = '2012-11-27 23:12:25.745411', \"created_at\" = '2012-11-27 23:12:25.745411'"
+ #
+ # # Touch records with scope
+ # Person.where(:name => 'David').touch
+ # # => "UPDATE \"persons\" SET \"updated_at\" = '2012-11-27 23:12:30.087110' WHERE \"persons\".\"name\" = 'David'"
+ def touch(name = nil)
+ attributes = [:updated_at]
+ attributes << name if name
+ now = klass.default_timezone == :utc ? Time.now.utc : Time.now
+ value_hash = attributes.inject({}) do |values, column|
+ values[table[column]] = now
+ values
+ end
+ sql = compile_update(value_hash).to_sql
+ ActiveRecord::Base.connection.execute(sql)
@jeremy Owner
jeremy added a note

Possible to implement using update_all ?

def touch(name = nil)
  name ||= :updated_at
  now = klass.default_timezone == :utc ? Time.now.utc : Time.now
  update_all name => now
end

ActiveRecord#touch always updates :updated_at in addition to any optional passed attribute.

My code above emulates this behavior on a batch scale.

@jeremy Owner
jeremy added a note

Do you need to reimplement the update using arel, or can you use update_all?

@jeremy Thank you. That shortens the code a bit!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@jeremy jeremy commented on the diff
activerecord/lib/active_record/relation.rb
((6 lines not shown))
+ # === Examples
+ #
+ # # Touch all records
+ # Person.all.touch
+ # # => "UPDATE \"persons\" SET \"updated_at\" = '2063-04-04 22:55:23.132670'"
+ #
+ # # Touch multiple records with a custom attribute
+ # Person.all.touch(:created_at)
+ # # => "UPDATE \"persons\" SET \"updated_at\" = '2012-11-27 23:12:25.745411', \"created_at\" = '2012-11-27 23:12:25.745411'"
+ #
+ # # Touch records with scope
+ # Person.where(:name => 'David').touch
+ # # => "UPDATE \"persons\" SET \"updated_at\" = '2012-11-27 23:12:30.087110' WHERE \"persons\".\"name\" = 'David'"
+ def touch(name = nil)
+ attributes = [:updated_at]
+ attributes << name if name
@jeremy Owner
jeremy added a note

This always sets updated_at, so you can't touch(:other_timestamp) without changing updated_at. Not sure if that's a good idea. It's not demonstrated in the test coverage, in any case.

The behavior of this method closely follows the behavior of ActiveRecord#touch as shown here: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/persistence.rb#L338-L357

The only caveat is that the :udpated_at attribute is something that is being derived form an instance method in ActiveRecord which of course is inaccessible in this case.

@jeremy Owner
jeremy added a note

Good point. That feels weird to me too :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@jeremy
Owner

@duggieawesome got an example of when you've needed this & why? It does feel natural to me but, upon review, I don't have a use in my apps.

@bcardarella

@jeremy there are use cases with admin sections when touching a bunch of records have been necessary for our client apps. @btucker has also expressed an interest in this behavior: https://twitter.com/btucker/status/264108386717667328

@jeremy
Owner

Thoughts on optimistic locking?

@bcardarella

@jeremy shouldn't that be the responsibility of update_all?

@jeremy
Owner

#touch increments the lock on the record manually. update_all isn't operating on a collection so it can't optimistically lock en masse.

@bcardarella

@jeremy I see your point. What is your suggestion, we believe this method makes sense but obviously want to have it implemented in the most responsible way

@bcardarella

@jeremy according to: http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html OptimisticLocking requires a lock_version column on the record that ActiveRecord then updates to ensure proper locking between multiple instances. Is the concern to ensure the optimistic locking is still occurring and increment the lock_version if present? If that is the case can we just add a incr on that column if it is present? This would require something like:

update users
set updated_at = #{Time.now},
lock_version = lock_version + 1

But again, I feel this would be the responsibility of update_all

@btucker

Thanks for working on this @duggieawesome & @bcardarella!

#touch increments the lock on the record manually

My opinion, actually, is that having #touch in any context increment the lock version is unexpected.

this is the situations I would want to avoid:

class Person < AR::B
  belongs_to :group, touch: true
end

g = Group.find(1)
p = g.persons.first

p.name = "new name"
p.save

g.name = "new group name"
g.save # raises ActiveRecord::StaleObjectError

I haven't actually used Optimistic Locking in Rails, but am I correct in my code reading that the above currently happens?

@duggiefresh

Hey @jeremy any updates to this issue?

Thank you.

@strzalek
Collaborator

@jeremy any updates?

@arunagw
Collaborator

@duggiefresh Hey, Rebase is also required with master for this Pull Request.

@dgalarza

Any updates on this feature? I recently started trying to put something similar together because I hadn't noticed a pull request was opened for it already. It's very useful when using key-based caching to expire a large number of records at once.

@bcardarella

@duggiefresh rebase por favor

duggiefresh added some commits
@duggiefresh duggiefresh Add Touch method to ActiveRelation
This will allow for a more efficient touch on multiple records, handled
by the database rather than iterating objects in Ruby
fd9853e
@duggiefresh duggiefresh Create an update_on field for developers.yml
This is a required addition for activerecord/test/cases/integration_test.rb
32ae453
@dgalarza

I'm also curious if it might follow convention more closely if the method was named touch_all rather than touch. It utilizes update_all as it is and would more closely represent the relationship between them. It also follows the convention of update_all, destroy_all, etc..

@leafac

I see this PR has been stale for a while. Can I help making any progress?

@duggiefresh

Thanks. @leafac I've got time during this holiday to work on this. Feel free to pull it down and check it out.

@exviva exviva commented on the diff
activerecord/lib/active_record/relation.rb
((5 lines not shown))
+ # Touches multiple records.
+ #
+ # === Examples
+ #
+ # # Touch all records
+ # Person.all.touch
+ # # => "UPDATE \"persons\" SET \"updated_at\" = '2063-04-04 22:55:23.132670'"
+ #
+ # # Touch multiple records with a custom attribute
+ # Person.all.touch(:created_at)
+ # # => "UPDATE \"persons\" SET \"updated_at\" = '2012-11-27 23:12:25.745411', \"created_at\" = '2012-11-27 23:12:25.745411'"
+ #
+ # # Touch records with scope
+ # Person.where(:name => 'David').touch
+ # # => "UPDATE \"persons\" SET \"updated_at\" = '2012-11-27 23:12:30.087110' WHERE \"persons\".\"name\" = 'David'"
+ def touch(name = nil)
@exviva
exviva added a note

Shouldn't it be called touch_all, for consistency with update_all, destroy_all, etc.?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@rafaelfranca
Owner

I think we should address the optimistic locking issue on this method. touch at instance level increment it, so lets keep the same behaviour at the class method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Sep 25, 2013
  1. @duggiefresh

    Add Touch method to ActiveRelation

    duggiefresh authored
    This will allow for a more efficient touch on multiple records, handled
    by the database rather than iterating objects in Ruby
  2. @duggiefresh

    Create an update_on field for developers.yml

    duggiefresh authored
    This is a required addition for activerecord/test/cases/integration_test.rb
This page is out of date. Refresh to see the latest.
View
28 activerecord/lib/active_record/relation.rb
@@ -519,7 +519,33 @@ def to_sql
end
end
- # Returns a hash of where conditions.
+ # Touches multiple records.
+ #
+ # === Examples
+ #
+ # # Touch all records
+ # Person.all.touch
+ # # => "UPDATE \"persons\" SET \"updated_at\" = '2063-04-04 22:55:23.132670'"
+ #
+ # # Touch multiple records with a custom attribute
+ # Person.all.touch(:created_at)
+ # # => "UPDATE \"persons\" SET \"updated_at\" = '2012-11-27 23:12:25.745411', \"created_at\" = '2012-11-27 23:12:25.745411'"
+ #
+ # # Touch records with scope
+ # Person.where(:name => 'David').touch
+ # # => "UPDATE \"persons\" SET \"updated_at\" = '2012-11-27 23:12:30.087110' WHERE \"persons\".\"name\" = 'David'"
+ def touch(name = nil)
@exviva
exviva added a note

Shouldn't it be called touch_all, for consistency with update_all, destroy_all, etc.?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ attributes = [:updated_at]
+ attributes << name if name
@jeremy Owner
jeremy added a note

This always sets updated_at, so you can't touch(:other_timestamp) without changing updated_at. Not sure if that's a good idea. It's not demonstrated in the test coverage, in any case.

The behavior of this method closely follows the behavior of ActiveRecord#touch as shown here: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/persistence.rb#L338-L357

The only caveat is that the :udpated_at attribute is something that is being derived form an instance method in ActiveRecord which of course is inaccessible in this case.

@jeremy Owner
jeremy added a note

Good point. That feels weird to me too :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ now = klass.default_timezone == :utc ? Time.now.utc : Time.now
+ value_hash = attributes.inject({}) do |values, column|
+ values[column] = now
+ values
+ end
+ update_all(value_hash)
+ end
+
+ # Returns a hash of where conditions
#
# User.where(name: 'Oscar').where_values_hash
# # => {name: "Oscar"}
View
18 activerecord/test/cases/relations_test.rb
@@ -1391,6 +1391,24 @@ def test_presence
assert topics.loaded?
end
+ def test_touching_multiple_record_updates_their_timestamps
+ Developer.all.touch
+ assert Developer.first.updated_at >= 1.second.ago, "First Developer was not updated"
+ assert Developer.last.updated_at >= 1.second.ago, "Last Developer was not updated"
+ end
+
+ def test_touching_multiple_record_with_custom_timestamp
+ Developer.all.touch(:created_at)
+ assert Developer.first.created_at >= 1.second.ago
+ assert Developer.last.created_at >= 1.second.ago
+ end
+
+ def test_touching_records_from_a_scope
+ Developer.where(:name => 'David').touch
+ assert Developer.first.updated_at >= 1.second.ago, "First Developer was not updated"
+ assert Developer.last.updated_at <= 1.second.ago, "Last Developer was updated"
+ end
+
test "find_by with hash conditions returns the first matching record" do
assert_equal posts(:eager_other), Post.order(:id).find_by(author_id: 2)
end
View
14 activerecord/test/fixtures/developers.yml
@@ -2,20 +2,32 @@ david:
id: 1
name: David
salary: 80000
+ created_at: <%= 10.minutes.ago.to_s(:db) %>
+ updated_at: <%= 5.minutes.ago.to_s(:db) %>
+ updated_on: <%= 5.minutes.ago.to_s(:db) %>
jamis:
id: 2
name: Jamis
salary: 150000
+ created_at: <%= 15.minutes.ago.to_s(:db) %>
+ updated_at: <%= 12.minutes.ago.to_s(:db) %>
+ updated_on: <%= 12.minutes.ago.to_s(:db) %>
<% (3..10).each do |digit| %>
dev_<%= digit %>:
id: <%= digit %>
name: fixture_<%= digit %>
salary: 100000
+ created_at: <%= (digit+5).minutes.ago.to_s(:db) %>
+ updated_at: <%= digit.minutes.ago.to_s(:db) %>
+ updated_on: <%= digit.minutes.ago.to_s(:db) %>
<% end %>
poor_jamis:
id: 11
name: Jamis
- salary: 9000
+ salary: 9000
+ created_at: <%= 20.minutes.ago.to_s(:db) %>
+ updated_at: <%= 16.minutes.ago.to_s(:db) %>
+ updated_on: <%= 16.minutes.ago.to_s(:db) %>
Something went wrong with that request. Please try again.