Let’s start with a simple example:
class Company < ActiveRecord::Base
has_many :users
end
class User < ActiveRecord::Base
belongs_to :company
end
A Company
has many users and a User
belongs to a Company
. A 1-to-many
from Company
to User
.
However, for this app a User
may or may not belong to a Company
. In other
words a Company
is optional for a given User
.
Let’s look at our database.
companies (id, name)
users (id, email, password, company_id)
Obviously the users
table has a foreign key linking to the companies
table.
But for User
s that don’t have a Company
the value for that field will be
NULL. To me that seems strange. I don’t like the fact that there exists a row
in a table that has a relationship to another table with no value for that
relationship. How would it have been created in the first place. Its like
having a Comment
without a Post
, you’d never have a Comment
in your
database with a NULL value in its post_id
field.
One argument is that the value should be NULL because in the object world a
User
object’s value for its Company
would be nil
. The database equivalent
of nil
would be NULL. But let’s try something else out to see where it takes
us.
I want to get rid of that company_id
foreign key in the users
table. I
think we’re missing a concept here.
class Employment < ActiveRecord::Base
belongs_to :user
belongs_to :company
end
class User < ActiveRecord::Base
has_one :employment
end
class Company < ActiveRecord::Base
has_many :employments
has_many :users, :through => :employments
end
There it is Employment
. Employment
looks a little odd because its a join
model but one side of it is a 1-to-1. A User
has one Employment
and a
Company
has many Employment
s and has many User
s :through Employment
s.
Its fine because in the database world a 1-to-1 looks exactly like a 1-to-many.
Now to get a User
‘s Company
you’d go
user.employment.company
Maybe I can use has_many
:through, to make it feel just like the original
design without Employment
.
class User < ActiveRecord::Base
has_one :employment
has_one :company, :through => :employment
end
Nope. Rails doesn’t like it. That sucks.
How about performance?
In the first design without the Employment
model,
user.company
would result in 1 join, from the users
table to the companies
table.
In the second design with the Employment
model,
user.employment.company
would result in 2 joins, 1 from the users
table to the employments
table and
1 from the employments
table to the companies
table. If you have a web page
that lists all your User
s and all their Company
s or vice versa then this
could get expensive. In that case it would be best to use
ActiveRecord::Base#find’s :include parameter to eagerly fetch the other side of
the relationship.
Damn NULLs.