So here's my code :
class Recipe < ActiveRecord::Base
has_many :neededingredients
end
class Neededingredient < ActiveRecord::Base
belongs_to :ingredients
belongs_to :recipes
end
class Ingredient < ActiveRecord::Base
has_many :neededingredients
end
And here's my migration :
create_table :recipes do |recipes|
recipes.string :name, null: false
recipes.string :indications, null: false
end
create_table :ingredients do |ingredients|
ingredients.string :ingredient, null: false, uniqueness: true
end
create_table :neededingredients do |neededingredients|
neededingredients.integer :quantity, null: false
neededingredients.string :unit
neededingredients.references :ingredients, foreign_key: true
neededingredients.references :recipes, foreign_key: true
end
So a Recipe has multiple NeededIngredients. A NeededIngredient has a reference to an Ingredient, and has a quantity and a unit (grams, for example). I'd like to be able to do something like a_recipe.neededingredients to find all neededingredients of a given recipe, or a_needed_ingredient.recipeto get the recipe of a given neededingredient. For some reason, I can't find a way to do this. Is there anything wrong in my migration or in my model ? Thanks for the help !
The key of the issue here is really bad naming / pluralization. I would highly really recommend that you rename this according to the Ruby conventions to avoid confusion and future bugs.
Class names should be CamelCase - NeededIngredient. Do seperate compound words!
Attributes, table names, instance variables, routes, method names and everything else should be snake_case - needed_ingredient or needed_ingredients depending on the context.
This is much easier to read and get right then insanelylongthingwitteninlowercase.
Cramming three different table definitions into a single migration (if that is what you're doing) is a bad idea. Especially since its trivial to generate migrations though the model generator. Having separate migrations lets you run them sequentially and roll them back individually to get it right.
class CreateNeededIngredients < ActiveRecord::Migration[6.0]
def change
# there is no need to use super long block argument names
create_table :needed_ingredients do |t|
t.decimal :quantity, null: false # yeah decimal
t.string :unit
t.references :ingredient, foreign_key: true # this should be singular!
t.references :recipe, foreign_key: true # this should be singular!
t.timestamps
end
# consider adding a unique index to avoid duplicates
# add_index: :needed_ingredients, [:ingredient_id, :recipe_id], unique: true
end
end
Not that the argument to references should be singular since its the name of the foreign key. Rails will deduce the target table by inflection.
You then need to setup a has_many through: association to tie recipe and ingredients together:
class Recipe < ActiveRecord::Base
has_many :needed_ingredients
has_many :ingredients, through: :needed_ingredients
end
class NeededIngredient
belongs_to :recipe # belongs_to is always singular!
belongs_to :ingredient # belongs_to is always singular!
end
class Ingredient < ActiveRecord::Base
has_many :needed_ingredients
has_many :recipies, through: :needed_ingredients
end
And this is not a one-to-many assocation. Its many-to-many. If you need to display the ingredients of recipe you should note that you should loop through the needed_ingredients association and not ingredients.
class NeededIngredient
belongs_to :recipe # belongs_to is always singular!
belongs_to :ingredient # belongs_to is always singular!
# use delegation/proxing to avoid Law of Demeter violations
def name
ingredient.ingredient # why isn't this column just called name?
end
end
class RecipiesController < ApplicationController
def show
#recipe = Recipe.include(:ingedients).find(params[:id])
end
end
# app/views/recipies/show.html.erb
<h1><%= #recipe.name %></h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Quantity</th>
</tr>
</thead>
<tbody>
<% #recipe.needed_ingredients.each do |i|%>
<tr>
<td><%= i.name %></td>
<td><%= i.quantity %> <%= i.unit %></td>
</tr>
<% end %>
</tbody>
</table>
You use plural instead of singular (in the migration, and in the model), and you should use has_many :through association
Change the following:
class Recipe < ApplicationRecord
has_many :neededingredients
has_many :ingredients, through: :neededingredients
end
class Neededingredient < ApplicationRecord
belongs_to :ingredient
belongs_to :recipe
end
class Ingredient < ApplicationRecord
has_many :neededingredients
has_many :recipes, through: :neededingredients
end
create_table :neededingredients do |neededingredients|
neededingredients.integer :quantity, null: false
neededingredients.string :unit
neededingredients.references :ingredient, foreign_key: true
neededingredients.references :recipe, foreign_key: true
end
Now you can call (for example): Recipe.first.ingredients, Recipe.first.neededingredients, Ingredient.first.recipes
Related
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
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>
Addendum
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.
I have this migration and model for order and order_detail with cocoon gem.
class CreateOrders < ActiveRecord::Migration[5.0]
def change
create_table :orders do |t|
t.integer :total_price
t.timestamps
end
end
end
class CreateOrderDetails < ActiveRecord::Migration[5.0]
def change
create_table :order_details do |t|
t.integer :subtotal_price
t.integer :unit_price
t.integer :quantity
t.references :order, foreign_key: true
t.timestamps
end
end
end
class Order < ApplicationRecord
has_many :order_details, inverse_of: :order, dependent: :destroy
before_validation :calculate_order_price
accepts_nested_attributes_for :order_details, :reject_if => :all_blank, :allow_destroy => true
def calculate_order_price
order_details.each(&:calculate_order_detail_price)
self.total_price = order_details.map(&:subtotal_price).sum
end
end
class OrderDetail < ApplicationRecord
belongs_to :order
def calculate_order_detail_price
self.subtotal_price = unit_price * quantity
end
end
When I save the record after to add or edit the nested field, it works well. But if I edit to delate nested field, calculate_order_price doesn't work.
If somebody knows about this, please advise me.
There is an option :touch which will make sure upon update the parent sets the updated_at (or other fields) but it will not run the validations. There is however also an option :validate (but not entirely sure it will be called upon destroy):
belongs_to :order, validate: true
Otherwise, if those not work, you could do something like
class OrderDetail < ApplicationRecord
belongs_to :order
after_destroy :trigger_parent_validation
def trigger_parent_validation
if order.present?
order.validate
end
end
end
Say I have this:
class Picture < ApplicationRecord
belongs_to :imageable, polymorphic: true
end
class Employee < ApplicationRecord
has_many :pictures, as: :imageable
end
class Product < ApplicationRecord
has_many :pictures, as: :imageable
end
Does this require me to define a table exactly in the following way?
class CreatePictures < ActiveRecord::Migration[5.0]
def change
create_table :pictures do |t|
t.string :name
t.integer :imageable_id
t.string :imageable_type
t.timestamps
end
add_index :pictures, [:imageable_type, :imageable_id]
end
end
Or may I define a bit differently, with different columns or types, for example, that is, in a way I see more efficient? Will the polymorphic association remain functioning?
The polymorphic association only relates to the _type and _id pair of columns. Everything else is up to you.
So yes, you can add additional metadata if you like.
Title isn't explicit, but I didn't know how to explain my problem in few words.
I've a Sale model with this fields:
create_table "sales", force: true do |t|
t.string "title"
...
t.integer "seller_id"
t.integer "buyer_id"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "category_id"
...
end
In a view, I'm able to do that with Category:
<td><%= link_to sale.category.label, category_path(sale.category) %></td>
Cause I think Rails convention know category_id is related to an Category object
So, I want to do exactly the same for seller_id and buyer_id which are both User.
Unfortunally, I fall on error when I try:
<td><%= sale.seller.first_name %></td>
output:
undefined method `seller' for Sale
There, how my Models are linked:
User:
class User < ActiveRecord::Base
has_many :offers
has_many :sales
end
Sale:
class Sale < ActiveRecord::Base
belongs_to :category
belongs_to :user, foreign_key: "seller_id"
belongs_to :user, foreign_key: "buyer_id"
EDIT:
Yeah, it's make more sense. I had misunderstood the documentation about that.
But I've still an error:
undefined method `first_name' for nil:NilClass
I think it's cause Rails didn't find the User... But I've a good value in seller_id...
EDIT 2:
Still not working with:
User model:
class User < ActiveRecord::Base
has_many :offers
has_many :sales, foreign_key: :seller_id
has_many :sales, foreign_key: :buyer_id
end
Sale model:
class Sale < ActiveRecord::Base
belongs_to :category
belongs_to :seller, class_name: :User
belongs_to :buyer, class_name: :User
end
Same error on :
<td><%= sale.seller.first_name %></td>
output
undefined method `first_name' for nil:NilClass
You need to tweak your associations in your Sale model.
This should work
Class Sale < ActiveRecord::Base
belongs_to :category
belongs_to :seller, class_name: "User",foreign_key: "seller_id",
belongs_to :buyer, class_name: "User",foreign_key: "buyer_id"
end
I believe the models should seem as follows:
class User < ActiveRecord::Base
has_many :sales, foreign_key: :seller_id
has_many :buys, foreign_key: :buyer_id
end
class Sale < ActiveRecord::Base
belongs_to :seller, class_name: :User
belongs_to :buyer, class_name: :User
end
So when you use belongs_to you should just specify class_name, but in class that contains has_many related to the specific belongs_to you should explicitly denote the name of field in the belongs_to class.
I got the following use-case:
I got three types of Users: Advertisers, Publishers and Administrators. Each user has some common properties (like name or surname) but also several unique associations. The advertiser has an association with Ad(verttisement)s and Campaigns. Each of with is another model of its own.
My question is how would I go about and model that using ActiveRecords? What would the migration code look like?
Here are the model classes:
User:
class User < ActiveRecord :: Base
require 'pbkdf2'
require 'date'
has_many :messages
attribute :name, :surname, :email, :password_hash, :password_salt
attr_accessor :password, :password_confirmation, :type
attribute :user_since, :default => lambda{ Date.today.to_s }
[...]
end
Publisher:
class Publisher < User
has_many :websites
end
Advertiser:
class Advertiser < User
has_many :campaigns
has_many :ads
end
I got the following migration file to create the User:
class AddUser < ActiveRecord::Migration
def up
create_table :users do |t|
t.string :name
t.string :surname
t.string :email
t.string :password_hash
t.string :password_salt
t.date :user_since
t.string :type
end
create_table :messages do |t|
t.belongs_to :user
t.string :account_number
t.timestamps
end
end
def down
drop_table :user
end
end
How do I modify this file in order to incorporate the aforementioned associations?
Edit: Corrected the associations to use plural form.
Polymorphic relationships is one way to solve this, while another way would be to use Single Table Inheritance (STI). Each approach has its benefits and drawbacks, and your decision would probably depend in how different the subclasses of User would tend to be. The more drastically they would differ, the more the decision would tend toward polymorphic relationships.
Using STI approach:
# a single :users table
# one table for each of the other (non-user) models
class User < ActiveRecord::Base
has_many :messages
end
class Publisher < User
has_many :websites
end
class Advertiser < User
# if :campaign supports multiple user-types (polymorphic)
has_many :campaigns, :as => :user
# otherwise
has_many :campaigns
has_many :ads
end
class Message < ActiveRecord::Base
belongs_to :user
end
class Campaign < ActiveRecord::Base
# if multiple user-types will have campaigns
belongs_to :user # referential column should be :user_id
# otherwise
belongs_to :advertiser # referential column should be :advertiser_id
end
Using Polymorphic approach:
# there should be no :users table, as User will be an abstract model class
# instead make a table for each of all the other models
class User < ActiveRecord::Base
self.abstract_class = true
has_many :messages, :as => :messageable
end
class Publisher < User
has_many :websites
end
class Advertiser < User
has_many :campaigns
has_many :ads
end
class Message < ActiveRecord::Base
belongs_to :messageable, polymorphic: true # referential columns should be :messageable_id and :messageable_type
end
class Campaign < ActiveRecord::Base
# if multiple user-types will have campaigns
belongs_to :user, polymorphic: true # referential columns should be :user_id and :user_type
# otherwise
belongs_to :advertiser # referential column should be :advertiser_id
end