Rails self referencing association - ruby-on-rails

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.

Related

Rails : Access other object in a one-to-many association

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

Self referential association in Rails

I am trying to figure out how to do a self referencing association in Rails. I'm a Rails beginner.
Basically, I have a model Group. Each Group can have many sub-groups. I feel like I've tried everything, but I can't get the join to work.
What I have now is
# GroupSubGroup Model
class GroupSubGroup < ApplicationRecord
belongs_to :group
belongs_to :sub_group, class_name: 'Group'
end
and then my Group Model looks like
has_many :group_sub_groups
has_many :sub_groups, foreign_key: :sub_group_id, through: :group_sub_groups, class_name: 'GroupSubGroup'
has_many :groups, through: :sub_groups
has_many :groups, class_name: 'GroupSubGroup'
And my migration looks like
create_table :group_sub_groups do |t|
t.integer :group_id, index: true, foreign_key: { to_table: :groups }
t.references :sub_group, index: true, foreign_key: { to_table: :groups }
t.timestamps
end
My main issue is that I can add a new GroupSubGroup row into the join table using parent_group.sub_groups.new, however when I retrieve the parent group and loop over it's sub_groups, none of the instances are of the Group class and therefore don't have any of the methods.
For example
Group.all.each do |group|
group.sub_groups.each do |s|
puts "#{s.name} is a sub group for #{group.name}"
end
end
Throws an undefined method 'name' error.
Its actually a lot simpler then this. You don't need a separate model/table for subgroups. Which is the whole of the point of a self referential association.
Lets just start out with the groups table and add our self-refential foreign key:
class CreateGroups < ActiveRecord::Migration[6.0]
def change
create_table :groups do |t|
t.references :parent, index: true, foreign_key: { to_table: :groups }
t.string :name
t.timestamps
end
end
end
Then lets create a one-to-many association to the same table:
class Group < ApplicationRecord
belongs_to :parent,
class_name: 'Group',
inverse_of: :sub_groups
has_many :sub_groups,
class_name: 'Group',
foreign_key: 'parent_id',
inverse_of: :parent
scope :top_level, ->{ where(parent_id: nil) }
end
You can then iterate through the top-level groups and their subgroups with:
# eager_load prevents an n+1 query
Group.top_level.eager_load(:sub_groups).each do |group|
group.sub_groups.each do |s|
puts "#{s.name} is a sub group for #{group.name}"
end
end
I think it would be how you set up the associations
has_many :group_sub_groups
# The following should suffice for sub groups. This should return all
# sub_groups that the group has
has_many :sub_groups, through: :group_sub_groups, source: :sub_group
# The following should suffice for groups. This should return all groups
# where the group is a sub_group of.
has_many :groups, through: :group_sub_groups, source: :group
For the following code
Group.all.each do |group|
group.sub_groups.each do |s|
puts "#{s.name} is a sub group for #{group.name}"
end
end
The error is most probably because of s.name. In your original implementation, sub_groups has a class of class_name: 'GroupSubGroup' which is not what you want. Using the associations I mentioned above should fix that error.

Rails Many to Many with Extra Column

