How do you prevent database changes inside a Rails ActiveRecord before_create filter from getting rolled back when it returns false?

StackOverflow https://stackoverflow.com/questions/1597434

문제

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
도움이 되었습니까?

해결책

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">]

다른 팁

Use a transaction in the filter.

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.

Could those changes be done in an after_create instead?

This uses the same concept as Rich Cavanaugh's answer. I added a parent model so it's clearer what the filter is doing. The key is to use a thread + an automatic check-out/check-in of a separate connection. Note: you should make sure your :pool value is set to at least 2 in your connection specification, depending on how many concurrent threads you'll run. I think it defaults to 5.

class Gadget < ActiveRecord::Base
  has_many :widgets
end

class Widget < ActiveRecord::Base
  belongs_to :gadget
  before_create :update_instead

  def update_some_record_in_same_model
    # the thread forces a new connection to be checked out
    Thread.new do
      ActiveRecord::Base.connection_pool.with_connection do |conn|
        # try this part without the 2 surrounding blocks and it will be rolled back
        gadget.touch_count += 1
        gadget.save!
      end
    end.join
  end
  def some_condition?
    true
  end

  def update_instead
    if some_condition?
      update_some_record_in_same_model # this is getting rolled back
      p [:touch_count_in_filter, gadget.reload.touch_count]
      return false # don't create a new record
    else
      return true # let it continue
    end
  end
end

Test:

  g = Gadget.create(:name => 'g1')
  puts "before:"
  p [:touch_count, g.reload.touch_count]
  p [:widget_count, Widget.count]

  g.widgets.create(:name => 'w1')
  puts "after:"
  # Success means the count stays incremented
  p [:touch_count, g.reload.touch_count]
  p [:widget_count, Widget.count]

Further reading: http://bibwild.wordpress.com/2011/11/14/multi-threading-in-rails-activerecord-3-0-3-1/

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top