views:

170

answers:

2

I have two models: Person and Address which I'd like to create in a transaction. That is, I want to try to create the Person and, if that succeeds, create the related Address. I would like to use save semantics (return true or false) rather than save! semantics (raise an ActiveRecord::StatementInvalid or not).

This doesn't work because the user.save doesn't trigger a rollback on the transaction:

class Person
  def save_with_address(address_options = {})
    transaction do
      self.save
      address = Address.build(address_options)
      address.person = self
      address.save
    end
  end
end

(Changing the self.save call to an if self.save block around the rest doesn't help, because the Person save still succeeds even when the Address one fails.)

And this doesn't work because it raises the ActiveRecord::StatementInvalid exception out of the transaction block without triggering an ActiveRecord::Rollback:

class Person
  def save_with_address(address_options = {})
    transaction do
      save!
      address = Address.build(address_options)
      address.person = self
      address.save!
    end
  end
end

The Rails documentation specifically warns against catching the ActiveRecord::StatementInvalid inside the transaction block.

I guess my first question is: why isn't this transaction block... transacting on both saves?

+1  A: 

How about this?

class Person
  def save_with_address(address_options = {})
    tx_error = false
    transaction do
      begin
        self.save!
        address = Address.build(address_options)
        address.person = self
        address.save!
      rescue Exception => e
        # add relevant error message to self using errors.add_to_base
        raise ActiveRecord::Rollback 
        tx_error = true 
      end
    end
    return true unless tx_error

    # now roll back the Person TX.
    raise ActiveRecord::Rollback
    return false
  end
end

I don't like the way the TX is implemented. But this is how you can work around the issues.

KandadaBoggu
Fixed the the code to return true/false as required.
KandadaBoggu
Added additional code to rollback Person model changes
KandadaBoggu
I'm not sure what that second `ActiveRecord::Rollback` is doing there outside of the `transaction` block.
James A. Rosen
Also, your `tx_error = true` will never be run because the `raise` will happen first.
James A. Rosen
I will have to rest this out. From what I understand the ActiveRecord:Rollback exception is used by transaction block to roll the tx back and it is not forwarded. So if you want to rollback the Person tx you need the second rollback. I have to confirm this for your case. That's the behavior I have seen in other places.
KandadaBoggu
A: 

Tell ActiveRecord to do this for you. Saves you mounds of problems:

class Person < ActiveRecord::Base
  belongs_to :address, :autosave => true
end

The nice thing is that Person's errors will contain address' validation errors, correctly formatted.

See the AutosaveAssocation module for more information.

François Beausoleil
It's actually `Person.has_many :addresses`, and I only need at least one, but it looks like `autosave` will at least give me the validation on the `Address`. I'll probably need to add some sort of custom error on zero addresses.
James A. Rosen
Also, as far as I can tell, `:autosave => true` does not work on creation, only on update. Specifically, the `Address`'s `user_id` is not getting set (since the `User` does not yet have one) and is thus always invalid.
James A. Rosen