Rails-транзакция: сохранение данных в нескольких моделях
мои модели
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
текущее использование
Аукцион отображается на странице, пользователь вводит сумму и нажимает ставку. Код контроллера может выглядеть примерно так:
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
желаемая функциональность
Это отлично работает! Тем не менее, мне нужно обеспечить пару других вещей.
-
@ab.auction.bid_count
нужно увеличить на единицу.
-
@ab.user.bid_count
нужно увеличить на
-
@ab.auction.current_winner_id
необходимо установить на @ab.user_id
То есть значения User
и Auction
, связанные с значениями AuctionBid
, также должны обновляться, чтобы AuctionBid#save
возвращал true.
Ответы
Ответ 1
Сохранение и уничтожение автоматически завертываются в транзакцию
Оба База # save и База # destroy приходят завернутый в transaction, который гарантирует, что все, что вы делаете при проверке или обратном вызове, произойдет под защитой обложки транзакции. Таким образом, вы можете использовать проверки для проверки значений, от которых зависит транзакция, или вы можете создавать исключения в обратных вызовах для отката, включая after_ * обратные вызовы.
Настоящее соглашение!
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
почетное упоминание
Франсуа Босолейл +1. Спасибо за рекомендацию :foreign_key
, но столбцы current_winner_*
необходимо кэшировать в db, чтобы оптимизировать запрос.
Алекс +1. Спасибо, что начал с Model.transaction { ... }
. Хотя это не оказалось для меня полным решением, это определенно поможет мне в правильном направлении.
Ответ 2
Вероятно, вы можете переопределить AuctionBid.save, что-то вроде этого:
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
Вероятно, вам также придется поймать исключения, поднятые в блоке транзакций, и вернуть false. Я думаю, вам также нужно добавить belongs_to :auction
в AuctionBid
, чтобы иметь возможность ссылаться на объект аукциона.
Ответ 3
Вы хотите включить кэширование счетчиков, добавив: counter_cache to association_to association.
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
Не забудьте добавить столбцы через миграцию. Чтобы создать аукционную ставку и установить пользователя, я бы предложил использовать следующий код:
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
Сохраняет шаг и гарантирует, что вы никогда не сможете забыть назначить пользователя.
Последнее требование - найти текущего победителя. Это фактически ассоциация has_one на Аукционе. Для этого вам не нужен столбец:
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