Skipping: Touch Associations When Saving an Activerecord Object

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 A

class 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:

  1. 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.
  2. 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.
  3. Overriding initialize can work, but don't forget to call super!
  4. 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?
  5. Overriding after_initialize is deprecated as of Rails 3. When I override after_initialize in rails 3.0.3 I get the following warning in the console:

DEPRECATION WARNING: Base#after_initialize has been deprecated, please use Base.after_initialize :method instead. (called from /Users/me/myapp/app/models/my_model:15)

Therefore I'd say write an 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:

  1. For boolean fields do:

    self.bool_field = true if self.bool_field.nil?

    See Paul Russell's comment on this answer for more details

  2. If you're only selecting a subset of columns for a model (ie; using select in a query like Person.select(:firstname, :lastname).all) you will get a MissingAttributeError if your init method accesses a column that hasn't been included in the select 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



Leave a reply



Submit