If you do things the Rails Way, including using the standard table names that come with the migrations generated using rails generate migration
, you barely need to think about the underlying database. If you are interfacing with an existing database, or your models and the corresponding tables have different names, it can be a little more tricky. Luckily, ActiveRecord lets us specify the table, keys, and so on in these situations.
Let’s look at a standard has_many
and belongs_to
relationship, using the Rails way:
rails generate model investor
Gives the following model and migration:
class Investor < ApplicationRecord
end
class CreateInvestors < ActiveRecord::Migration[5.1]
def change
create_table :investors do |t|
t.timestamps
end
end
end
And the corresponding database table:
________________________________
| id | created_at | updated_at |
--------------------------------
We now want a house
that belongs_to
an investor
. Of course, an investor
has_many
houses
. We can do this in one go:
rails generate model house investor:references
Which gives us:
class House < ApplicationRecord
belongs_to :investor
end
class CreateHouses < ActiveRecord::Migration[5.1]
def change
create_table :houses do |t|
t.references :investor, foreign_key: true
t.timestamps
end
end
end
The corresponding table looks like this:
______________________________________________
| id | investor_id | created_at | updated_at |
----------------------------------------------
The Rails Way knows if we have a belongs_to
, it will use the corresponding model’s name and append _id
to create the foreign key.
There is one part we have to do by hand, however. If we drop down into rails console
and run:
Investor.new.houses
We get
NoMethodError: undefined method `houses' for #<Investor id: nil, created_at: nil, updated_at: nil>
That’s because we need to add has_many :houses
to the investor
model:
class Investor < ApplicationRecord
has_many :houses
end
Now it works:
i = Investor.new.save
i = Investor.first # load newly created investor
i.first.houses.create
#=> #<House id: 2, investor_id: 1, created_at: "2018-05-01 07:31:06", updated_at: "2018-05-01 07:31:06">
However, what if the model had been called something other than investor
, or the database table had a different name? Let’s build the able, using differently named models and tables, to see how to handle it.
Instead of generating the migration along with model, we will start with just the migration. The tables will be called existing_investors
and existing_houses
.
rails g migration create_existing_investor
bin/rails db:migrate
Now the model:
touch app/models/my_investor.ruby
class MyInvestor < ApplicationRecord
end
Dropping into a rails console
and running MyInvestor.new
yields ActiveRecord::StatementInvalid: Could not find table 'my_investors'
.
By default, Rails looks for a table which is the pluralized version of the model. We can specify another table name using self.table_name
:
class MyInvestor < ApplicationRecord
self.table_name = "existing_investors"
end
The same thing can be applied for the initial my_house
model and existing_houses
migration:
rails g migration create_existing_house
class CreateExistingHouse < ActiveRecord::Migration[5.1]
def change
create_table :existing_houses do |t|
t.timestamps
end
end
end
# app/models/my_house.ruby
class MyHouse < ApplicationRecord
end
Let’s add the has_many
relationship first. Instead of houses, I want to call them investments
.
class MyInvestor < ApplicationRecord
self.table_name = "existing_investors"
has_many :investments
end
rails console
gives us:
MyInvestor.first.investments
#=> NameError: uninitialized constant MyInvestor::Investment
Obviously, we don’t even have an investments
model. Try using class_name
:
has_many :investments, class_name: 'MyHouse'
And we get a new error:
MyHouse Load (0.3ms) SELECT "existing_houses".* FROM "existing_houses" WHERE "existing_houses"."my_investor_id" = ? LIMIT ? [["my_investor_id", 1], ["LIMIT", 11]]
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: existing_houses.my_investor_id: SELECT "existing_houses".* FROM "existing_houses" WHERE "existing_houses"."my_investor_id" = ? LIMIT ?
no such column: existing_houses.my_investor_id is the important part. The logic goes like this:
Investor
has_many investments
. They are a model called MyHouse
. So calling investor.investments
looks to the MyHouse
model, which in turn has a class_name: 'existing_houses'
- whcih leads to a database query for existing_houses
with an id
prefixed by the model’s table name, in this case my_investor
- my_investor_id
.
We need to do three things:
existing_houses
column. Since the table is called existing_investors
, I want it to be called existing_investor_id
.
belongs_to
to MyHouse
foreign_key
to the has_many
function in MyInvestor
For the first step, we generate new migration:
class AddInvestorKeyToMyHouse < ActiveRecord::Migration[5.1]
def change
change_table :existing_houses do |t|
t.references :existing_investor, foreign_key: true
end
end
end
t.references :existing_investor
creates a key of the same name with _id
appended - so existing_investor_id
.
For the second step, we can update my_house.ruby
:
class MyHouse < ApplicationRecord
self.table_name = "existing_houses"
belongs_to :buyer, class_name: "MyInvestor"
end
For the third, we update MyHouse
as such:
class MyHouse < ApplicationRecord
self.table_name = "existing_houses"
belongs_to :existing_investor, class_name: "MyInvestor"
end
Great! Now we can do:
MyInvestor.first.investments.create!
#=> #<MyHouse id: 3, created_at: "2018-05-01 08:17:48", updated_at: "2018-05-01 08:17:48", existing_investor_id: 1>
Another nice thing Rails lets us to is customize the belongs_to
, which can make the relationships between models more intuitive. Let’s change belongs_to :existing_investor
to belongs_to :buyer
:
class MyHouse < ApplicationRecord
self.table_name = "existing_houses"
belongs_to :buyer, class_name: "MyInvestor", foreign_key: "existing_investor_id"
end
And MyInvestor.first.investments.create!
still works. Now we can type MyHouse.last.buyer
, which is much more intuitive, and get:
#=> #<MyInvestor id: 1, created_at: "2018-05-01 07:39:21", updated_at: "2018-05-01 07:39:21">
RoR is not that trendy nowdays, but seeing the fantastic abstractions and power ActiveRecord deliverers out of the box, I still think RoR is the best way to build websites.