开发者

Retrieve has_many association as hash by property of child

Is it possible to retrieve the child elements in a has_many association into a hash based on one of their properties?

For example, imagine a menu that has a singl开发者_Go百科e dish for each day:

class Menu
  has_many :dishes
end

and

class Dish
  belongs_to :menu
end

Dish has a key, day, which is one of either monday, tuesday etc. Is there some way to set up the has_many association such that Menu.dishes returns a hash similar to {:monday => 'spaghetti', :tuesday => 'tofu', ... }?


Sure. Something like this should suffice (assuming e.g. "spaghetti" is stored in a column called food).

class Dish
  belongs_to :menu

  scope :by_day { select [ :day, :food ] }

  def self.by_day_hash
    by_day.all.reduce({}) {|hsh,dish| hsh[dish.day] = dish.food; hsh }
  end
end

class Menu
  has_many :dishes

  def dishes_by_day
    dishes.by_day_hash
  end
end

# Usage
m = Menu.where( ... )
m.dishes_by_day #=> { "monday" => "Spaghetti", "tuesday" => "Tofu" }

So what's happening here is that in Dish the by_day scope returns only two columns, day and food. It still, however, returns Dish objects rather than a Hash (because that's what scopes do), so we define a class method, by_day_hash which takes that result and turns it into a Hash.

Then in Menu we define dishes_by_day which just calls the method we made above on the association. You could just call this dishes but I think it's better to keep that name for the original association since you might want to use it for other things later on.

Incidentally (optional stuff below, skip for now if your eyes have glazed over), I might define by_day_hash like this instead:

class Dish
  belongs_to :menu

  scope :by_day { select [ :day, :hash ] }

  def to_s
    food
  end

  def by_day_hash
    hsh = HashWithIndifferentAccess.new
    by_day.reduce(hsh) {|hsh, dish| hsh[dish.day] = dish }
  end
end

# Usage
m = Menu.where( ... )
m.dishes_by_day #=> { "monday" => #<Dish food: "Spaghetti", ...>, "tuesday" => #<Dish food: "Tofu", ...>, ... }

...This way you still get the full Dish object when you call e.g. by_day_hash["monday"] but the to_s method means you can just drop it into a view like <%= @menu.dishes_by_day["monday"] %> and get "Spaghetti" instead of #<Dish day: "monday", food: "Spaghetti">.

Finally, you might also notice I used HashWithIndifferentAccess.new instead of {} (Hash). HashWithIndifferentAccess is a class provided (and used everywhere) by Rails that is identical to Hash but lets you do either e.g. some_hash["monday"] or some_hash[:monday] and get the same result. Totally optional but very handy.


Here's a nice way to do it in modern ActiveRecords and Rubys:

# In class Menu
def foods_by_day
  Hash[ dishes.pluck(:day, :food) ]
end

dishes.pluck(:day, :food) returns an array like [ ['monday', 'spaghetti'], ['tuesday', 'tofu], ... ]; Hash[] converts that array-of-arrays into a hash.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