Rails transaction: save data in multiple models

activerecord, ruby-on-rails

Solution

Save and destroy are automatically wrapped in a transaction

ActiveRecord::Transactions::ClassMethods

Both Base#save and Base#destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction depends on or you can raise exceptions in the callbacks to rollback, including after_* callbacks.

The real convention!

class AuctionBid < ActiveRecord::Base
  
  belongs_to :auction, :counter_cache => true
  belongs_to :user
  
  validate              :auction_bidable?
  validate              :user_can_bid?
  validates_presence_of :auction_id
  validates_presence_of :user_id

  # the real magic!
  after_save  :update_auction, :update_user
  
  def auction_bidable?
    errors.add_to_base("You cannot bid on this auction!") unless auction.bidable?
  end
  
  def user_can_bid?
    errors.add_to_base("You cannot bid on this auction!") unless user.can_bid?
  end
  
  protected
  
  def update_auction
    auction.place_bid(user)
    auction.save!
  end
  
  def update_user
    user.place_bid
    user.save!
  end
  
end

honorable mention

François Beausoleil +1. Thanks for the `:foreign_key` recommendation, but the `current_winner_*` columns need to be cached in the db in order to optimize the query.

Alex +1. Thanks for getting me started with `Model.transaction { ... }`. While this didn't end up being a complete solution for me, it definitely help point me in the right direction.

Problem

my models ``` class Auction belongs_to :item belongs_to :user, :foreign_key => :current_winner_id has_many :auction_bids end class User has_many :auction_bids end class AuctionBid belongs_to :auction belongs_to :user end ``` current usage An auction is displayed on the page, the user enters an amount and clicks bid. Controller code might look something like this: ``` class MyController def bid @ab = AuctionBid.new(params[:auction_bid]) @ab.user = current_user if @ab.save render :json => {:response => 'YAY!'} else render :json => {:response => 'FAIL!'} end end end ``` desired functionality This works great so far! However, I need to ensure a couple other things happen. - `@ab.auction.bid_count` needs to be incremented by one. - `@ab.user.bid_count` needs to be incremented by one - `@ab.auction.current_winner_id` needs to be set to `@ab.user_id` That is, the `User` and the `Auction` associated with the `AuctionBid` need values updated as well in order for the `AuctionBid#save` to return true.

Original source