This is so common I’m amazed that it isn’t right in the docs.

From one-to-many to many-to-many relationship

Let’s say you wanted Users to belong to a company, but then your customer tells you, “Oh yeah we have these people who work at more than one location.”

The models have some Ruby code like this:

class User < ApplicationRecord

belongs_to :company

end



class Company < ApplicationRecord

has_many :users

end

It turns out to be very straight forward to refactor.

Generate a migration with the goals of creating a new join table for companies and users, migrate the existing company_id data from the Users table, and then update the Rails model accordingly.

class AddCompanyUsers < ActiveRecord::Migration[5.2]

def change

create_join_table :companies, :users do |t|

t.index %i[company_id user_id], unique: true

end

# migrate users to new table

up_only do

User.all.each do |u|

unless u.company_id.nil?

u.update(companies: [Company.find(u.company_id)])

end

end

# drop company_id from users

remove_column :users, :company_id

end

end

end

One of the important flags is unique: true because this clears any duplication in the join table for either side of the tables. This way a user can belong to many companies, but only once per company.

Update the models to support many-to-many relationship between them:

class User < ApplicationRecord

has_and_belongs_to_many :companies

end



class Company < ApplicationRecord

has_and_belongs_to_many :users

end

Last step is to run the migration:

rails db:migrate

That will take care of the foundation, but now the controllers need to handle the new information. Previously we would just permit a company_id param and then set the user’s company directly in their row. But now we have an array of company id’s (or transverse an array of user id’s) to accept in our params.

class UserController < ApplicationController

def update

# woop no changes probably

end



def user_params

# How it was

params.require(:user).permit(:name, company_ids: [])

end

end

Now just set the company_id’s in your UI and send them in with your other params. Rails will handle the rest.

From many-to-many to one-to-many

Unless there is some huge performance hit happening from the join table, which is unlikely, leave it a many-to-many. This kind of change likely requires throwing out a choice the customers wanted, and they would know which one is the one to keep.

This is the reverse of the above, but we’ll want to keep the old join table around for a while, and the migration just picks the first value in the array to default the new column value.

class AddCompanyIdToUsers < ActiveRecord::Migration[5.2]

def change

add_column :company_id, :users, optional: true



# migrate users to new table

up_only do

User.all.each do |u|

u.update(company: user.companies.first)

end

end

end

end

Then your models would be updated as such:

class User < ApplicationRecord

belongs_to :company, optional: true

end



class Company < ApplicationRecord

has_many :users

end

Optional is important here because for a while the app has two sources of truth for membership, but we’re accepting that the first value we found is probably the right one. The UI would need to stop updating via company_ids and start accepting that user.company_id is the right value. That’s where all the real refactoring will happen.

Want me to cover more migration paths? Have questions about Rails? Drop a note below and I’ll update the article.

Cheers!