Skipping :touch associations when saving an ActiveRecord object
One option that avoids directly monkey patching is to override the method that gets created when you have a relation with a :touch
attribute.
Given the setup from the OP:
class Student < ActiveRecord::Base
belongs_to :school, touch: true
attr_accessor :skip_touch
def belongs_to_touch_after_save_or_destroy_for_school
super unless skip_touch
end
after_commit :reset_skip_touch
def reset_skip_touch
skip_touch = false
end
end
@student.skip_touch = true
@student.save # touch will be skipped for this save
This is obviously pretty hacky and depends on really specific internal implementation details in AR. Can I disable touch for belongs_to association temporarily when I save a model?
I'd suggest the opposite to be explicit, only touch in cases that you want to. Testing for the negative will quickly become unmanageable and could be hard to pick up by those unfamiliar with the code.
You can achieve a simple touch by calling topic.touch
How to skip ActiveRecord callbacks?
For Rails 2, but not Rails 3 you can use these:
object.send(:create_without_callbacks)
object.send(:update_without_callbacks)
Disable touch for the duration of a block in ActiveRecord
In Rails 4.1 or later you can use the ActiveRecord no_touching
method to prevent touching in a single model or in all models like this:
ActiveRecord::Base.no_touching do
Project.first.touch # does nothing
Message.first.touch # does nothing
end
Project.no_touching do
Project.first.touch # does nothing
Message.first.touch # works, but does not touch the associated project
end
Is there a way to avoid automatically updating Rails timestamp fields?
Do this in a migration or in a rake task (or in the new database seeds if you're on edge rails):
ActiveRecord::Base.record_timestamps = false
begin
run_the_code_that_imports_the_data
ensure
ActiveRecord::Base.record_timestamps = true # don't forget to enable it again!
end
You can safely set created_at
and updated_at
manually, Rails won't complain.Note:
This also works on individual models, e.g.
User.record_timestamps = false
How can I avoid running ActiveRecord callbacks?
This solution is Rails 2 only.
I just investigated this and I think I have a solution. There are two ActiveRecord private methods that you can use:
update_without_callbacks
create_without_callbacks
You're going to have to use send to call these methods. examples:p = Person.new(:name => 'foo')
p.send(:create_without_callbacks)
p = Person.find(1)
p.send(:update_without_callbacks)
This is definitely something that you'll only really want to use in the console or while doing some random tests. Hope this helps! Skip callbacks on Factory Girl and Rspec
I'm not sure if it is the best solution, but I have successfully achieved this using:
FactoryGirl.define do
factory :user do
first_name "Luiz"
last_name "Branco"
#...
after(:build) { |user| user.class.skip_callback(:create, :after, :run_something) }
factory :user_with_run_something do
after(:create) { |user| user.send(:run_something) }
end
end
end
Running without callback:FactoryGirl.create(:user)
Running with callback:FactoryGirl.create(:user_with_run_something)
How do I force rails to not use a cached result for has_many through relations?
I did some more research into this issue. While using clear_association_cache
was convenient enough, adding it after every operation that invalidated the cache did not feel DRY. I thought Rails should be able to keep track of this. Thankfully, there is a way!
I will use your example models: A (has many B, has many C through B), B (belongs to A, has many C), and C (belongs to B).
We will need to use the touch: true
option for the belongs_to
method. This method updates the updated_at
attribute on the parent model, but more importantly it also triggers an after_touch
callback. This callback allows to us to automatically clear the association cache for any instance of A whenever a related instance of B or C is modified, created, or destroyed.
First modify the belongs_to
method calls for B and C, adding touch:true
class B < ActiveRecord::Base
belongs_to :a, touch: true
has_many :cs
end
class C < ActiveRecord::Base
belongs_to :b, touch: true
end
Then add an after_touch
callback to Aclass A < ActiveRecord::Base
has_many :bs
has_many :cs, through: :bs
after_touch :clear_association_cache
end
Now we can safely hack away, creating all sorts of methods that modify/create/destroy instances of B and C, and the instance of A that they belong to will automatically have its cache up to date without us having to remember to call clear_association_cache
all over the place.Depending on how you use model B, you may want to add an after_touch
callback there as well.
Documentation for belongs_to
options and ActiveRecord callbacks:
http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-belongs_to
Hope this helps!
Rails: How can I set default values in ActiveRecord?
There are several issues with each of the available methods, but I believe that defining an after_initialize
callback is the way to go for the following reasons:
default_scope
will initialize values for new models, but then that will become the scope on which you find the model. If you just want to initialize some numbers to 0 then this is not what you want.- Defining defaults in your migration also works part of the time... As has already been mentioned this will not work when you just call Model.new.
- Overriding
initialize
can work, but don't forget to callsuper
! - Using a plugin like phusion's is getting a bit ridiculous. This is ruby, do we really need a plugin just to initialize some default values?
- Overriding
after_initialize
is deprecated as of Rails 3. When I overrideafter_initialize
in rails 3.0.3 I get the following warning in the console:
Therefore I'd say write anDEPRECATION WARNING: Base#after_initialize has been deprecated, please use Base.after_initialize :method instead. (called from /Users/me/myapp/app/models/my_model:15)
after_initialize
callback, which lets you default attributes in addition to letting you set defaults on associations like so: class Person < ActiveRecord::Base
has_one :address
after_initialize :init
def init
self.number ||= 0.0 #will set the default value only if it's nil
self.address ||= build_address #let's you set a default association
end
end
Now you have just one place to look for initialization of your models. I'm using this method until someone comes up with a better one.Caveats:
For boolean fields do:
self.bool_field = true if self.bool_field.nil?
See Paul Russell's comment on this answer for more details
If you're only selecting a subset of columns for a model (ie; using
select
in a query likePerson.select(:firstname, :lastname).all
) you will get aMissingAttributeError
if yourinit
method accesses a column that hasn't been included in theselect
clause. You can guard against this case like so:self.number ||= 0.0 if self.has_attribute? :number
and for a boolean column...
self.bool_field = true if (self.has_attribute? :bool_value) && self.bool_field.nil?
Also note that the syntax is different prior to Rails 3.2 (see Cliff Darling's comment below)
How do I avoid a race condition in my Rails app?
Optimistic locking is the way to go, but as you might have noticed already, your code will never raise ActiveRecord::StaleObjectError, since child object creation in has_many association skips the locking mechanism. Take a look at the following SQL:
UPDATE `scheduled_runs` SET `lock_version` = COALESCE(`lock_version`, 0) + 1, `attendances_count` = COALESCE(`attendances_count`, 0) + 1 WHERE (`id` = 113338481)
When you update attributes in the parent object, you usually see the following SQL instead:UPDATE `scheduled_runs` SET `updated_at` = '2010-07-23 10:44:19', `lock_version` = 2 WHERE id = 113338481 AND `lock_version` = 1
The above statement shows how optimistic locking is implemented: Notice the lock_version = 1
in WHERE clause. When race condition happens, concurrent processes try to run this exact query, but only the first one succeeds, because the first one atomically updates the lock_version to 2, and subsequent processes will fail to find the record and raise ActiveRecord::StaleObjectError, since the same record doesn't have lock_version = 1
any longer.So, in your case, a possible workaround is to touch the parent right before you create/destroy a child object, like so:
def attend(user)
self.touch # Assuming you have updated_at column
attendance = self.attendances.create(:user_id => user.id)
rescue ActiveRecord::StaleObjectError
#...do something...
end
It's not meant to strictly avoid race conditions, but practically it should work in most cases.
Related Topics
Ruby: Class C Includes Module M; Including Module N in M Does Not Affect C. What Gives
How to Interact with a Caldav Server from Ruby
Undefined Method 'Merge' for '####':String <%= Form_For %> Helper
Why Does Including This Module Not Override a Dynamically-Generated Method
Error While Starting Puma Server with Workers
How to Tell Unicorn to Understand Heroku's Signals
Regex to Check Alphanumeric String in Ruby
How to Write (Large) Files with Ruby Eventmachine
Difference Between "Class A; Class B" and "Class A::B"
Problems While Making a Generic Model in Ruby on Rails 3
Hw Impossibility: "Create a Rock Paper Scissors Program in Ruby Without Using Conditionals"
Accessing Microsoft Exchange Server from Ruby
Creating a Ruby on Rails Environment on Windows, in a Vm Vagrant Box