How do you prevent database changes inside a Rails ActiveRecord before_create filter from getting rolled back when it returns false?
-
22-09-2019 - |
문제
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:
- before validation (on_create)
- validate
- after validation (on_create)
- before_create_actions_that_wont_be_rolled_back
- before validation (on_create)
- validate
- after validation (on_create)
- before save callbacks
- before create callbacks
- record is created
- after create callbacks
- 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/