views:

303

answers:

3

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.

  1. @ab.auction.bid_count needs to be incremented by one.
  2. @ab.user.bid_count needs to be incremented by one
  3. @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.

+2  A: 

You could probably override AuctionBid.save, something like this:

def save
  AuctionBid.transaction {
    auction.bid_count += 1
    user.bid_count += 1
    auction.current_winner_id = user_id
    auction.save!
    user.save!
    return super
  }
end

You'll probably also need to catch exceptions raised in the transaction block and return false. I think you also need to add belongs_to :auction to AuctionBid to be able to reference the auction object.

Alex - Aotea Studios
I fixed my code to show `AuctionBid` `belongs_to :auction`. If I call `auction.save!` or `user.save!` in the transaction block and `user.save!` fails, will the `auction.save!` still persist?
macek
The transaction is aborted if an exception is raised. save! should raise exceptions.
Alex - Aotea Studios
+1  A: 

You want to enable counter caching by adding :counter_cache to belongs_to associations.

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, :counter_cache => true
  belongs_to :user, :counter_cache => true
end

Remember to add the columns through a migration. To create the auction bid and set the user, I'd suggest using the following code:

class MyController
  def bid
    @ab = current_user.auction_bids.build(params[:auction_bid])
    if @ab.save
      render :json => {:response => 'YAY!'}
    else
      render :json => {:response => 'FAIL!'}
    end
  end 
end

Saves a step, and makes sure you can never forget to assign the user.

Last requirement is to find the current winner. This is actually a has_one association on the Auction. You don't need a column for it:

class Auction
  # has_one is essentially has_many with an enforced :limit => 1 added
  has_one :winning_bid, :class_name => "AuctionBid", :order => "bid_amount DESC"
end
François Beausoleil
These are useful tips but I made the mistake of making my example a little to simple. What if other parts of the associated models needed to be changed? How would the pattern change?
macek
Same answer: before/after save callbacks will be used to trigger saves on other models, all wrapped in the same transaction.
François Beausoleil
A: 

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.

macek