We write about Ruby on Rails, React.js, React Native, remote work, open source, engineering and design.
Rails provides some good tools like automatically updating created_at
and updated_at
columns. Developers do not need to worry about these columns.
Rails updates these columns automatically which is great.
However I have a unique business need where I need to update a column but I do not want updated_at
to be changed. Or we can see the problem this way. I want to change the updated_at
to a particular value.
1>> User.first.update_attributes(:updated_at => 100.years.ago)
2UPDATE `users` SET `updated_at` = '2009-01-20 19:15:25' WHERE `id` = 2
Look at the sql that is generated. Rails discarded the updated_at
value that I had supplied and replaced the value by the current time. Rails works fine if you supply created_at
value. It is the updated_at
value that is discarded.
Rails provides a feature called ActiveRecord::Base.record_timestamps . Using this feature I can tell rails to not to auto time stamp records.
Let's try that.
1>> User.record_timestamps=false
2=> false
3>> User.first.update_attributes(:updated_at => 100.years.ago)
4UPDATE `users` SET `updated_at` = '1909-01-20 18:52:50' WHERE `id` = 2
5>> User.record_timestamps=true
6=> true
It worked. I have successfully set updated_at
to year 1909. However there is a problem.
For a brief duration User.record_timestamps
was set to false. That is a class level variable. It means that for that brief duration if any other User record is updated then that record will not have correct updated_at
value. That is not right. I want just one record ( User.first) to not to change updated_at
without changing the behavior for the whole application.
In order to isolate the behavior to only the record we are interested in, I can do this.
1>> u = User.first
2
3>> class << u
4>> def record_timestamps
5>> false
6>> end
7>> end
8
9>> u.update_attributes(:updated_at => 100.years.ago)
10UPDATE `users` SET `updated_at` = '1909-01-20 18:58:10' WHERE `id` = 2
11
12>> class << u
13>> def record_timestamps
14>> super
15>> end
16>> end
17
18>> u.update_attributes(:updated_at => 200.years.ago)
19UPDATE `users` SET `updated_at` = '2009-01-20 19:22:11' WHERE `id` = 2
In order to restrict the changes to a model, I am opening up the metaclass of u ( user object) and in that object I am adding a method called record_timestamps
. The idea is to insert a method called record_timestamps
in the metaclass which will return true and in this way the changes are restricted to a single object rather than making change at the class level.
At this point the meta class of the user object has the method record_timestamps
and this returns false. Now I update the record with updated_at
set to 100 years ago. And I succeed.
Now I need to put the object behavior back to normal. I open up the metaclass and call super on the method so that the method call will go up the chain. And that's what happens when I try to test updated_at
. This time the updated_at
value that I set is ignored and rails changes the updated_at
value.
update_without_timestamping
methodThis strategy of opening up an instance object works but it is messy. I would like to have a method that is much easier to use and this is what I came up with. Stick this piece of code in an initializer.
1module ActiveRecord
2 class Base
3
4 def update_record_without_timestamping
5 class << self
6 def record_timestamps; false; end
7 end
8
9 save!
10
11 class << self
12 def record_timestamps; super ; end
13 end
14 end
15
16 end
17end
This is how you can use it.
1>> u = User.first
2>> u.updated_at = 100.years.ago
3>> u.created_at = 200.years.ago
4>> u.update_record_without_timestamping
5UPDATE `users` SET `created_at` = '1809-01-20 19:08:21',
6`updated_at` = '1909-01-20 19:08:22' WHERE `id` = 2
In the above solution I used super when I want to bring back the default auto time stamping behavior. In stead of super I can also use remove_method. More about the what remove_method does is here .
1module ActiveRecord
2 class Base
3
4 def update_record_without_timestamping
5 class << self
6 def record_timestamps; false; end
7 end
8
9 save!
10
11 class << self
12 remove_method :record_timestamps
13 end
14 end
15
16 end
17end
Using the above technique, I can fully control updated_at
values without rails messing up anything.