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...
> 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.
Thanks
Martyn