Let’s say we’re building a jokes application that allows users to share a bunch of jokes. Now, let’s say we have a new requirement 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 or database schemas, I find it helpful to think about how I want the interaction between the models to work and then let the requirements drive my 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
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
joke_id columns to prevent users from favouriting a joke more than once.
has_and_belongs_to_many association between users and jokes
Now, all that is left is to configure the
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
class_name arguments are set correctly.
Use the associations
Now, it’s time to bask in the magic of Rails.
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]