DEV Community

Cover image for Multiple Foreign Keys for the Same Relationship in Rails 6
Lucas Hiago
Lucas Hiago

Posted on • Updated on

Multiple Foreign Keys for the Same Relationship in Rails 6

It’s quite easy to create tables with Ruby On Rails. However, some cases require personalizations to reproduce real world situations and the relationships between them. Here, we have two models: User and Meeting. They have a 1..N relation, so one User is part of a meeting and a Meeting has two users (yes, specifically two - a two-person meeting). The problem doesn’t rely on this relationship but in the generated migration because we cannot have two t.references :user_id and also we want to identify which user is available for the meeting and which user requested the meeting.

How can I generate a model on Rails?

Before we start with the solution, let’s understand the command rails generate or the shorthand code rails g.
This command is one of the many options that follows the rails command. If you open your terminal and run rails --help, you’ll see a list of options:

$ rails --help
The most common rails commands are:
 generate     Generate new code (short-cut alias: "g")
 console      Start the Rails console (short-cut alias: "c")
 server       Start the Rails server (short-cut alias: "s")
 test         Run tests except system tests (short-cut alias: "t")
 test:system  Run system tests
 dbconsole    Start a console for the database specified in config/database.yml (short-cut alias: "db")
 new          Create a new Rails application. "rails new my_app" creates a new application called MyApp in "./my_app"
Enter fullscreen mode Exit fullscreen mode

And if we run rails generate --help, another list of options will be shown:

$ rails g --help                                
Spring preloader in process 70748
Usage: rails generate GENERATOR [args] [options]
...
Please choose a generator below.
Rails:
  application_record
  assets
  channel
  controller
  generator
  helper
  integration_test
  jbuilder
  job
  mailbox
  mailer
  migration
  model
  resource
  responders_controller
  scaffold
  scaffold_controller
  system_test
  task
...
Enter fullscreen mode Exit fullscreen mode

In this article, we will use the rails db:migrate and rails generate model options to create our models and save them to db/schema.rb, the file that represents our database.

The solution

First, we will create the User and Meeting models.

$ rails generate model User name:string email:string 
$ rails generate model Meeting starts_at:datetime ends_at:datetime available_user:references requester_user:references
Enter fullscreen mode Exit fullscreen mode

This will generate the migration under db/migrate, the model files under app/models, and the test files. The migration for Meeting should look like this:

class CreateMeetings < ActiveRecord::Migration[6.0]
  def change
    create_table :meetings do |t|
      t.datetime :starts_at
      t.datetime :ends_at
      t.references :available_user, null: false, foreign_key: true
      t.references :requester_user, null: false, foreign_key: true

      t.timestamps
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

But if you try to run rails db:migrate to create the tables in the database, an error will be raised on your terminal since there aren't any tables called available_user and requester_user to make reference as Foreign Key. With that in mind, our next step is to remove the foreign_key: true, create the foreign key reference with the correct columns, and then the file should look like this:

class CreateMeetings < ActiveRecord::Migration[6.0]
  def change
    create_table :meetings do |t|
      t.datetime :starts_at, null: false
      t.datetime :ends_at, null: false
      t.references :available_user, null: false # Remove foreign_key: true
      t.references :requester_user, null: false # Remove foreign_key: true

      t.timestamps
    end

    add_foreign_key :meetings, :users, column: :available_user_id
    add_foreign_key :meetings, :users, column: :requester_user_id
  end
end
Enter fullscreen mode Exit fullscreen mode

Now we can safely run rails db:migrate and see the changes on db/schema.rb

ActiveRecord::Schema.define(version: 2020_06_13_202030) do

  create_table "meetings", force: :cascade do |t|
    t.datetime "starts_at"
    t.datetime "ends_at"
    t.integer "available_user_id", null: false
    t.integer "requester_user_id", null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["available_user_id"], name: "index_meetings_on_available_user_id"
    t.index ["requester_user_id"], name: "index_meetings_on_requester_user_id"
  end

  create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "email"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  add_foreign_key "meetings", "users", column: "available_user_id"
  add_foreign_key "meetings", "users", column: "requester_user_id"

end
Enter fullscreen mode Exit fullscreen mode

In order to tell Rails what table will be referenced by the Meeting model, we should make changes to our model files: app/models/meetings.rb and app/models/users.rb. They should look like this:

