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

Сохранение и уничтожение автоматически завертываются в транзакцию

ActiveRecord:: Transactions:: ClassMethods

Оба База # 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