Multiple Foreign Keys for the Same Relationship in Rails 6
Tags: rails, ruby, database, postgres
Published: 2020-06-14 15:00:41

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:
```shell
$ 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"
```
And if we run `rails generate --help`, another list of options will be shown:
```shell
$ 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
...
```
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.
```shell
$ rails generate model User name:string email:string 
$ rails generate model Meeting starts_at:datetime ends_at:datetime available_user:references requester_user:references
```
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:
```ruby
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
```
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:
```ruby
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
```
Now we can safely run `rails db:migrate` and see the changes on `db/schema.rb`
```ruby
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
```
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:
```ruby
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
```
Now the relationship is done and we can create meetings and users in this way:
```ruby
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">
```

## 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:
```ruby
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
```
Easier, isn’t it?

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