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.

Buy the new Rails 5.1 version of this book.

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
  has_one :office
end
And its counterpart in the file app/model/office.rb:
class Office < ActiveRecord::Base
  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 4.0.0)
>> Employee.create(last_name: 'Udelhoven')
   (0.1ms)  begin transaction
  SQL (2.3ms)  INSERT INTO "employees" ("created_at", "last_name", "updated_at") VALUES (?, ?, ?)  [["created_at", Tue, 16 Jul 2013 12:44:24 UTC +00:00], ["last_name", "Udelhoven"], ["updated_at", Tue, 16 Jul 2013 12:44:24 UTC +00:00]]
   (1.0ms)  commit transaction
=> #<Employee id: 1, last_name: "Udelhoven", created_at: "2013-07-16 12:44:24", updated_at: "2013-07-16 12:44:24">
>> Employee.create(last_name: 'Meier')
   (0.2ms)  begin transaction
  SQL (0.7ms)  INSERT INTO "employees" ("created_at", "last_name", "updated_at") VALUES (?, ?, ?)  [["created_at", Tue, 16 Jul 2013 12:44:32 UTC +00:00], ["last_name", "Meier"], ["updated_at", Tue, 16 Jul 2013 12:44:32 UTC +00:00]]
   (2.2ms)  commit transaction
=> #<Employee id: 2, last_name: "Meier", created_at: "2013-07-16 12:44:32", updated_at: "2013-07-16 12:44:32">
>>
Now the first employee gets his own office:
>> Office.create(location: '2nd floor', employee_id: Employee.first.id)
  Employee Load (0.2ms)  SELECT "employees".* FROM "employees" ORDER BY "employees"."id" ASC LIMIT 1
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "offices" ("created_at", "employee_id", "location", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", Tue, 16 Jul 2013 12:45:13 UTC +00:00], ["employee_id", 1], ["location", "2nd floor"], ["updated_at", Tue, 16 Jul 2013 12:45:13 UTC +00:00]]
   (2.2ms)  commit transaction
=> #<Office id: 1, location: "2nd floor", employee_id: 1, created_at: "2013-07-16 12:45:13", updated_at: "2013-07-16 12:45:13">
>> 
Both directions can be accessed the normal way:
>> Employee.first.office
  Employee Load (0.4ms)  SELECT "employees".* FROM "employees" ORDER BY "employees"."id" ASC LIMIT 1
  Office Load (0.2ms)  SELECT "offices".* FROM "offices" WHERE "offices"."employee_id" = ? ORDER BY "offices"."id" ASC LIMIT 1  [["employee_id", 1]]
=> #<Office id: 1, location: "2nd floor", employee_id: 1, created_at: "2013-07-16 12:45:13", updated_at: "2013-07-16 12:45:13">
>> Office.first.employee
  Office Load (0.3ms)  SELECT "offices".* FROM "offices" ORDER BY "offices"."id" ASC LIMIT 1
  Employee Load (0.2ms)  SELECT "employees".* FROM "employees" WHERE "employees"."id" = ? ORDER BY "employees"."id" ASC LIMIT 1  [["id", 1]]
=> #<Employee id: 1, last_name: "Udelhoven", created_at: "2013-07-16 12:44:24", updated_at: "2013-07-16 12:44:24">
>>
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.2ms)  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", Tue, 16 Jul 2013 12:46:58 UTC +00:00], ["employee_id", 2], ["location", "1st floor"], ["updated_at", Tue, 16 Jul 2013 12:46:58 UTC +00:00]]
   (2.3ms)  commit transaction
  Office Load (0.1ms)  SELECT "offices".* FROM "offices" WHERE "offices"."employee_id" = ? ORDER BY "offices"."id" ASC LIMIT 1  [["employee_id", 2]]
=> #<Office id: 2, location: "1st floor", employee_id: 2, created_at: "2013-07-16 12:46:58", updated_at: "2013-07-16 12:46:58">
>> 
Removing is intuitively done via destroy:
>> Employee.first.office.destroy
  Employee Load (0.3ms)  SELECT "employees".* FROM "employees" ORDER BY "employees"."id" ASC LIMIT 1
  Office Load (0.1ms)  SELECT "offices".* FROM "offices" WHERE "offices"."employee_id" = ? ORDER BY "offices"."id" ASC LIMIT 1  [["employee_id", 1]]
   (0.1ms)  begin transaction
  SQL (0.3ms)  DELETE FROM "offices" WHERE "offices"."id" = ?  [["id", 1]]
   (2.4ms)  commit transaction
=> #<Office id: 1, location: "2nd floor", employee_id: 1, created_at: "2013-07-16 12:45:13", updated_at: "2013-07-16 12:45:13">
>>>> Employee.first.office
  Employee Load (0.4ms)  SELECT "employees".* FROM "employees" ORDER BY "employees"."id" ASC LIMIT 1
  Office Load (0.1ms)  SELECT "offices".* FROM "offices" WHERE "offices"."employee_id" = ? ORDER BY "offices"."id" ASC LIMIT 1  [["employee_id", 1]]
=> nil
>>>>

Buy the new Rails 5.1 version of this book.

If you create a new Office for an Employee with an existing Office then you will not get an error message:
>> Employee.last.create_office(location: 'Basement')
  Employee Load (0.3ms)  SELECT "employees".* FROM "employees" ORDER BY "employees"."id" DESC LIMIT 1
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "offices" ("created_at", "employee_id", "location", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", Tue, 16 Jul 2013 12:48:44 UTC +00:00], ["employee_id", 2], ["location", "Basement"], ["updated_at", Tue, 16 Jul 2013 12:48:44 UTC +00:00]]
   (1.9ms)  commit transaction
  Office Load (0.1ms)  SELECT "offices".* FROM "offices" WHERE "offices"."employee_id" = ? ORDER BY "offices"."id" ASC LIMIT 1  [["employee_id", 2]]
   (0.1ms)  begin transaction
  SQL (0.4ms)  UPDATE "offices" SET "employee_id" = ?, "updated_at" = ? WHERE "offices"."id" = 2  [["employee_id", nil], ["updated_at", Tue, 16 Jul 2013 12:48:44 UTC +00:00]]
   (0.8ms)  commit transaction
=> #<Office id: 3, location: "Basement", employee_id: 2, created_at: "2013-07-16 12:48:44", updated_at: "2013-07-16 12:48:44">
>> Employee.last.office
  Employee Load (0.2ms)  SELECT "employees".* FROM "employees" ORDER BY "employees"."id" DESC LIMIT 1
  Office Load (0.2ms)  SELECT "offices".* FROM "offices" WHERE "offices"."employee_id" = ? ORDER BY "offices"."id" ASC LIMIT 1  [["employee_id", 2]]
=> #<Office id: 3, location: "Basement", employee_id: 2, created_at: "2013-07-16 12:48:44", updated_at: "2013-07-16 12:48:44">
>>
The old Office is even still in the database (the employee_id was automatically set to nil):
>> Office.all
  Office Load (0.3ms)  SELECT "offices".* FROM "offices"
=> #<ActiveRecord::Relation [#<Office id: 2, location: "1st floor", employee_id: nil, created_at: "2013-07-16 12:46:58", updated_at: "2013-07-16 12:48:44">, #<Office id: 3, location: "Basement", employee_id: 2, created_at: "2013-07-16 12:48:44", updated_at: "2013-07-16 12:48:44">]>
>> 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 very rarely and depends on the degree of normalization of the data schema.