best practice for object transaction?

I have a new @offer that needs to generate a document if saved. But that's all or nothing:

    transaction do       @offer.save!       generate_document     end     # handle exceptions if needed

If save! fails everything is fine, but if generate_document raises an exception @offer is left as a model with timestamps, id, which is not a new_record? anymore (though the database was rolled back).

I've played a bit cloning, and with the object transaction plugin, but I am not satisfied with the solutions. Problem is we need to rollback the object except its errors, so to speak.

Do you have any best practices for this?

-- fxn

I'm not 100% sure on this, but I think what you're looking for is ActiveRecord::Observer.

See this example that is used to send an email after saving comments to what I assume is a blogging application:

class CommentObserver < ActiveRecord::Observer     def after_save(comment)       Notifications.deliver_comment("admin@do.com", "New comment was posted", comment)     end   end

Thank you, problem is that the document is not always generated.

By now the best I've got is

     rollback_active_record_state! do        transaction do          save!          generate_document        end      end      # handle exceptions

self preserves the @new_record flag and id housekeeping. It still has created_at even if it was not actually created, but is the better solution I've seen so far.

-- fxn

I guess I'm not seeing the problem.

What do you mean by?

Thank you, problem is that the document is not always generated.

Does this mean that if the document fails to generate then the offer should not save to the database and remain as a new (unsaved) object?

I am assuming that you only want to create the document when creating offers, not updating existing offers. If that's the case, then the observer should implement after_create instead of after_save. If anything goes wrong during create_document and it raises an exception then ActiveRecord will not insert the offer, and you should still have a new Offer object. Then you can present errors back to the user, or whatever error handling you need to do.

I suppose I must be missing something obvious here, but I can't think of what that might be.

Never mind. I think I see the problem now. You need the document to be created when and only when the offer creates successfully, and you also need the offer to roll back if the document creation fails.

Is your create_document method inside of the Offer model class or is it in a different class? The observer allows you to move this rather unrelated code outside of the model object (since creating the document is not a direct responsibility of the Offer model class). Normally doing before_create inside the model class and returning false in that method if something goes wrong will suspend the callback chain, and will leave your table in an unaltered state. However, returning false from the before_create implementation inside the observer does not appear to suspend the callback chain. So this must be why you are using transactions to work around this problem.

Out of curiosity would create_document be creating database records? If that's the case then transactions would likely be necessary anyway.

Never mind. I think I see the problem now. You need the document to be created when and only when the offer creates successfully, and you also need the offer to roll back if the document creation fails.

That's right.

Is your create_document method inside of the Offer model class or is it in a different class? The observer allows you to move this rather unrelated code outside of the model object (since creating the document is not a direct responsibility of the Offer model class).

Well in this case an offer knows how to generate its own document.

Note than an observer as far as this problem is concerned is not much different than a regular AR callback. I mean, the techique that does not work is after_*, no matter whether it is in the model or in an observer.

after_* does not work for what I need because the offer is not aware of whether it needs to save a document or not, so I can't put the conditional in the callback. I think I can revise that, but even if it knew it you can't rollback the INSERT in an after_create, you can abort in before_*s. Well you could raise an exception in after_*s, but that's unexpected behaviour because you do not expect @offer.save to raise an exception like that.

So what I do with rollback_active_record_state! is basically what AR does, plus catching exceptions from document generation. With that code I can implement the transaction I need, have @offer (almost) restored, and return a controlled boolean as usual.

   if @offer.save_and_generate_document      # ...    end

I think leaving timestamps with the new values is a bug. I mean, a new_record? shouldn't have a timestamp in created_at, I can live with that though I'll try to determine if that's a bug and fill a ticket if that's the case.

Thank you for helping Robert.

-- fxn

Here it is: #93 Restore timestamps in model rollback - Ruby on Rails - rails