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"
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
...
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
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
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
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
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
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">
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
Easier, isn’t it?
Conclusion
I hope this post was useful to you, and remember: always look up the docs!