Reputation: 77778
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
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
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.
Upvotes: 12
Views: 7334
Reputation: 77778
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.
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
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.
Upvotes: 12
Reputation: 16515
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
Upvotes: 1
Reputation: 15126
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.
Upvotes: 4