Many-to-Many Association in Rails
To setup a basic many-to-many associations, we need to start by declaring the needed models withing the rails console. We need three models for this purpose. Two tables with a regular ID field and one table with its ID field along with the ID fields of each of the other tables. This table serves the purpose of a joining table (a.ka. Join Table or Pivot Table). The many-to-many association is really
Just two one-to-many associations with a joining table in the middle as demonstrated in the table diagram below.
From the rails console create the needed models:
rails g model Product
rails g mdoel Satisfaction
rails g model Buyer
This will create three model and migration files. Open each of the migration files and added the needed fields so they can be created with a rails db:migrate
command.
Product Migration
class CreateProducts < ActiveRecord::Migration[5.0]
def change
create_table :products do |t|
t.string :name
t.date :manufacture_date
t.timestamps
end
end
end
Satisfaction Migration
class CreateSatisfactions < ActiveRecord::Migration[5.0]
def change
create_table :satisfactions do |t|
t.integer :rank
t.references :product, foreign_key: true
t.references :buyer, foreign_key: true
t.timestamps
end
end
end
Buyer Migration
class CreateBuyers < ActiveRecord::Migration[5.0]
def change
create_table :buyers do |t|
t.string :name
t.string :age
t.date :buy_date
t.timestamps
end
end
end
This will setup the following tables
product | ||||
---|---|---|---|---|
id | name | manufacture_date | created_at | updated_at |
The product_id
is a foreign key pointing to the 'products' table and the buyer_id
is a foreign key pointing to the 'buyers' table.
satisfactions | |||||
---|---|---|---|---|---|
id | rank | product_id | buyer_id | created_at | updated_at |
buyers | |||||
---|---|---|---|---|---|
id | name | age | buy_date | created_at | updated_at |
At this point, we need to setup the necessary associations within the model files. This can be accomplished by added the belongs_to
and has_many
lines listed int the model files below.
Product Model:
class Product < ApplicationRecord
has_many :satisfactions
end
Satisfaction Model:
class Satisfaction < ApplicationRecord
belongs_to :product
belongs_to :buyer
end
Buyer Model:
class Buyer < ApplicationRecord
# The 'dependent: :destroy' says to destroy the satisfaction
# ranking for that buyer when the buyer is removed:
has_many :satisfactions, dependent: :destroy
end
If we open up the rails console with rails c
, then we have the following relationships available to us:
prod = Product.create(name: "Dell Inspiron")
buy = Buyer.create(name: "Joe", buy_date: "2017-07-04")
sat = Satisfaction.new
sat.rank = 4
sat.product = prod
sat.buyer = buy
sat.save
# Then we can also print the product's satisfaction rankings:
prod.satisfactions
# Also, we can print the buyers satisfaction rankings:
buy.satisfactions
With this relationship setup we can create new satisfaction ranking like this:
prod = Product.first
buy = Buyer.find_by(name: "Joe")
sat = prod.satisfactions.new(rank: 2)
sat.buyer = buy
sat.save
In addition, we could also setup the buyer and product for the new satisfaction ranking in one fell swoop:
prod = Product.first
buy = Buyer.first
prod.satisfactions.create!(rank: 5, buyer: buy)
Using these tables, we can find the buyers for a product like this:
# Find the product:
prod = Product.first
# Find satisfaction rankings attached to the product we just queried:
# 'sat' now holds an array list of satisfaction rank objects
sat = prod.satisfactions
# Now we can use a loop to get the 'buyer' associated with each satisfaction like this:
# This results in 1+n queries:
sat.each { |s| s.buyer.name }
# This will end up running some queries like this:
# SELECT "buyers".* FROM "buyers" WHERE "buyers"."id" = ? LIMIT ?
# SELECT "buyers".* FROM "buyers" WHERE "buyers"."id" = ? LIMIT ?
# SELECT "buyers".* FROM "buyers" WHERE "buyers"."id" = ? LIMIT ?
# SELECT "buyers".* FROM "buyers" WHERE "buyers"."id" = ? LIMIT ?
# SELECT "buyers".* FROM "buyers" WHERE "buyers"."id" = ? LIMIT ?
Instead of having to get a list of the satisfaction rankings assigned to a particular product, and then loop through each one to find the user associated with each one (1+n queries), we can instead use a through association with rails. This will drastically reduce the number of queries that are necessary.
To setup a through association for our current example, we can add this to the 'Product' model:
Product Model:
class Product < ApplicationRecord
has_many :satisfactions
# Add this line:
has_many :buyers, through: :satisfactions
end
# This will allow this query:
p = Product.first
p.buyers
# Resultant query:
# SELECT "buyers".* FROM "buyers" INNER JOIN "satisfactions" ON "buyers"."id" = "satisfactions"."buyer_id" WHERE "satisfactions"."product_id" = ?
To setup the reverse of this relationship, we can add the line below to the 'Buyer' model:
Buyer Model:
class Buyer < ApplicationRecord
has_many :satisfactions, dependent: :destroy
has_many :products, through: :satisfactions
end
# This will allow this query:
buy = Buyer.first
buy.products
We can even abbreviate the name of our through association relationship by adding a 'source' declaration and changing our 'Product' model as listed below:
Product Model:
class Product < ApplicationRecord
has_many :satisfactions
# Add this line:
has_many :purchasers, through: :satisfactions, source: :buyer
end
# Now we can do this:
p = Product.first
p.purchasers
# SQL:
# SELECT "buyers".* FROM "buyers" INNER JOIN "satisfactions" ON "buyers"."id" = "satisfactions"."buyer_id" WHERE "satisfactions"."product_id" = ?
We can do the same thing in the reverse as well, by modifying the 'Buyer' model file:
class Buyer < ApplicationRecord
has_many :satisfactions, dependent: :destroy
has_many :things, through: :satisfactions, source: :product
# This allows for:
b = Buyer.first
b.things
# SQL:
# SELECT "products".* FROM "products" INNER JOIN "satisfactions" ON "products"."id" = "satisfactions"."product_id" WHERE "satisfactions"."buyer_id" = ?
end
I have included the code for this mvc application in this repo: many-to-many-rails.