4.10. has_one – 1:1 Association

Similar to has_many (see Section 4.8, “has_many – 1:n Association”), the method has_one also creates a logical relation between two models. But in contrast to has_many, one record is only ever associated with exactly one other record in has_one. In most practical cases of application, it logically makes sense to put both into the same model and therefore the same database table, but for the sake of completeness I also want to discuss has_one here.

Tip

You can probably safely skip has_one without losing any sleep.
In the examples, I assume that you have already read and understood Section 4.8, “has_many – 1:n Association”. I am not going to explain methods like build (the section called “build”) again but assume that you already know the basics.

Preparation

We use the example from the Rails documentation (see http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html) and create an application containing employees and offices. Each employee has an office. First the application:
$ rails new office-space
  [...]
$ cd office-space
$
And now the two models:
$ rails generate model employee last_name
  [...]
$ rails generate model office location employee_id:integer
  [...]
$ rake db:migrate
  [...]
$

Association

The association in the file app/model/employee.rb:
class Employee < ActiveRecord::Base
  attr_accessible :last_name

  has_one :office
end
And its counterpart in the file app/model/office.rb:
class Office < ActiveRecord::Base
  attr_accessible :employee_id, :location

  belongs_to :employee
end

Options

The options of has_one are similar to those of has_many. So for details, please refer to the section called “Options” or http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_one.

Console Examples

Let's start the console and create two employees:
$ rails console
Loading development environment (Rails 3.2.9)
>> Employee.create(last_name: 'Udelhoven')
   (0.1ms)  begin transaction
  SQL (24.3ms)  INSERT INTO "employees" ("created_at", "last_name", "updated_at") VALUES (?, ?, ?)  [["created_at", Sun, 18 Nov 2012 14:38:58 UTC +00:00], ["last_name", "Udelhoven"], ["updated_at", Sun, 18 Nov 2012 14:38:58 UTC +00:00]]
   (2.3ms)  commit transaction
=> #<Employee id: 1, last_name: "Udelhoven", created_at: "2012-11-18 14:38:58", updated_at: "2012-11-18 14:38:58">
>> Employee.create(last_name: 'Meier')
   (0.1ms)  begin transaction
  SQL (1.4ms)  INSERT INTO "employees" ("created_at", "last_name", "updated_at") VALUES (?, ?, ?)  [["created_at", Sun, 18 Nov 2012 14:39:06 UTC +00:00], ["last_name", "Meier"], ["updated_at", Sun, 18 Nov 2012 14:39:06 UTC +00:00]]
   (2.2ms)  commit transaction
