Neu: Das englische Ruby on Rails 4.0 Buch.

4.10. has_one – 1:1-Verknüpfung

Ähnlich wie bei has_many (siehe Abschnitt 4.8, „has_many – 1:n-Verknüpfung“), wird auch bei has_one ein Model mit einem anderen Model logisch verknüpft. Im Gegensatz zu has_many, gibt es aber bei has_one genau jeweils nur einen Datensatz, der mit genau einem anderen Datensatz verknüpft ist. In den meisten praktischen Anwendungsfällen bietet es sich logischerweise an, beides in das gleiche Model und damit in die gleiche Datenbank-Tabelle zu packen, aber der Vollständigkeit halber möchte ich hier auch has_one besprechen.

Tipp

Wahrscheinlich können Sie mit ruhigem Gewissen has_one überspringen.
Bei den Beispielen gehe ich davon aus, dass Sie Abschnitt 4.8, „has_many – 1:n-Verknüpfung“ bereits gelesen und verstanden haben. Ich werde Methoden wie build („build“) nicht neu erklären, sondern als Grundlage voraussetzen.

Vorbereitung

Wir greifen das Beispiel aus der Rails-Doku auf (siehe http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html) und erstellen eine Applikation, in der es Angestellte und Büros gibt. Jeder Angestellte hat ein Büro. Erst die Applikation:
MacBook:~ xyz$ rails new office-space
[...]
MacBook:~ xyz$ cd office-space 
MacBook:office-space xyz$
Und jetzt die beiden Modele:
MacBook:office-space xyz$ rails generate model employee last_name
[...]
MacBook:office-space xyz$ rails generate model office location employee_id:integer
[...]
MacBook:office-space xyz$ rake db:migrate
[...]
MacBook:office-space xyz$

Verknüpfung

Die Verknüpfung in der Datei app/model/employee.rb:
class Employee < ActiveRecord::Base
  attr_accessible :last_name

  has_one :office
end
Und das Gegenstück in der app/model/office.rb:
class Office < ActiveRecord::Base
  attr_accessible :employee_id, :location

  belongs_to :employee
end

Optionen

Die Optionen von has_one ähneln denen von has_many. Aus diesem Grund verweise ich an dieser Stelle auf „Optionen“ oder auf http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_one. Auf die Schnelle hier ein paar allgemeine Beispiele (zum Teil aus der Rails-Doku):
has_one :office, :dependent => :destroy  # loescht den Office Eintrag automatisch mit
has_one :office, :dependent => :nullify  # wie :destroy, nur das der Office Datensatz nicht 
                                         # geloescht wird, sondern employee_id auf NULL gesetzt
                                         # wird
has_one :last_comment, :class_name => "Comment", :order => "posted_on"
has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
has_one :boss, :readonly => :true
has_one :club, :through => :membership
has_one :primary_address, :through => :addressables, :conditions => ["addressable.primary = ?", true], :source => :addressable

Consolen-Beispiele

Starten wir einmal die Console und legen zwei Angestellte an:
MacBook:office-space xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Employee
 => Employee(id: integer, last_name: string, created_at: datetime, updated_at: datetime) 
1.9.3p194 :002 > Employee.create(:last_name => 'Udelhoven')
   (0.1ms)  begin transaction
  SQL (9.3ms)  INSERT INTO "employees" ("created_at", "last_name", "updated_at") VALUES (?, ?, ?)  [["created_at", Tue, 08 May 2012 06:42:43 UTC +00:00], ["last_name", "Udelhoven"], ["updated_at", Tue, 08 May 2012 06:42:43 UTC +00:00]]
   (0.9ms)  commit transaction
 => #<Employee id: 1, last_name: "Udelhoven", created_at: "2012-05-08 06:42:43", updated_at: "2012-05-08 06:42:43"> 
1.9.3p194 :003 > Employee.create(:last_name => 'Meier')
   (0.1ms)  begin transaction
  SQL (0.5ms)  INSERT INTO "employees" ("created_at", "last_name", "updated_at") VALUES (?, ?, ?)  [["created_at", Tue, 08 May 2012 06:42:49 UTC +00:00], ["last_name", "Meier"], ["updated_at", Tue, 08 May 2012 06:42:49 UTC +00:00]]
   (5.2ms)  commit transaction
 => #<Employee id: 2, last_name: "Meier", created_at: "2012-05-08 06:42:49", updated_at: "2012-05-08 06:42:49"> 
