Search code examples
ruby-on-railstransactionsbefore-filter

How can I refactor this complicated transaction into my model from my controller?


I have a complex save process (a cyclical reference that needs to be resolved by validating and saving and one model, validating and saving another and then updating the first with an id from the second) that has rollbacks depending on if something bad happened etc.

My models are related in this way:

  • Accounts have one SiteContact.
  • Accounts have many Customers.
  • SiteContacts are a type of Customer.
  • Customers belong to an Account

Because of the cyclical reference (i.e. Customers are invalid WITHOUT an Account but an Account needs a SiteContact which is a type of Customer.) I decide to save the Account with a blank id for site contact first, then save the site_contact.

I have code similar to this:

# remove the site contact from the account because its not valid yet.
site_contact = params[:account].delete(:site_contact_attributes)
account = Account.new(params[:account])

# some pseudo code
# start database transaction
# validate account
# if valid then
#   save account
#   site_contact.account_id = account.id
#   validate site_contact
#   if valid then
#     account.site_contact_id = site_contact.id
#     save account
#     break out!
#
#   else
#     rollback transaction
#   end
# else
#   rollback transaction
# end

if Account.exists?(account)
  # everything was good! flash[:notice] and redirect somewhere
else
  # something bad happened, go back to the page and display the errors.
end

Is it possible to override the Account#save or some other method and move all of that code into my controller? I was looking at before_save and before_create but I'm worried there might be some unintended recursion since I have to save the account twice.

Any ideas?


Solution

  • In your controller:

    site_contact = SiteContact.new(params[:account].delete(:site_contact_attributes))
    account = Account.new(params[:account])
    
    if account.save_nested(site_contact)
     # success
    else
     # error
    end
    

    In your Account model.

    class Account < ActiveRecord::Base
      def save_nested(site_contact)
        Account.transaction do
          save!
          site_contact.account_id = self.id
          site_contact.save!
          self.site_contact_id = site_contact.id
          save!
          return true
        end
        return false
      end
    end
    

    The save! will throw an exception when the validation fails. This will automatically roll back the transaction.