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(“[email protected]”, “New comment was
posted”, comment)
end
end

On May 1, 2008, at 2:00 , Robert W. wrote:

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.

On Thu, May 1, 2008 at 11:58 AM, Xavier N. [email protected] wrote:

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.

Here it is:
http://rails.lighthouseapp.com/projects/8994/tickets/93-restore-timestamps-in-model-rollback#ticket-93-1

On May 1, 2008, at 3:41 , Robert W. wrote:

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