sajad torkamani

Let's say we're building an app that lists a bunch of jokes and allows user to upload their own. Now, imagine we have a new request to allow users to save a joke that they particularly liked so they can quickly look that up later.

Let's assume our app has the following models:

class User < ApplicationRecord
  has_many :jokes
end

class Joke < ApplicationRecord
  belongs_to :user
end

When designing models and database schemas, I find it helpful to think about how I want the interaction between the models to work and then let that inform my schema design.

In this case, I'd want to be able to do three things.

1. Allow a user to save a joke

current_user.saved_jokes << @joke

2. Retrieve a user's saved jokes

@jokes = current_user.saved_jokes

3. Get list of users who have saved a joke

@users = @joke.saved_by # should return list of User records

Having considered these requirements, it's clear that we need a many-to-many relationship between the User and Joke models. A user can have many saved jokes and a joke can be saved by many users.

Create a join table to model many-to-many relationships

To setup this many-to-many relationship, we need to first create a join table in the database (assuming a SQL database for this example).

Let's generate a migration to create a jokes_saves join table.

rails g migration CreateJokesSaves user:references joke:references

The corresponding migration should be as follows:

class CreateJokesSaves < ActiveRecord::Migration[6.0]
  def change
    create_table :jokes_saves, id: false do |t|
      t.references :user, null: false, foreign_key: true
      t.references :joke, null: false, foreign_key: true

      t.timestamps
    end

    add_index :jokes_saves, [:user_id, :joke_id], unique: true
  end
end

Make sure to pass id: false to create_table to tell Rails not to create a id column. Apparently having IDs on join tables can end up causing very subtle issues with ActiveRecord!

Also, make sure to add a unique index on the combined user_id and joke_id columns to prevent users from favouriting a joke more than once.

Setup has_and_belongs_to_many association between users and jokes

Now, all that is left is to configure the has_and_belongs_to_many associations.

class User < ApplicationRecord
  has_many :jokes
  has_and_belongs_to_many :saved_jokes, join_table: 'jokes_saves', class_name: 'Joke'
end

class Joke < ApplicationRecord
  belongs_to :user
  has_and_belongs_to_many :saved_by, join_table: 'jokes_saves', class_name: 'User'
end

You can call the associations anything you want, just make sure the join_table and class_name arguments are set correctly.

Use the associations

Now, it's time to bask in the magic of Rails. Open up the rails console by running rails console and play around.

user = User.first
joke = Joke.first

# Save joke
user.saved_jokes << joke

# Fetch saved jokes
user.saved_jokes # [joke]

# Fetch list of users who saved a joke
joke.saved_by # [user]
Tagged: Rails