So update, thanks to Jan for pointing this out.

The after_* callbacks are actually executed inside a transaction, and the COMMIT is sent after the transaction is complete. Rails callbacks guide states:

The entire callback chain of a|save|,|save!|, or|destroy|call runs within a transaction. That includes|after_*|hooks. If everything goes fine a COMMIT is executed once the chain has been completed.

If a|before_*|callback cancels the action a ROLLBACK is issued. You can also trigger a ROLLBACK raising an exception in any of the callbacks, including|after_*|hooks. Note, however, that in that case the client needs to be aware of it because an ordinary|save|will raise such exception instead of quietly returning|false|.

This still means we must use after_commit if we expect to execute the callback after the record is persisted. But the behaviour is slightly different:

1. after_create event triggered
2. user is looked up and name set to "foo"
3. user is saved
4. transaction commits or does a roll back

http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html

Cheers for pointing this out Jan, this is much clearer.

Regards
Martyn

On 03/18/2013 04:32 PM, Martyn Taylor wrote:
All,

I recently ran into a bit of a trap with ActiveRecord when using callbacks and a rails console. It's not totally obvious how callbacks work, so I thought I'd write it up so others don't fall into the same issues.

It's easiest to explain this with an example: lets say we have two classes a user model and a model observer:

class User < ActiveRecord::Base
  # has string field called name.
end

class UserObserver < ActiveRecord::Observer
  def after_create user
    u = User.find(user.id)
    u.name = "foo"
    u.save
  end
end

We'll start the rails server and then open up console to issue some commands. Now let's create a new user:

u = User.create

What would you expect to happen in this case?

If you think that:

1. User is persisted
2. after_create event triggered
3. user is found and name set to "foo"
4. user is saved

Then you thought the same as me and would be right... at least some of the time.

What actually happens is the following:

1. User persistence is triggered and a database transaction initialized

Split a, b:

2a. after_create event triggered
3a. user is looked up and name set to "foo"
3a. user is saved

2b. transaction commits or does a roll back

In most cases the transaction will commit or roll back pretty much straight away and we'll see our original expected behaviour.

However, in some cases the callback will execute before the transaction is complete. In our example this would result in a RecordNotFound exception. Since the the database entry does not exist when our observer tries to do the lookup for the user record.

Now, a point to note here: I've not actually tested this using a single rails session (i.e. having just the server or just the console running). I am assuming (though I do need to find out) that there is some cache at or around the persistence layer which stores our original user object, and returns it when we do User.find(:id). This would happen without touching the db and therefore we'll not see a RecordNotFound error).

However, we should at least be aware of the actual behaviour above, since it may well be that other sessions are used for rake tasks or general administration.

So, how to do we deal with the transactions issues.

Well as of rails 3.x ActiveRecord now have 2 extra callbacks events :after_commit and :after_rollback. These are used to ensure that the callback is fired after our transaction has completed (and our record actually exists or is updated in the database). These events can also be scoped to the standard callbacks, so they fire only on :create, :update etc...

after_commit  :do_foo<
/span>,:on  =>  :create
after_commit  :do_bar,:on  =>  :update
after_commit  :do_baz,:on  =>  :destroy

However, there isn't any obvious way (at least from what I gather) to scope these events in the observer. So, I created a generic after_commit callback, and do the check for :create/:update myself. This is less than ideal, I'm hoping there is (or will be soon) a better method for doing this. Updated observer example:

class UserObserver < ActiveRecord::Observer
  def after_commit user
    # check to see if this is a create event
    if user.created_at == updated_at
      u = User.find(user.id)
      u.name = "foo"
      u.save
    end
  end
end

So, it's worth bearing in mind this behaviour when using callbacks and you won't do what I originally did and start cursing sqlite :p. Anyways... Hope you found this useful and if any of you have a better idea on scoping the commit callbacks in observers, please let me know, it will be useful.

For more info checkout: http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html

Regards
Martyn

Reply via email to