2
votes

I'm learning my way around Rails and am working on a sample app to keep track of beer recipes.

I have a model called Recipe which holds the recipe name and efficiency.

I have a model called Ingredient which is using STI - this is subclassed into Malt, Hop, and Yeast.

Finally, to link the recipes and ingredients, I am using a join table called rec_items which holds the recipe_id, ingredient_id, and info particular to that recipe/ingredient combo, such as amount and boil time.

Everything seems to be working well - I can find all my malts by using Malt.all, and all ingredients by using Ingredient.all. I can find a recipe's ingredients using @recipe.ingredients, etc...

However, I'm working on my recipe's show view now, and am confused as to the best way to accomplish the below:

I want to display the recipe name and associated info, and then list the ingredients, but separated by ingredient type. So, if I have a Black IPA @ 85% efficiency and it has 5 malts and 3 hops varieties, the output would be similar to:

BLACK IPA (85%)
Ingredient List
MALTS:
malt 1
malt 2
...
HOPS:
hop 1
...

Now, I can pull @recipe.rec_items and iterate through them, testing each rec_item.ingredient for type == "Malt", then do the same for the hops, but that doesn't seem very Rails-y nor efficient. So what is the best way to do this? I can use @recipe.ingredients.all to pull all the ingredients, but can't use @recipe.malts.all or @recipe.hops.all to pull just those types.

Is there a different syntax I should be using? Should I using @recipe.ingredient.find_by_type("Malt")? Doing this in the controller and passing the collection to the view, or doing it right in the view? Do I need to specify the has_many relationship in my Hop and Malt models as well?

I can get it working the way I want using conditional statements or find_by_type, but my emphasis is on doing this "the Rails way" with as little DB overhead as possible.

Thanks for the help!

Current bare-bones code:

Recipe.rb

class Recipe < ActiveRecord::Base
  has_many :rec_items
  has_many :ingredients, :through => :rec_items
end

Ingredient.rb

class Ingredient < ActiveRecord::Base
  has_many :rec_items
  has_many :recipes, :through => :rec_items
end

Malt.rb

class Malt < Ingredient
end

Hop.rb

class Hop < Ingredient
end

RecItem.rb

class RecItem < ActiveRecord::Base
  belongs_to :recipe
  belongs_to :ingredient
end

recipes_controller.rb

class RecipesController < ApplicationController
  def show
    @recipe = Recipe.find(params[:id])
  end

  def index
    @recipes = Recipe.all
  end
end

Updated to add

I'm now unable to access the join table attributes, so I posted a new question:

Rails - using group_by and has_many :through and trying to access join table attributes

If anyone can help with that, I'd appreciate it!!

3

3 Answers

3
votes

It's been a while since I've used STI, having been burned a time or two. So I may be skipping over some STI-fu that would make this easier. That said...

There are many ways of doing this. First, you could make a scope for each of malt, hops, and yeast.

class Ingredient < ActiveRecord::Base
  has_many :rec_items
  has_many :recipes, :through => :rec_items
  named_scope :malt, :conditions =>  {:type => 'Malt'}
  named_scope :hops, :conditions => {:type => 'Hops'}
  ...
end

This will allow you to do something line:

malts = @recipe.ingredients.malt
hops = @recipe.ingedients.hops

While this is convenient, it isn't the most efficient thing to do, database-wise. We'd have to do three queries to get all three types.

So if we're not talking a ton of ingredients per recipe, it'll probably be better to just pull in all @recipe.ingredients, then group them with something like:

ingredients = @recipe.ingredients.group_by(&:type)

This will perform one query and then group them into a hash in ruby memory. The hash will be keyed off of type and look something like:

{"Malt" => [first_malt, second_malt],
 "Hops" => [first_hops],
 "Yeast" => [etc]
}

You can then refer to that collection to display the items however you wish.

ingredients["Malt"].each {|malt| malt.foo }
3
votes

You can use group_by here.

recipe.ingredients.group_by {|i| i.type}.each do |type, ingredients|
  puts type

  ingredients.each do |ingredient|
    puts ingredient.inspect
  end
end
0
votes

The utility of STI in this instance is dubious. You might be better off with a straight-forward categorization:

class Ingredient < ActiveRecord::Base
  belongs_to :ingredient_type

  has_many :rec_items
  has_many :recipes, :through => :rec_items
end

The IngredientType defines your various types and ends up being a numerical constant from that point forward.

When you're trying to display a list this becomes easier. I usually prefer to pull out the intermediate records directly, then join out as required:

RecItem.sort('recipe_id, ingredient_type_id').includes(:recipe, :ingredient).all

Something like that gives you the flexibility to sort and group as required. You can adjust the sort conditions to get the right ordering. This might also work with STI if you sort on the type column.