views:

560

answers:

4

I've added a before_create filter to one of my Rails ActiveRecord models and inside that filter I'm doing some database updates.

Sometimes I return false from the filter to prevent the creation of the target model, but that is causing all the other database changes I made (while inside the filter) to get rolled back.

How can I prevent that?

Update #1: Here's some pseudo code explaining my problem:

class Widget < ActiveRecord::Base
  before_create :update_instead

  def update_instead
    if some_condition?
      update_some_record_in_same_model # this is getting rolled back
      return false # don't create a new record
    else
      return true # let it continue
    end
  end
end

Update #2: Some good answers below but each had its shortcomings. I ended up overridding the create method like so:

def create
  super unless update_instead? # yes I reversed the return values from above
end
+2  A: 

Use a transaction in the filter.

James Deville
Thanks. Could you be a little more explicit? Perhaps some sample/pseudo code?
Teflon Ted
This doesn't work. The wrapped transaction gets rolled back with the wrapping transaction. From the documentation: "Most databases don’t support true nested transactions. At the time of writing, the only database that we’re aware of that supports true nested transactions, is MS-SQL. Because of this, Active Record emulates nested transactions by using savepoints."
Teflon Ted
A: 

Could those changes be done in an after_create instead?

JRL
No I have to prevent the create in some cases.
Teflon Ted
+1  A: 

I just had to do this recently. You need to specifically request another connection from AR. Then execute your changes on that connection. This way, if the creation fails and rolls back the transaction, your callback's changes were already committed in a different transaction.

Ignore my answer above. The example code you just gave really clarified things.

class Foo < ActiveRecord::Base
  before_create :update_instead

  def update_instead
    dbconn = self.class.connection_pool.checkout
    dbconn.transaction do
      dbconn.execute("update foos set name = 'updated'")
    end
    self.class.connection_pool.checkin(dbconn)
    false
  end
end


>> Foo.create(:name => 'sam')
=> #<Foo id: nil, name: "sam", created_at: nil, updated_at: nil>
>> Foo.all
=> [#<Foo id: 2, name: "updated", created_at: "2009-10-21 15:12:55", updated_at: "2009-10-21 15:12:55">]
Rich Cavanaugh
"You need to specifically request another connection from AR" - Fantastic; how do I do that? Thanks.
Teflon Ted
heh, that might have helped. I went back and checked and unfortunately what I was doing may be slightly different. The changes I needed to persist were in a different model. So I simply told the other model to use it's own connection.So, lets say the main transaction was on model Foo and in a before_create it did Bar.create. Even if the work on Foo failed I wanted the Bar to stick around. So I used establish_connection in the body of Bar like: class Bar < ActiveRecord::Base establish_connection end
Rich Cavanaugh
Yeah that's not going to work for me. I'm working with a single database here, and as you can see it my comments to the other response, nested transactions aren't working either. This is not looking good.
Teflon Ted
I'm working with a single database as well. My solution simply uses a separate connection, which brings with it a completely different transaction, for a specific model.
Rich Cavanaugh
Here's a pastie that demonstrates what I'm very bad at putting into words. http://pastie.textmate.org/private/9qc8ckyqvyuc8s8fbnqnng
Rich Cavanaugh
Thanks but that's not working for me. Please see my update to the question with the example code.
Teflon Ted
+1  A: 

Have you tried overwriting create/save and their destructive versions? ActiveRecord::Base.create, ActiveRecord::Base.save and their destructive versions are wrapped in a transaction, they're also what trigger callbacks and validations. If you're overriding it, only the stuff done by super will be part of a transaction. If you need yo run validations before then you can explicitly call valid to run them all.

Example:

before_create :before_create_actions_that_can_be_rolled_back

def create
  if valid? && before_create_actions_that_wont_be_rolled_back
    super
  end
end

def before_create_actions_that_wont_be_rolled_back
 # exactly what it sounds like
end

def before_create_actions_that_can_be_rolled_back
 # exactly what it sounds like
end

Caveat: With these modifications the methods will be called in this order:

  1. before validation (on_create)
  2. validate
  3. after validation (on_create)
  4. before_create_actions_that_wont_be_rolled_back
  5. before validation (on_create)
  6. validate
  7. after validation (on_create)
  8. before save callbacks
  9. before create callbacks
  10. record is created
  11. after create callbacks
  12. after save callbacks

If any validations fail or if any callback returns false in steps 5-12 the database will be rolled back to the state it was in before step 5.

If valid? fails, or before_create_actions_that_wont_be_rolled_back fails than the whole chain will be halted.

EmFi
That's clever and might make a great Plan B but I really need to apply the updates at the before_create step (#6 in your list) after validation. Thanks.
Teflon Ted
There's no reason you can't call valid? explicitly. That will ensure all validations pass before doing your actions that should not be rolled back. Of course it means validations will be run twice. Regardless the solution has been updated to address your concern.
EmFi