Reputation: 21
I have an Item and I want to have a 'Recipe' which is just an Item which references many_of :items.
I have a record in the ingredients table: (id: 1, recipe_id: 1, item_id: 2)
I want to be able to:
item.find(1).items
- return all the items for recipe_id: 1
item.find(1).ingredients
- return the ingredient records where recipe_id: 1
item.find(2).recipes
- return all the recipes for which item_id: 2
I am able to get item.find(1).items working OR item.find(2).recipes working BUT NOT BOTH... :(
MIGRATION
class CreateIngredients < ActiveRecord::Migration[7.0]
def change
create_table :ingredients do |t|
t.references :items, null: false, foreign_key: true
t.references :recipe, references: :items, foreign_key: { to_table: :items }
t.timestamps
end
end
end
INGREDIENTS model
class Ingredient < ApplicationRecord
belongs_to :recipe, class_name: 'Item'
belongs_to :item
end
ITEM model
has_many :ingredients
has_many :items, through: :ingredients
has_many :recipes, through: :ingredients
Upvotes: 1
Views: 101
Reputation: 101831
You haven't actually modeled the assocations correctly. This isn't actually a valid use case for a self-referential association.
What you actually want is to use a join table between recipies and ingredients:
class CreateRecipeIngredients < ActiveRecord::Migration[7.0]
def change
create_table :recipe_ingredients do |t|
t.belongs_to :recipe, null: false, foreign_key: true
t.belongs_to :ingredient, null: false, foreign_key: true
t.decimal :quantity
t.string :unit # better would be to use a separate table or enum
t.timestamps
end
end
end
You can name it whatever you want. Naming it a_bs
is just a lazy convention. This is also where you are going to want to store the quantity of the ingredients.
You then setup a many to many assocation with has_many through:
:
class Recipe < Application
has_many :recipe_ingredients
has_many :ingredients, through: :recipe_ingredients
end
class RecipeIngredient < Application
belongs_to :recipe
belongs_to :ingredient
delegate :name, to: :ingredient
end
class Ingredient < Application
has_many :recipe_ingredients
has_many :recipies, through: :recipe_ingredients
end
The reason you want has_many through:
and not has_and_belongs_to_many
is that the later is highly limited and doesn't let you access additional columns on the join table like the quantity in this case.
You can then get just use the indirect assocations to get recipies for an ingredient or vice versa:
Recipe.find(1).ingredients
Ingredient.find(5).recipies
You can also use joins to get recipies with certain ingredients:
Recipe.joins(:ingredients)
.where(ingredients: { name: 'Broccoli' })
# Recipies with at least one of Broccoli, Kale and Squash
Recipe.joins(:ingredients)
.where(ingredients: { name: ['Broccoli', 'Kale', 'Squash'] })
# Recipies with Broccoli, Kale and Squash
Recipe.left_joins(:ingredients)
.where(ingredients: { name: ['Broccoli', 'Kale', 'Squash'] })
.group(:id)
.having(Ingredient.arel_table[:id].count.gteq(3))
If you want to write out a recipe you want to iterate across the recipe_ingredients
assocation and not ingredients
:
<table>
<thead>
<tr>
<th>Name</th>
<th>Quantity</th>
<th>Unit</th>
</tr>
<thead>
<tbody>
<% recipe.recipe_ingredients.each do |ri| %>
<tr>
<td><%= ri.name %></td>
<td><%= ri.quantity %></td>
<td><%= ri.unit %></td>
</tr>
<% end %>
</tbody>
</table>
If you want to create a self-referential assocation between a recipe and "sub-recipes" you can do it either as one-to-many:
class AddParentToRecipes < ActiveRecord::Migration[7.0]
def change
change_table :recipes do |t|
t.belongs_to :parent, null: true,
foreign_key: { to_table: :recipes }
end
end
end
class Recipe < ApplicationRecord
# ...
belongs_to :parent,
class_name: 'Recipe',
optional: true
has_many :children,
class_name: 'Recipe'
foreign_key: :parent_id
end
Or many to many:
class CreateRecipeComponents < ActiveRecord::Migration[7.0]
def change
create_table :recipe_components do |t|
t.belongs_to :parent, null: false,
foreign_key: { to_table: :recipes }
t.belongs_to :child, null: false,
foreign_key: { to_table: :recipes }
end
end
end
class RecipeComponent < ApplicationRecord
belongs_to :parent, class_name: 'Recipe'
belongs_to :child, class_name: 'Recipe'
end
class Recipe < ApplicationRecord
# ...
has_many :recipe_components_as_parent,
class_name: 'RecipeComponent',
foreign_key: :parent_id
has_many :recipe_components_as_child,
class_name: 'RecipeComponent',
foreign_key: :child_id
has_many :sub_recipies,
through: :recipe_components_as_parent,
source: :child
has_many :parent_recipies,
through: :recipe_components_as_child,
source: :parent
end
While having an "item" that references either a recipe or ingredient might sound like a good idea from an object oriented POV you need to remember that its not how relational databases actually work. In a relational database tables have references to other tables through foreign keys and the table which that foreign key points to is not dynamic.
While you can use a polymorphic assocation to do this remember that its a hack around how databases are actually envisioned to work.
Upvotes: 2