1.9.3p194 :004 >
Jetzt bekommt ein Angestellter sein eigenes Büro:
1.9.3p194 :004 > Office.create(:location => '1. Stock', :employee_id => 1)
   (0.1ms)  begin transaction
  SQL (0.6ms)  INSERT INTO "offices" ("created_at", "employee_id", "location", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", Tue, 08 May 2012 06:44:04 UTC +00:00], ["employee_id", 1], ["location", "1. Stock"], ["updated_at", Tue, 08 May 2012 06:44:04 UTC +00:00]]
   (3.3ms)  commit transaction
 => #<Office id: 1, location: "1. Stock", employee_id: 1, created_at: "2012-05-08 06:44:04", updated_at: "2012-05-08 06:44:04"> 
1.9.3p194 :005 > 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: "1. Stock", employee_id: 1, created_at: "2012-05-08 06:44:04", updated_at: "2012-05-08 06:44:04"> 
1.9.3p194 :006 > 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-05-08 06:42:43", updated_at: "2012-05-08 06:42:43"> 
1.9.3p194 :007 >
Beim zweiten Angestellten benutzen wir die automatisch erzeugte create_office-Methode (bei has_many würden wir hier offices.create benutzen):
1.9.3p194 :007 > Employee.last.create_office(:location => 'Erdgeschoss')
  Employee Load (0.3ms)  SELECT "employees".* FROM "employees" ORDER BY "employees"."id" DESC LIMIT 1
   (0.1ms)  begin transaction
  SQL (0.7ms)  INSERT INTO "offices" ("created_at", "employee_id", "location", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", Tue, 08 May 2012 06:45:12 UTC +00:00], ["employee_id", 2], ["location", "Erdgeschoss"], ["updated_at", Tue, 08 May 2012 06:45:12 UTC +00:00]]
   (2.4ms)  commit transaction
  Office Load (0.2ms)  SELECT "offices".* FROM "offices" WHERE "offices"."employee_id" = 2 LIMIT 1
   (0.0ms)  begin transaction
   (0.0ms)  commit transaction
 => #<Office id: 2, location: "Erdgeschoss", employee_id: 2, created_at: "2012-05-08 06:45:12", updated_at: "2012-05-08 06:45:12"> 
1.9.3p194 :008 >
Löschen geht intuitiv mit destroy:
1.9.3p194 :008 > Employee.first.office
  Employee Load (0.2ms)  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: "1. Stock", employee_id: 1, created_at: "2012-05-08 06:44:04", updated_at: "2012-05-08 06:44:04"> 
1.9.3p194 :009 > Employee.first.office.destroy
  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
   (0.1ms)  begin transaction
  SQL (0.4ms)  DELETE FROM "offices" WHERE "offices"."id" = ?  [["id", 1]]
   (3.6ms)  commit transaction
 => #<Office id: 1, location: "1. Stock", employee_id: 1, created_at: "2012-05-08 06:44:04", updated_at: "2012-05-08 06:44:04"> 
1.9.3p194 :010 > Office.exists?(1)
  Office Exists (0.2ms)  SELECT 1 FROM "offices" WHERE "offices"."id" = 1 LIMIT 1
 => false 
1.9.3p194 :011 > exit
MacBook:office-space xyz$

Warnung

Sollten Sie für einen Employee mit einem existierenden Office ein neues Office anlegen, so bekommen Sie keine Fehlermeldung:
MacBook:office-space xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Employee.last.office
  Employee Load (0.1ms)  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: 2, location: "Erdgeschoss", employee_id: 2, created_at: "2012-05-08 06:45:12", updated_at: "2012-05-08 06:45:12"> 
1.9.3p194 :002 > Employee.last.create_office(:location => 'Keller')
  Employee Load (0.3ms)  SELECT "employees".* FROM "employees" ORDER BY "employees"."id" DESC LIMIT 1
   (0.1ms)  begin transaction
  SQL (29.3ms)  INSERT INTO "offices" ("created_at", "employee_id", "location", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", Tue, 08 May 2012 06:51:58 UTC +00:00], ["employee_id", 2], ["location", "Keller"], ["updated_at", Tue, 08 May 2012 06:51:58 UTC +00:00]]
   (3.7ms)  commit transaction
  Office Load (0.2ms)  SELECT "offices".* FROM "offices" WHERE "offices"."employee_id" = 2 LIMIT 1
   (0.0ms)  begin transaction
   (0.3ms)  UPDATE "offices" SET "employee_id" = NULL, "updated_at" = '2012-05-08 06:51:58.969829' WHERE "offices"."id" = 2
   (0.9ms)  commit transaction
 => #<Office id: 3, location: "Keller", employee_id: 2, created_at: "2012-05-08 06:51:58", updated_at: "2012-05-08 06:51:58"> 
1.9.3p194 :003 > 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: "Keller", employee_id: 2, created_at: "2012-05-08 06:51:58", updated_at: "2012-05-08 06:51:58"> 
1.9.3p194 :004 >
Das alte Office ist sogar noch in der Datenbank (die employee_id wurde automatisch auf nil gesetzt):
1.9.3p194 :004 > Office.all
  Office Load (0.2ms)  SELECT "offices".* FROM "offices" 
 => [#<Office id: 2, location: "Erdgeschoss", employee_id: nil, created_at: "2012-05-08 06:45:12", updated_at: "2012-05-08 06:51:58">, #<Office id: 3, location: "Keller", employee_id: 2, created_at: "2012-05-08 06:51:58", updated_at: "2012-05-08 06:51:58">] 
1.9.3p194 :005 > exit
MacBook:office-space xyz$

has_one vs. belongs_to

Sowohl has_one wie auch belongs_to bietet die Möglichkeit, eine 1:1-Verbindung abzubilden. Der Unterschied liegt in der Praxis in der persönlichen Präferenz des Programmierers und dem Ort des Foreign-Keys. Im Allgemeinen wird has_one eher selten benutzt und hängt vom Normalisierungsgrad des Datenschemas ab.