On 2013-03-18 4: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_f!
oo<
/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
Probably goes without saying that one should minimize the amount
of trips to the database triggered by callbacks. In the example
above the #find call would be superfluous (I do realise this is
just an example!), since callback already has access to the object
in question (and its fields will be up to date). Personally, I
find callbacks useful for simple stuff, but not worth the trouble
in more complicated cases. I think using a "unit of work"-type
pattern for more involved operations on objects/object-graphs
might be a better approach.
Cheers,
-d
Sure. I can imagine unit of work type patterns would be ideal in
many cases and certainly should be considered. In our case, we are
acting upon a 3rd party service updating our models via REST
callback, we felt observers would be most appropriate. I think in
all cases, it's worth noting that AR save and create operations
don't block on transaction, and any simple callbacks may be
performed before database transactions are complete.