So I have these tables:
create_table :users do |t|
t.string :username
t.string :email
t.string :password_digest
t.timestamps
end
create_table :rooms do |t|
t.string :name
t.string :password
t.integer :size
t.integer :current_size
t.timestamps
end
create_table :rooms_users do |t|
t.belongs_to :user, index: true
t.belongs_to :room, index: true
t.boolean :is_admin
t.timestamps
end
I made it so, when I call Room.find(1).users I get a list of all the users in the room. However, I also want to be able to call something like Room.find(1).admins and get a list of users that are admins (where is_admin in rooms_users is true). How would I do that?
Thank you for your time!
You want to use has_many through: instead of has_and_belongs_to_many. Both define many to many associations but has_many through: uses a model for the join rows.
The lack of a model makes has_and_belongs_to_many very limited. You cannot query the join table directly or add additional columns since the rows are created indirectly.
class User < ApplicationRecord
has_many :user_rooms
has_many :rooms, through: :user_rooms
end
class Room < ApplicationRecord
has_many :user_rooms
has_many :users, through: :user_rooms
end
class UserRoom < ApplicationRecord
belongs_to :user
belongs_to :room
end
You can use your existing schema but you need to rename the table users_rooms to user_rooms with a migration - otherwise rails will deride the class name as Rooms::User.
class RenameUsersRooms < ActiveRecord::Migration[5.0]
def change
rename_table(:users_rooms, :user_rooms)
end
end
However, I also want to be able to call something like
Room.find(1).admins and get a list of users that are admins (where
is_admin in rooms_users is true). How would I do that?
You want to use a left inner join:
User.joins(:user_rooms)
.where(user_rooms: { room_id: 1, is_admin: true })
To roll that into the class you can setup an association with a scope applied:
class Room < ApplicationRecord
has_many :user_rooms
has_many :users, through: :user_rooms
has_many :user_room_admins, class_name: 'UserRoom', ->{ where(is_admin: true) }
has_many :user_room_admins, through: :user_rooms,
class_name: 'User',
source: :user
end
You can define a proc in the has_many relation to set SQL clauses, like ORDER or WHERE:
# room.rb
has_many :rooms_users, class_name: 'RoomsUser'
has_many :users, through: :rooms_users
has_many :admins,
proc { where(rooms_users: { is_admin: true }) },
through: :rooms_users,
class_name: 'User',
source: :users
# user.rb
has_many :administrated_rooms,
proc { where(rooms_users: { is_admin: true }) },
through: :rooms_users,
class_name: 'Room',
source: :rooms
You can simplify this with a simple scope defined in the RoomsUser model, something like:
# rooms_user.rb
scope :as_admins, -> { where(is_admin: true) }
And use it in the proc:
# user.rb
has_many :administrated_rooms,
proc { as_admins },
through: :rooms_users,
class_name: 'Room',
source: :rooms
source option explained:
With source: :users, we're telling Rails to use an association called :users on the RoomsUser model (as that's the model used for :rooms_users).
(from Understanding :source option of has_one/has_many through of Rails)

Use custom id "foreign key name" for use it like object

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.

Rails has_and_belongs_to_many relations with two types of Users and one type of Table

I have a problem related with this association. A pasted code is better than any title:
table.rb
class Table < ActiveRecord::Base
has_and_belongs_to_many :clients, class_name: 'User'
has_and_belongs_to_many :managers, class_name: 'User'
end
user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :tables
end
migration - join table
class UsersToTable < ActiveRecord::Migration
def change
create_table :tables_users, id: false do |t|
t.references :user, as: :client
t.references :user, as: :manager
t.references :table
end
end
end
Problem
tab = Table.new
tab.save
tab.clients.create
tab.clients.create
tab.clients.create
tab.managers.create
tab.managers.size # == 4
tab.clients.size # == 4
When I creating associated Objects(Users) they all are linked to both clients and managers.
I want to be able to create them separately - When creating a client - only number of clients rise, when creating manager, only number of managers rise.
In other words I want this:
tab.managers.size # == 1
tab.clients.size # == 3
Could you please help?
has_and_belongs_to_many :stuff, class_name: 'StuffClass' is just DSL for:
has_many "<inferred_join_table_name>"
has_many :stuff, through: "<inferred_join_table_name>"
It seems that since clients and managers are names for Users, the inferred join table get's to be "TablesUsers", and that is not right.
Try specifyng the join table for both and using different join tables for each relationship:
class Table
has_many :tables_clients
has_many :clients, through: :tables_clients
has_many :tables_managers
has_many :clients, through: :tables_managers
end
class TablesClients
belongs_to :client, class_name: 'User'
belongs_to :table
end
create_table :tables_clients, id: false do |t|
t.references :client, index: true
t.references :table, index: true
end
# and the same for tables_managers
Then the user belongs to Tables in too different ways:
class User
has_many :client_tables_users, class_name: 'TablesUsers', foreign_key: :client_id
has_many :tables_as_client, through: :client_tables_users, source: :table
has_many :managed_tables_users, class_name: 'TablesUsers', foreign_key: :manager_id
has_many :managed_tables, through: :managed_tables_users, source: :table
end

Resources