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.
精彩评论