I have 3 very simple models
class Receipt < ActiveRecord::Base
has_many :receipt_items
end
class ReceiptItem < ActiveRecord::Base
after_create :create_transaction
belongs_to :receipt
private
def create_transaction
Transaction.new.save!
end
end
class Transaction < ActiveRecord::Base
validates :transacted_at, :presence => true
end
So every time a new ReceiptItem is created, it triggers the after_create callback to create a new Transaction object using save!. But because Transaction requires that the column transacted_at to be present, Transaction.new.save! should raise an ActiveRecord::RecordInvalid every time, I assumed.
So then I created 3 tests:
test "creating an invalid transaction" do
assert_raises ActiveRecord::RecordInvalid do
Transaction.new.save!
end
end
test "creating invalid transaction in after_create" do
assert_raises ActiveRecord::RecordInvalid do
ReceiptItem.new.save!
end
end
test "creating invalid transaction in after_create of associated model" do
assert_raises ActiveRecord::RecordInvalid do
r = Receipt.new
i = r.receipt_items.new
r.save!
end
end
The first two tests passed as expected. The third test, however, failed because the exception was never raised. As a matter of fact, if I add the following lines after the 'r.save!' line:
r.reload
p r.inspect
p r.receipt_items.inspect
I could see that the Receipt and the ReceiptItem have been created successfully.
Furthermore, if I replaced
assert_raises ActiveRecord::RecordInvalid do
with
assert_difference "Transaction.count", +1 do
I confirmed that the Receipt and the ReceiptItems were created but the Transaction wasn't. That means the creation of the Transaction failed, but was silently ignored, even though I used 'save!' as opposed to just 'save'.
Does anyone know if this is the intended behaviour, or is this actually a bug in Rails?
(Tried this in Rails 4.0.13 and 4.2.0)
I've filed a bug report here: https://github.com/rails/rails/issues/24301
The whole callback chain is wrapped in a transaction. If any before callback method returns exactly false or raises an exception, the execution chain gets halted and a ROLLBACK is issued; after callbacks can only accomplish that by raising an exception.
So although documentation states that raising an exception should at least roll the transaction back (and refuse to save your object), ActiveRecord seems not to discharge its duties.
It turns out that community has already met the similar issue (you should probably check out a github discussion) with rolling back parent transaction during after_save. Some workarounds to this coming to my mind (and mentioned at github thread) include:
Raising something non-active-record-related, like RuntimeError during after_save:
class ReceiptItem < ActiveRecord::Base
def create_transaction
raise RuntimeError unless Transaction.new.save
# NOTE: this error gonna propagate till you rescue it somewhere manually
end
end
Wrapping up your saving into an explicit transaction:
class ReceiptItem < ActiveRecord::Base
belongs_to :receipt
# NOTE: we removed after_save callback here
private
def create_transaction
Transaction.new.save!
end
end
r = Receipt.new
i = r.receipt_items.new
Receipt.transaction do
r.save!
r.receipt_items.each &:create_transaction
# NOTE: whole transaction gonna be rolled back
end
Saving a transaction in a different manner then after_save callback. May be you could pre-check a transaction validness in validation section of receipt_item?
As of my point of view, this behaviour is not intended as it is not documented anywhere explicitly. Rails repo owners seem to not sight a significant attention to the corresponding issue, but it's still worth a try to remind them about it.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With