=> #<Employee id: 2, last_name: "Meier", created_at: "2012-11-18 14:39:06", updated_at: "2012-11-18 14:39:06">
>> 
Now an employee gets his own office:
>> Office.create(location: '2nd floor', employee_id: 1)
   (0.1ms)  begin transaction
  SQL (0.5ms)  INSERT INTO "offices" ("created_at", "employee_id", "location", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", Sun, 18 Nov 2012 14:39:47 UTC +00:00], ["employee_id", 1], ["location", "2nd floor"], ["updated_at", Sun, 18 Nov 2012 14:39:47 UTC +00:00]]
   (3.1ms)  commit transaction
=> #<Office id: 1, location: "2nd floor", employee_id: 1, created_at: "2012-11-18 14:39:47", updated_at: "2012-11-18 14:39:47">
>> Employee.first.office
  Employee Load (0.3ms)  SELECT "employees".* FROM "employees" LIMIT 1
  Office Load (0.2ms)  SELECT "offices".* FROM "offices" WHERE "offices"."employee_id" = 1 LIMIT 1
=> #<Office id: 1, location: "2nd floor", employee_id: 1, created_at: "2012-11-18 14:39:47", updated_at: "2012-11-18 14:39:47">
>> Office.first.employee
  Office Load (0.3ms)  SELECT "offices".* FROM "offices" LIMIT 1
  Employee Load (0.2ms)  SELECT "employees".* FROM "employees" WHERE "employees"."id" = 1 LIMIT 1
=> #<Employee id: 1, last_name: "Udelhoven", created_at: "2012-11-18 14:38:58", updated_at: "2012-11-18 14:38:58">
>>
For the second employee, we use the automatically generated method create_office (with has_many, we would use offices.create here):
>> Employee.last.create_office(location: '1st floor')
  Employee Load (0.3ms)  SELECT "employees".* FROM "employees" ORDER BY "employees"."id" DESC LIMIT 1
   (0.1ms)  begin transaction
  SQL (0.5ms)  INSERT INTO "offices" ("created_at", "employee_id", "location", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", Sun, 18 Nov 2012 14:40:51 UTC +00:00], ["employee_id", 2], ["location", "1st floor"], ["updated_at", Sun, 18 Nov 2012 14:40:51 UTC +00:00]]
   (1.9ms)  commit transaction
  Office Load (0.2ms)  SELECT "offices".* FROM "offices" WHERE "offices"."employee_id" = 2 LIMIT 1
=> #<Office id: 2, location: "1st floor", employee_id: 2, created_at: "2012-11-18 14:40:51", updated_at: "2012-11-18 14:40:51">
>>
Removing is intuitively done via destroy:
>> Employee.first.office.destroy
  Employee Load (0.3ms)  SELECT "employees".* FROM "employees" LIMIT 1
  Office Load (0.1ms)  SELECT "offices".* FROM "offices" WHERE "offices"."employee_id" = 1 LIMIT 1
   (0.1ms)  begin transaction
  SQL (0.3ms)  DELETE FROM "offices" WHERE "offices"."id" = ?  [["id", 1]]
   (2.2ms)  commit transaction
=> #<Office id: 1, location: "2nd floor", employee_id: 1, created_at: "2012-11-18 14:39:47", updated_at: "2012-11-18 14:39:47">
>> Office.exists?(1)
  Office Exists (0.2ms)  SELECT 1 AS one FROM "offices" WHERE "offices"."id" = 1 LIMIT 1
=> false
>> exit
$ 

Warning

If you create a new Office for an Employee with an existing Office then you will not get an error message:
>> Employee.last.office
  Employee Load (0.1ms)  SELECT "employees".* FROM "employees" ORDER BY "employees"."id" DESC LIMIT 1
  Office Load (0.1ms)  SELECT "offices".* FROM "offices" WHERE "offices"."employee_id" = 2 LIMIT 1
=> #<Office id: 2, location: "1st floor", employee_id: 2, created_at: "2012-11-18 14:40:51", updated_at: "2012-11-18 14:40:51">
>> Employee.last.create_office(location: 'Basement')
  Employee Load (0.3ms)  SELECT "employees".* FROM "employees" ORDER BY "employees"."id" DESC LIMIT 1
   (0.0ms)  begin transaction
  SQL (5.8ms)  INSERT INTO "offices" ("created_at", "employee_id", "location", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", Sun, 18 Nov 2012 14:42:59 UTC +00:00], ["employee_id", 2], ["location", "Basement"], ["updated_at", Sun, 18 Nov 2012 14:42:59 UTC +00:00]]
   (3.0ms)  commit transaction
  Office Load (0.2ms)  SELECT "offices".* FROM "offices" WHERE "offices"."employee_id" = 2 LIMIT 1
   (0.0ms)  begin transaction
   (0.2ms)  UPDATE "offices" SET "employee_id" = NULL, "updated_at" = '2012-11-18 14:42:59.648228' WHERE "offices"."id" = 2
   (0.6ms)  commit transaction
=> #<Office id: 3, location: "Basement", employee_id: 2, created_at: "2012-11-18 14:42:59", updated_at: "2012-11-18 14:42:59">
>> Employee.last.office
  Employee Load (0.3ms)  SELECT "employees".* FROM "employees" ORDER BY "employees"."id" DESC LIMIT 1
  Office Load (0.2ms)  SELECT "offices".* FROM "offices" WHERE "offices"."employee_id" = 2 LIMIT 1
=> #<Office id: 3, location: "Basement", employee_id: 2, created_at: "2012-11-18 14:42:59", updated_at: "2012-11-18 14:42:59">
>>
The old Office is even still in the database (the employee_id was automatically set to nil):
>> Office.all
  Office Load (0.5ms)  SELECT "offices".* FROM "offices" 
=> [#<Office id: 2, location: "1st floor", employee_id: nil, created_at: "2012-11-18 14:40:51", updated_at: "2012-11-18 14:42:59">, #<Office id: 3, location: "Basement", employee_id: 2, created_at: "2012-11-18 14:42:59", updated_at: "2012-11-18 14:42:59">]
>> exit
$

has_one vs. belongs_to

Both has_one and belongs_to offer the option of representing a 1:1 relationship. The difference in practice is in the programmer's personal preference and the location of the foreign key. In general, has_one tends to be used rarely and depends on the degree of normalization of the data schema.

Updates about this book will be published on my Twitter feed.