Rails 4 before_update condition on individual colu

2019-09-17 02:36发布

问题:

tI have a Player model in my rails app. 2 columns I am evaluating are highestLevel and highestScore. This is a stats tracking for a single player across multiple profiles, so there is the possibility that either of these values coming in could be lower than the current value in the db. Therefore I only wish it update a particular column IF the incoming posted value is greater than the one in the DB. Reading up on some of the built in validation options, I was not able to get any to work as I intended, however, I was able to write my own validations which work, but at the cost of calling Player.find(id) within the model. Is there a way around this so that my Player.update() does not result in both and UPDATE and SELECT?

Here is my model:

class Player < ActiveRecord::Base
  #validates_numericality_of :highestLevel, greater_than: Proc.new { |r| r.highestLevel }
  #validates_numericality_of :highestScore, greater_than: Proc.new { |r| r.highestScore }

  before_update :player_record, :eval_highestLevel, :eval_highestScore

  # TODO: Find a more effective way to handle update evaluations with less SQL overhead
  private
    def eval_highestLevel
      # if highestLevel in DB has higher value , update the value
      if @p.highestLevel > self.highestLevel
        self.highestLevel = @p.highestLevel
      end
    end

    def eval_highestScore
      # if record in DB has higher value , update the value
      if @p.highestScore > self.highestScore
        self.highestScore = @p.highestScore
      end
    end

    def player_record
      @p = Player.find(id)
    end
end

Any ideas on how to make this more efficient, or should I leave it alone? I'm always looking for the bigger and better mouse trap for Rails 4.x

回答1:

Rails automatically defines helpers to get the previous value of an attribute when the attribute has changed but the record hasn't been persisted yet. They're named e.g. attribute name_was, so in this case Player#highestLevel_was and highestScore_was:

def eval_highestLevel
  # if highestLevel in DB has higher value , update the value
  if self.highestLevel_was > self.highestLevel
    self.highestLevel = @p.highestLevel
  end
end

This is documented in ActiveModel::Dirty. A number of other useful methods are defined, for example:

  • attribute_name_changed? returns true if the attribute has changed.
  • attribute_name_change returns an array with two elements, the old value and the new value.

Armed with this knowledge we can actually simplify your callbacks a lot:

class Player < ActiveRecord::Base
  before_update :ensure_highestLevel, if: :highestLevel_changed?
  before_update :ensure_highestScore, if: :highestLevel_changed?

  protected
  def ensure_highestLevel
    self.highestLevel = self.highestLevel_change.compact.max
  end

  def ensure_highestScore
    self.highestScore = self.highestScore_change.compact.max
  end
end

Since self.highestScore_change will be an array of two numbers, we can call max to get the higher one. We use Array#compact because if either the old value or the new value is nil we'd get an error ([nil,1].max # => ArgumentError: comparison of NilClass with 1 failed). compact removes any nils from the array first.

Or even more succinctly:

class Player < ActiveRecord::Base
  before_update ->{ highestLevel = highestLevel_change.compact.max },
    if: :highestLevel_changed?

  before_update ->{ highestScore = highestScore_change.compact.max },
    if: :highestScore_changed?
end