Techioz Blog

Rails: enumよりもソート/比較可能なリストを取得する慣用的な方法?

概要

私は初心者っぽい Rails 開発者で、いくつかの列挙型を使用して本質的に順序付けされた属性のリストを作成するプロジェクトがあります。たとえば、次のようになります。

enum :priority, { 低: 10、中: 20、高: 30、非常に高: 40、}

ここで、MyClass.priorities.sort を呼び出すと、ラベルが関連付けられた整数に基づいてではなく、アルファベット順に並べ替えられます。宣言内で順序を作成するだけでよく、ドロップダウンの選択などでも順序が保持されるため、これは大したことではありません。しかし、ビジネス ロジックで大なり小なりの比較 (および >= と <=) を行うために定期的に列挙型を整数に変換しているため、コードが冗長になり、読みにくくなっていることがわかりました。例えば

priority_int = MyClass.priorities[priority]
results = MyClass.where('priority >= ?', priority_int)

私が欠落している本質的に順序付けられた固定属性のリストを取得する、より慣用的な方法はありますか?理想的には、比較/並べ替えが組み込まれているものです。これは一般的なことのように思えますが、私はここ数日間WebとSOを調べてきましたが、明確なオプションが見つかりませんでした。簡潔でドライで読みやすいコードでこのニーズに対処する最善の方法は何でしょうか?

どうもありがとう!

解決策

おそらくスコープがあなたが探しているものです。好きなものを選んでください:

class Model < ApplicationRecord
  enum :priority, {low: 10, moderate: 20, high: 30, very_high: 40}

  ##
  # Add scopes, aka class methods. Append `.order(:priority)` if you like
  #
  #   Model.min_priority(:high).pluck(:priority)
  #   #=> ["high", "high", "very_high"]
  #
  scope :min_priority, ->(name) { where("priority >= ?", priorities[name]) }
  scope :max_priority, ->(name) { where("priority <= ?", priorities[name]) }


  ##
  # Add a shortcut method
  #
  #   Model.first.rank > Model.last.rank
  #   #=> false
  #
  def rank
    priority_before_type_cast
  end


  ##
  # Define the methods you need
  #
  #   Model.first.gt(Model.last)
  #   #=> false
  #
  def rank = priority_before_type_cast
  def gt(other)   = (rank >  other.rank)
  def gteq(other) = (rank >= other.rank)
  def lt(other)   = (rank <  other.rank)
  def lteq(other) = (rank <= other.rank)


  ##
  # You could include Comparable into the model, which will give you:
  # <, <=, >, >=, ==, between?, clamp
  # However, `==` method will cause different models with the same priority
  # to be equal each other. Other than that, you can compare your models:
  #
  #   Model.first <= Model.last
  #   #=> true
  #
  alias_method :eq_copy, :==   # copy the == method
  include Comparable
  alias_method :==, :eq_copy   # put it back
  remove_method :eq_copy       # get rid of the witness
  # this defines how your models compare to each other, aka spaceship operator
  def <=> other
    priority_before_type_cast <=> other.priority_before_type_cast
  end
end

Comparable をモデルに含めたくない場合もありますが、並べ替えるときに以下を比較するため、<=> を定義するだけでも役立ちます。

>> Model.all.pluck(:priority)
=> ["low", "moderate", "high", "very_high", "low", "low"]

>> Model.all.sort.pluck(:priority)
=> ["low", "low", "low", "moderate", "high", "very_high"]

しかし、sort_by では、余分なものを追加することなく、より速くその問題を解決できます。 (該当する場合は代わりに order(:priority) を使用してください):

>> Model.all.sort_by(&:priority_before_type_cast) # or sort_by(&:rank)
=>
[#<Model:0x00007f99f1380b98 id: 1, priority: "low">,
 #<Model:0x00007f99f06ad178 id: 5, priority: "low">,
 #<Model:0x00007f99f06ad0d8 id: 6, priority: "low">,
 #<Model:0x00007f99f06ad358 id: 2, priority: "moderate">,
 #<Model:0x00007f99f06ad2b8 id: 3, priority: "high">,
 #<Model:0x00007f99f06ad218 id: 4, priority: "very_high">]

また、任意の順序を取得するためにできること:

Model.in_order_of(:priority, [:low, :moderate, :high, :very_high])