class Meeting < ApplicationRecord
  belongs_to :available_user, class_name: 'User'
  belongs_to :requester_user, class_name: 'User'
end

class User < ApplicationRecord
  has_many :available_user_meetings, class_name: 'Meeting', foreign_key: 'available_user_id'
  has_many :requester_user_meetings, class_name: 'Meeting', foreign_key: 'requester_user_id'
end
Enter fullscreen mode Exit fullscreen mode

Now the relationship is done and we can create meetings and users in this way:

irb(main):001:0> user1 = User.create!(name: 'John Snow', email: 'night_watcher@thegreatwall.com')
=> #<User id: 1, name: "John Snow", email: "night_watcher@thegreatwall.com", created_at: "2020-06-12 20:34:41", updated_at: "2020-06-12 20:34:41">
irb(main):002:0> user2 = User.create!(name: 'Daenerys Targaryen', email: 'stormborn@dragonstone.com')
=> #<User id: 2, name: "Daenerys Targaryen", email: "stormborn@dragonstone.com", created_at: "2020-06-12 20:36:10", updated_at: "2020-06-12 20:36:10">

irb(main):003:0> starting_time = Time.zone.now
irb(main):004:0> ending_time = Time.zone.now + 2.hour
irb(main):005:0> meeting_about_family = Meeting.create!(starts_at: starting_time, ends_at: ending_time, available_user_id: user2.id, requester_user_id: user1.id)
=> #<Meeting id: 1, starts_at: "2020-06-12 20:40:45", ends_at: "2020-06-12 22:41:04", available_user_id: 2, requester_user_id: 1, created_at: "2020-06-12 20:41:41", updated_at: "2020-06-12 20:41:41">

irb(main):006:0> meeting_about_family.available_user
=> #<User id: 2, name: "Daenerys Targaryen", email: "stormborn@dragonstone.com", created_at: "2020-06-12 20:36:10", updated_at: "2020-06-12 20:36:10">
irb(main):007:0> meeting_about_family.requester_user
=> #<User id: 1, name: "John Snow", email: "night_watcher@thegreatwall.com", created_at: "2020-06-12 20:34:41", updated_at: "2020-06-12 20:34:41">
Enter fullscreen mode Exit fullscreen mode

There's a better way

Instead of writing a two new lines in our migration, in order to replace foreign_key: true, we could simply add foreign_key: { to_table: :users }. The file should look like this:

class CreateMeetings < ActiveRecord::Migration[6.0]
  def change
    create_table :meetings do |t|
      t.datetime :starts_at, null: false
      t.datetime :ends_at, null: false
      t.references :available_user, null: false,  foreign_key: { to_table: :users }
      t.references :requester_user, null: false,  foreign_key: { to_table: :users }

      t.timestamps
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Easier, isn’t it?

Conclusion

I hope this post was useful to you, and remember: always look up the docs!

Top comments (11)

Collapse
 
jelilfaisalabudu profile image
JelilFaisalAbudu

@luchiago , thanks for the wonderful post of yours. You saved me from a big headache. I was wondering whether or not I would be able to find a post on this particular topic for Rails v6. Yeah, I'm now learning the basics, you know? Thanks man

Collapse
 
luchiago profile image
Lucas Hiago

Hey @jelilfaisalabudu , I appreciate your feedback and that I could help you! Stay strong and keep learning every day.

Collapse
 
chase439 profile image
Chase

Could you add "inverse_of" to your examples?

Collapse
 
luchiago profile image
Lucas Hiago

Why it's necessary?

Collapse
 
chase439 profile image
Chase

Rubocop would complain if you don't have it.

Collapse
 
chase439 profile image
Chase

If you have data in these tables (e.g. when changing the tables instead of creating them), you need to make sure the columns have valid values before adding foreign key constraints.

Collapse
 
luchiago profile image
Lucas Hiago

You mean, for the available_user and requester_user variables?

Collapse
 
chase439 profile image
Chase

I mean data records in these tables (not variables).

Collapse
 
sorinaduca profile image
Sorina Duca

Thanks a lot for the great post! I was just building this and it was hard to wrap my mind around it. Your post scratches the exact itch!

Collapse
 
luchiago profile image
Lucas Hiago

Glad to hear this!

Collapse
 
amartinezre05 profile image
Albéniz

@luchiago Thanks! I spent much time today trying to solve this