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: http://rails.lighthouseapp.com/projects/8994/tickets/93-restore-timestamps-in-model-rollback#ticket-93-1