Neu: Das englische Ruby on Rails 4.0 Buch.

4.8. has_many – 1:n-Verknüpfung

Um has_many zu erklären, erstellen wir eine Buch-Datenbank. In dieser Datenbank gibt es ein Model mit Büchern (books) und ein Model mit Autoren (authors). Da ein Buch mehrere Autoren haben kann, benötigen wir zum Abbilden eine 1:n-Verknüpfung (one-to-many association). Sie werden sehen, wie einfach das Ganze mit ActiveRecord ist.

Anmerkung

Verknüpfungen nennt man übrigens auch Beziehung, Relationen (Relations Relationships) oder Assoziationen (Associations).
Wir erstellen als Erstes die Rails-Applikation:
MacBook:~ xyz$ rails new bookshelf
[...]
MacBook:~ xyz$ cd bookshelf 
MacBook:bookshelf xyz$ 
Jetzt legen wir das Model für die Bücher an:
MacBook:bookshelf xyz$ rails generate model book title
      invoke  active_record
      create    db/migrate/20120506132424_create_books.rb
      create    app/models/book.rb
      invoke    test_unit
      create      test/unit/book_test.rb
      create      test/fixtures/books.yml
MacBook:bookshelf xyz$ 
Und zum Schluss legen wir noch die Datenbank-Tabelle für die Autoren an. In dieser benötigen wir ein Zuordnungsfeld zur Bücher-Tabelle. Dieser Fremdschlüssel (Foreign key) wird per Default immer als Name des referenzierten Objektes (hier: book) mit angehängtem _id gesetzt:
MacBook:bookshelf xyz$ rails generate model author book_id:integer first_name last_name
      invoke  active_record
      create    db/migrate/20120506132619_create_authors.rb
      create    app/models/author.rb
      invoke    test_unit
      create      test/unit/author_test.rb
      create      test/fixtures/authors.yml
MacBook:bookshelf xyz$
Danach ein rake db:migrate ausführen, damit die Datenbank-Tabellen auch angelegt werden:
MacBook:bookshelf xyz$ rake db:migrate
==  CreateBooks: migrating ====================================================
-- create_table(:books)
   -> 0.0014s
==  CreateBooks: migrated (0.0015s) ===========================================

==  CreateAuthors: migrating ==================================================
-- create_table(:authors)
   -> 0.0015s
==  CreateAuthors: migrated (0.0015s) =========================================

MacBook:bookshelf xyz$ 
Schauen wir uns das auf der Console an:
MacBook:bookshelf xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Book
 => Book(id: integer, title: string, created_at: datetime, updated_at: datetime) 
1.9.3p194 :002 > Author
 => Author(id: integer, book_id: integer, first_name: string, last_name: string, created_at: datetime, updated_at: datetime) 
1.9.3p194 :003 > exit
MacBook:bookshelf xyz$ 
Die zwei Datenbank-Tabellen sind eingerichtet und können mit ActiveRecord benutzt werden. Allerdings weiß ActiveRecord noch nichts von der 1:n-Beziehung der beiden. Das ist aber recht einfach in zwei Schritten realisierbar:
  • Wir fügen in der Model-Datei app/models/book.rb die Option has_many ein:
    class Book < ActiveRecord::Base
      attr_accessible :title
    
      has_many :authors
    end
  • Und wir fügen in der Model-Datei app/models/author.rb die Option belongs_to ein:
    class Author < ActiveRecord::Base
      attr_accessible :book_id, :first_name, :last_name
    
      belongs_to :book
    end
Diese zwei einfachen Definitionen sorgen für eine ganze Menge Rails-Magie.

Datensätze erstellen

Wir wollen in diesem Beispiel das Buch "Homo faber" von Max Frisch abspeichern.

Manuell

Wir löschen die Datenbank mit rake db:reset
MacBook:bookshelf xyz$ rake db:reset
-- create_table("authors", {:force=>true})
   -> 0.0151s
-- create_table("books", {:force=>true})
   -> 0.0024s
-- initialize_schema_migrations_table()
   -> 0.0033s
-- assume_migrated_upto_version(20120506132619, ["/Users/xyz/bookshelf/db/migrate"])
   -> 0.0033s
MacBook:bookshelf xyz$ 
und erstellen zuerst ein Objekt mit den Buch-Daten. Dann merken wir uns die ID für diesen Datensatz und erstellen danach die Autoren-Datensatz mit dieser ID im Feld book_id:
MacBook:bookshelf xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > book = Book.create(:title => 'Homo faber')
   (0.1ms)  begin transaction
  SQL (5.0ms)  INSERT INTO "books" ("created_at", "title", "updated_at") VALUES (?, ?, ?)  [["created_at", Sun, 06 May 2012 13:47:45 UTC +00:00], ["title", "Homo faber"], ["updated_at", Sun, 06 May 2012 13:47:45 UTC +00:00]]
   (1.2ms)  commit transaction
 => #<Book id: 1, title: "Homo faber", created_at: "2012-05-06 13:47:45", updated_at: "2012-05-06 13:47:45"> 
1.9.3p194 :002 > author = Author.create(:book_id => 1, :first_name => 'Max', :last_name => 'Frisch')
   (0.1ms)  begin transaction
  SQL (0.7ms)  INSERT INTO "authors" ("book_id", "created_at", "first_name", "last_name", "updated_at") VALUES (?, ?, ?, ?, ?)  [["book_id", 1], ["created_at", Sun, 06 May 2012 13:48:17 UTC +00:00], ["first_name", "Max"], ["last_name", "Frisch"], ["updated_at", Sun, 06 May 2012 13:48:17 UTC +00:00]]
   (3.1ms)  commit transaction
 => #<Author id: 1, book_id: 1, first_name: "Max", last_name: "Frisch", created_at: "2012-05-06 13:48:17", updated_at: "2012-05-06 13:48:17"> 
1.9.3p194 :003 > Book.all
  Book Load (0.3ms)  SELECT "books".* FROM "books" 
 => [#<Book id: 1, title: "Homo faber", created_at: "2012-05-06 13:47:45", updated_at: "2012-05-06 13:47:45">] 
1.9.3p194 :004 > Author.all
  Author Load (0.3ms)  SELECT "authors".* FROM "authors" 
 => [#<Author id: 1, book_id: 1, first_name: "Max", last_name: "Frisch", created_at: "2012-05-06 13:48:17", updated_at: "2012-05-06 13:48:17">] 
1.9.3p194 :005 > exit
MacBook:bookshelf xyz$
Die book_id quasi manuell einzutragen, ist natürlich sehr unpraktisch und fehleranfällig. Deshalb gibt es die Methode „create“.

create

Jetzt versuchen wir das Gleiche wie in „Manuell“, aber benutzen diesmal ein wenig ActiveRecord-Magie. Wir können mit der Methode create von authors zu jedem Book-Objekt neue Autoren hinzufügen. Diese werden automatisch korrekt mit der book_id bestückt:
MacBook:bookshelf xyz$ rake db:reset
[...]
MacBook:bookshelf xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > book = Book.create(:title => 'Homo faber')
   (0.1ms)  begin transaction
  SQL (4.9ms)  INSERT INTO "books" ("created_at", "title", "updated_at") VALUES (?, ?, ?)  [["created_at", Sun, 06 May 2012 13:52:28 UTC +00:00], ["title", "Homo faber"], ["updated_at", Sun, 06 May 2012 13:52:28 UTC +00:00]]
   (2.6ms)  commit transaction
 => #<Book id: 1, title: "Homo faber", created_at: "2012-05-06 13:52:28", updated_at: "2012-05-06 13:52:28"> 
1.9.3p194 :002 > author = book.authors.create(:first_name => 'Max', :last_name => 'Frisch')
   (0.1ms)  begin transaction
  SQL (0.6ms)  INSERT INTO "authors" ("book_id", "created_at", "first_name", "last_name", "updated_at") VALUES (?, ?, ?, ?, ?)  [["book_id", 1], ["created_at", Sun, 06 May 2012 13:52:52 UTC +00:00], ["first_name", "Max"], ["last_name", "Frisch"], ["updated_at", Sun, 06 May 2012 13:52:52 UTC +00:00]]
   (0.8ms)  commit transaction
 => #<Author id: 1, book_id: 1, first_name: "Max", last_name: "Frisch", created_at: "2012-05-06 13:52:52", updated_at: "2012-05-06 13:52:52"> 
1.9.3p194 :003 > Book.all
  Book Load (0.3ms)  SELECT "books".* FROM "books" 
 => [#<Book id: 1, title: "Homo faber", created_at: "2012-05-06 13:52:28", updated_at: "2012-05-06 13:52:28">] 
1.9.3p194 :004 > Author.all
  Author Load (0.3ms)  SELECT "authors".* FROM "authors" 
 => [#<Author id: 1, book_id: 1, first_name: "Max", last_name: "Frisch", created_at: "2012-05-06 13:52:52", updated_at: "2012-05-06 13:52:52">] 
1.9.3p194 :005 > exit
MacBook:bookshelf xyz$
Sie können auch direkt das authors.create() hinter das Book.create() setzen:
MacBook:bookshelf xyz$ rake db:reset
[...]
MacBook:bookshelf xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Book.create(:title => 'Homo faber').authors.create(:first_name => 'Max', :last_name => 'Frisch')
   (0.1ms)  begin transaction
  SQL (5.2ms)  INSERT INTO "books" ("created_at", "title", "updated_at") VALUES (?, ?, ?)  [["created_at", Sun, 06 May 2012 13:56:38 UTC +00:00], ["title", "Homo faber"], ["updated_at", Sun, 06 May 2012 13:56:38 UTC +00:00]]
   (3.6ms)  commit transaction
   (0.1ms)  begin transaction
  SQL (0.6ms)  INSERT INTO "authors" ("book_id", "created_at", "first_name", "last_name", "updated_at") VALUES (?, ?, ?, ?, ?)  [["book_id", 1], ["created_at", Sun, 06 May 2012 13:56:38 UTC +00:00], ["first_name", "Max"], ["last_name", "Frisch"], ["updated_at", Sun, 06 May 2012 13:56:38 UTC +00:00]]
   (1.1ms)  commit transaction
 => #<Author id: 1, book_id: 1, first_name: "Max", last_name: "Frisch", created_at: "2012-05-06 13:56:38", updated_at: "2012-05-06 13:56:38"> 
1.9.3p194 :002 > Book.all
  Book Load (0.3ms)  SELECT "books".* FROM "books" 
 => [#<Book id: 1, title: "Homo faber", created_at: "2012-05-06 13:56:38", updated_at: "2012-05-06 13:56:38">] 
1.9.3p194 :003 > Author.all
  Author Load (0.3ms)  SELECT "authors".* FROM "authors" 
 => [#<Author id: 1, book_id: 1, first_name: "Max", last_name: "Frisch", created_at: "2012-05-06 13:56:38", updated_at: "2012-05-06 13:56:38">] 
1.9.3p194 :004 > exit
MacBook:bookshelf xyz$
Da create statt nur einem Hash alternativ auch ein Array von Hashes akzeptiert, können Sie auch mehrere Autoren für ein Buch auf einmal anlegen:
MacBook:bookshelf xyz$ rake db:reset
[...]
MacBook:bookshelf xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Book.create(:title => 'Beispiel').authors.create([{:last_name => 'A'}, {:last_name => 'B'}, {:last_name => 'C'}])
   (0.1ms)  begin transaction
  SQL (5.0ms)  INSERT INTO "books" ("created_at", "title", "updated_at") VALUES (?, ?, ?)  [["created_at", Sun, 06 May 2012 14:03:27 UTC +00:00], ["title", "Beispiel"], ["updated_at", Sun, 06 May 2012 14:03:27 UTC +00:00]]
   (2.7ms)  commit transaction
   (0.1ms)  begin transaction
  SQL (0.6ms)  INSERT INTO "authors" ("book_id", "created_at", "first_name", "last_name", "updated_at") VALUES (?, ?, ?, ?, ?)  [["book_id", 1], ["created_at", Sun, 06 May 2012 14:03:27 UTC +00:00], ["first_name", nil], ["last_name", "A"], ["updated_at", Sun, 06 May 2012 14:03:27 UTC +00:00]]
   (0.9ms)  commit transaction
   (0.0ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "authors" ("book_id", "created_at", "first_name", "last_name", "updated_at") VALUES (?, ?, ?, ?, ?)  [["book_id", 1], ["created_at", Sun, 06 May 2012 14:03:27 UTC +00:00], ["first_name", nil], ["last_name", "B"], ["updated_at", Sun, 06 May 2012 14:03:27 UTC +00:00]]
   (0.8ms)  commit transaction
   (0.0ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "authors" ("book_id", "created_at", "first_name", "last_name", "updated_at") VALUES (?, ?, ?, ?, ?)  [["book_id", 1], ["created_at", Sun, 06 May 2012 14:03:27 UTC +00:00], ["first_name", nil], ["last_name", "C"], ["updated_at", Sun, 06 May 2012 14:03:27 UTC +00:00]]
   (1.0ms)  commit transaction
 => [#<Author id: 1, book_id: 1, first_name: nil, last_name: "A", created_at: "2012-05-06 14:03:27", updated_at: "2012-05-06 14:03:27">, #<Author id: 2, book_id: 1, first_name: nil, last_name: "B", created_at: "2012-05-06 14:03:27", updated_at: "2012-05-06 14:03:27">, #<Author id: 3, book_id: 1, first_name: nil, last_name: "C", created_at: "2012-05-06 14:03:27", updated_at: "2012-05-06 14:03:27">] 
1.9.3p194 :002 > Book.all
  Book Load (0.3ms)  SELECT "books".* FROM "books" 
 => [#<Book id: 1, title: "Beispiel", created_at: "2012-05-06 14:03:27", updated_at: "2012-05-06 14:03:27">] 
1.9.3p194 :003 > Author.all
  Author Load (0.3ms)  SELECT "authors".* FROM "authors" 
 => [#<Author id: 1, book_id: 1, first_name: nil, last_name: "A", created_at: "2012-05-06 14:03:27", updated_at: "2012-05-06 14:03:27">, #<Author id: 2, book_id: 1, first_name: nil, last_name: "B", created_at: "2012-05-06 14:03:27", updated_at: "2012-05-06 14:03:27">, #<Author id: 3, book_id: 1, first_name: nil, last_name: "C", created_at: "2012-05-06 14:03:27", updated_at: "2012-05-06 14:03:27">] 
1.9.3p194 :004 > exit
MacBook:bookshelf xyz$
Allerdings ist das in der Praxis nur dann sinnvoll, wenn die Text-Zeile dadurch nicht zu lang und unübersichtlich wird.

build

Die Methode build ähnelt create. Allerdings wird der Datensatz nicht abgespeichert. Dies erfolgt erst nach einem save:
MacBook:bookshelf xyz$ rake db:reset
[...]
MacBook:bookshelf xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > book = Book.create(:title => 'Homo faber')
   (0.1ms)  begin transaction
  SQL (4.9ms)  INSERT INTO "books" ("created_at", "title", "updated_at") VALUES (?, ?, ?)  [["created_at", Sun, 06 May 2012 14:05:40 UTC +00:00], ["title", "Homo faber"], ["updated_at", Sun, 06 May 2012 14:05:40 UTC +00:00]]
   (3.9ms)  commit transaction
 => #<Book id: 1, title: "Homo faber", created_at: "2012-05-06 14:05:40", updated_at: "2012-05-06 14:05:40"> 
1.9.3p194 :002 > author = book.authors.build(:first_name => 'Max', :last_name => 'Frisch')
 => #<Author id: nil, book_id: 1, first_name: "Max", last_name: "Frisch", created_at: nil, updated_at: nil> 
1.9.3p194 :003 > author.new_record?
 => true 
1.9.3p194 :004 > author.save
   (0.1ms)  begin transaction
  SQL (1.7ms)  INSERT INTO "authors" ("book_id", "created_at", "first_name", "last_name", "updated_at") VALUES (?, ?, ?, ?, ?)  [["book_id", 1], ["created_at", Sun, 06 May 2012 14:06:29 UTC +00:00], ["first_name", "Max"], ["last_name", "Frisch"], ["updated_at", Sun, 06 May 2012 14:06:29 UTC +00:00]]
   (3.3ms)  commit transaction
 => true 
1.9.3p194 :005 > author.new_record?
 => false 
1.9.3p194 :006 > exit
MacBook:bookshelf xyz$

Warnung

Bei der Benutzung von create und build müssen natürlich logische Abhängigkeiten beachtet werden, sonst gibt es einen Fehler. So kann man nicht zwei build-Methoden miteinander verketten. Beispiel:
MacBook:bookshelf xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Book.build(:title => 'Beispiel').authors.build(:last_name => 'A')
NoMethodError: undefined method `build' for #<Class:0x007ff8c52d2330>
 from /Users/xyz/.rvm/gems/ruby-1.9.3-p194/gems/activerecord-3.2.3/lib/active_record/dynamic_matchers.rb:50:in `method_missing'
 from (irb):1
 from /Users/xyz/.rvm/gems/ruby-1.9.3-p194/gems/railties-3.2.3/lib/rails/commands/console.rb:47:in `start'
 from /Users/xyz/.rvm/gems/ruby-1.9.3-p194/gems/railties-3.2.3/lib/rails/commands/console.rb:8:in `start'
 from /Users/xyz/.rvm/gems/ruby-1.9.3-p194/gems/railties-3.2.3/lib/rails/commands.rb:41:in `<top (required)>'
 from script/rails:6:in `require'
 from script/rails:6:in `<main>'
1.9.3p194 :002 > exit
MacBook:bookshelf xyz$ 

Auf Datensätze zugreifen

Dafür brauchen wir Beispieldaten. Deshalb füllen Sie bitte die Datei db/seeds.rb mit folgendem Inhalt:
# ruby encoding: utf-8

Book.create(:title => 'Homo faber').authors.create(:first_name => 'Max', :last_name => 'Frisch')
Book.create(:title => 'Der Besuch der alten Dame').authors.create(:first_name => 'Friedrich', :last_name => 'Dürrenmatt')
Book.create(:title => 'Julius Shulman: The Last Decade').authors.create([
  {:first_name => 'Thomas', :last_name => 'Schirmbock'},
  {:first_name => 'Julius', :last_name => 'Shulman'},
  {:first_name => 'Jürgen', :last_name => 'Nogai'}
  ])
Book.create(:title => 'Julius Shulman: Palm Springs').authors.create([
  {:first_name => 'Michael', :last_name => 'Stern'},
  {:first_name => 'Alan', :last_name => 'Hess'}
  ])
Book.create(:title => 'Photographing Architecture and Interiors').authors.create([
  {:first_name => 'Julius', :last_name => 'Shulman'},
  {:first_name => 'Richard', :last_name => 'Neutra'}
  ])
Book.create(:title => 'Der Zauberberg').authors.create(:first_name => 'Thomas', :last_name => 'Mann')
Book.create(:title => 'In einer Familie').authors.create(:first_name => 'Heinrich', :last_name => 'Mann')
Jetzt die Datenbank löschen und neu mit der db/seeds.rb befüllen:
MacBook:bookshelf xyz$ rake db:reset
[...]
MacBook:bookshelf xyz$
Das Praktische an der 1:n-Zuordnung in ActiveRecord ist der besonders einfache Zugang zu den n-Instanzen. Schauen wir uns das für den ersten Datensatz an:
MacBook:bookshelf xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Book.first
  Book Load (0.1ms)  SELECT "books".* FROM "books" LIMIT 1
 => #<Book id: 1, title: "Homo faber", created_at: "2012-05-06 14:23:43", updated_at: "2012-05-06 14:23:43"> 
1.9.3p194 :002 > Book.first.authors
  Book Load (0.3ms)  SELECT "books".* FROM "books" LIMIT 1
  Author Load (0.2ms)  SELECT "authors".* FROM "authors" WHERE "authors"."book_id" = 1
 => [#<Author id: 1, book_id: 1, first_name: "Max", last_name: "Frisch", created_at: "2012-05-06 14:23:43", updated_at: "2012-05-06 14:23:43">] 
1.9.3p194 :003 > exit
MacBook:bookshelf xyz$
Ist das cool?! Man kann einfach durch die Pluralform des n-Model auf die Datensätze zugreifen. Die Ergebnismenge wird als Array zurückgegeben. Ob es auch andersherum geht?
MacBook:bookshelf xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Author.first
  Author Load (0.1ms)  SELECT "authors".* FROM "authors" LIMIT 1
 => #<Author id: 1, book_id: 1, first_name: "Max", last_name: "Frisch", created_at: "2012-05-06 14:23:43", updated_at: "2012-05-06 14:23:43"> 
1.9.3p194 :002 > Author.first.book
  Author Load (0.3ms)  SELECT "authors".* FROM "authors" LIMIT 1
  Book Load (0.2ms)  SELECT "books".* FROM "books" WHERE "books"."id" = 1 LIMIT 1
 => #<Book id: 1, title: "Homo faber", created_at: "2012-05-06 14:23:43", updated_at: "2012-05-06 14:23:43"> 
1.9.3p194 :003 > exit
MacBook:bookshelf xyz$
Bingo! Auch der Zugriff zur zugeordneten Book-Klasse ist ganz einfach. Und da es sich nur um einen einzigen Datensatz handelt (belongs_to), wird hier die Singularform genommen.

Anmerkung

Wenn es zu einem Buch keinen Autor geben würde, so wäre das Ergebnis ein leeres Array. Wenn zu einem Author kein Buch zugeordnet ist, dann gibt ActiveRecord als Book den Wert nil aus.

Datensätze suchen

Zum Suchen brauchen wir wieder definierte Beispieldaten. Deshalb füllen Sie bitte die Datei db/seeds.rb mit folgendem Inhalt:
# ruby encoding: utf-8

Book.create(:title => 'Homo faber').authors.create(:first_name => 'Max', :last_name => 'Frisch')
Book.create(:title => 'Der Besuch der alten Dame').authors.create(:first_name => 'Friedrich', :last_name => 'Dürrenmatt')
Book.create(:title => 'Julius Shulman: The Last Decade').authors.create([
  {:first_name => 'Thomas', :last_name => 'Schirmbock'},
  {:first_name => 'Julius', :last_name => 'Shulman'},
  {:first_name => 'Jürgen', :last_name => 'Nogai'}
  ])
Book.create(:title => 'Julius Shulman: Palm Springs').authors.create([
  {:first_name => 'Michael', :last_name => 'Stern'},
  {:first_name => 'Alan', :last_name => 'Hess'}
  ])
Book.create(:title => 'Photographing Architecture and Interiors').authors.create([
  {:first_name => 'Julius', :last_name => 'Shulman'},
  {:first_name => 'Richard', :last_name => 'Neutra'}
  ])
Book.create(:title => 'Der Zauberberg').authors.create(:first_name => 'Thomas', :last_name => 'Mann')
Book.create(:title => 'In einer Familie').authors.create(:first_name => 'Heinrich', :last_name => 'Mann')
Jetzt die Datenbank löschen und neu mit der db/seeds.rb befüllen:
MacBook:bookshelf xyz$ rake db:reset
[...]
MacBook:bookshelf xyz$
Dann legen wir mal los. Als Erstes schauen wir uns an, wie viele Bücher in der Datenbank sind:
MacBook:bookshelf xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Book.count
   (0.1ms)  SELECT COUNT(*) FROM "books" 
 => 7 
1.9.3p194 :002 >
Und wie viele Autoren?
1.9.3p194 :002 > Author.count
   (0.2ms)  SELECT COUNT(*) FROM "authors" 
 => 11 
1.9.3p194 :003 > exit
MacBook:bookshelf xyz$

joins

Wie können wir alle Bücher herausfinden, die mindestens einen Autoren mit dem Nachnamen 'Mann' haben? Das geht mit einem Join.[25] Damit können wir die beiden Models auch in der Suche verknüpfen:
MacBook:bookshelf xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Book.joins(:authors).where(:authors => {:last_name => 'Mann'})
  Book Load (0.2ms)  SELECT "books".* FROM "books" INNER JOIN "authors" ON "authors"."book_id" = "books"."id" WHERE "authors"."last_name" = 'Mann'
 => [#<Book id: 6, title: "Der Zauberberg", created_at: "2012-05-07 07:14:08", updated_at: "2012-05-07 07:14:08">, #<Book id: 7, title: "In einer Familie", created_at: "2012-05-07 07:14:08", updated_at: "2012-05-07 07:14:08">] 
1.9.3p194 :002 > Book.joins(:authors).where(:authors => {:last_name => 'Mann'}).count
   (0.4ms)  SELECT COUNT(*) FROM "books" INNER JOIN "authors" ON "authors"."book_id" = "books"."id" WHERE "authors"."last_name" = 'Mann'
 => 2 
1.9.3p194 :003 > exit
MacBook:bookshelf xyz$
Zwei Bücher mit einem Autoren 'Mann' sind in der Datenbank. Im SQL sehen Sie, das die Methode joins ein INNER JOIN ausführt.
Selbstverständlich können wir das Ganze auch umdrehen. Wir suchen uns den Autor des Buches 'Homo faber':
MacBook:bookshelf xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Author.joins(:book).where(:books => {:title => 'Homo faber'})
  Author Load (0.2ms)  SELECT "authors".* FROM "authors" INNER JOIN "books" ON "books"."id" = "authors"."book_id" WHERE "books"."title" = 'Homo faber'
 => [#<Author id: 1, book_id: 1, first_name: "Max", last_name: "Frisch", created_at: "2012-05-07 07:14:08", updated_at: "2012-05-07 07:14:08">] 
1.9.3p194 :002 > exit
MacBook:bookshelf xyz$

includes

includes ähnelt sehr der Methode joins (siehe „joins“). Auch damit kann man innerhalb einer 1:n-Verknüpfung suchen. Suchen wir noch mal alle Bücher mit einem Autor, der als Nachnamen 'Mann' hat:
MacBook:bookshelf xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Book.includes(:authors).where(:authors => {:last_name => 'Mann'})
  SQL (0.4ms)  SELECT "books"."id" AS t0_r0, "books"."title" AS t0_r1, "books"."created_at" AS t0_r2, "books"."updated_at" AS t0_r3, "authors"."id" AS t1_r0, "authors"."book_id" AS t1_r1, "authors"."first_name" AS t1_r2, "authors"."last_name" AS t1_r3, "authors"."created_at" AS t1_r4, "authors"."updated_at" AS t1_r5 FROM "books" LEFT OUTER JOIN "authors" ON "authors"."book_id" = "books"."id" WHERE "authors"."last_name" = 'Mann'
 => [#<Book id: 6, title: "Der Zauberberg", created_at: "2012-05-07 07:14:08", updated_at: "2012-05-07 07:14:08">, #<Book id: 7, title: "In einer Familie", created_at: "2012-05-07 07:14:08", updated_at: "2012-05-07 07:14:08">] 
1.9.3p194 :002 > exit
MacBook:bookshelf xyz$
Sie sehen in der Console-Ausgabe, dass der SQL-Code sich von der joins-Abfrage unterscheidet.
joins liest nur die Book-Datensätze ein und includes liest auch noch die dazugehörigen Authors. Schon bei unserem kleinen Beispiel sieht man, dass dies natürlich länger dauert (0.2 ms vs. 0.4 ms).
Warum kann der Einsatz von includes dann überhaupt Sinn machen? Wenn Sie schon bei der Abfrage wissen, dass Sie später alle Autorendaten benötigen, dann ist ein includes sinnvoll, weil dann nur eine Datenbankanfrage gestellt wird. ActiveRecord cach't nämlich die Antwort.
Wäre es dann nicht besser, immer mit includes zu arbeiten? Nein, es kommt immer auf den konkreten Fall an. Denn bei der Benutzung von includes werden ja initial viel mehr Daten transportiert. Diese müssen von Ruby gecached und verarbeitet werden. Die Verarbeitung dauert also länger und verbraucht mehr Resourcen.

delete und destroy

Mit den Methoden destroy, destroy_all, delete und delete_all kann man, wie in Abschnitt 4.12, „Einen Datensatz löschen“ beschrieben, Datensätze löschen. Im Kontext von has_many bedeutet das, dass man die zu einem Book gehörigen Author-Datensätze in einem Streich löschen kann:
MacBook:bookshelf xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > book = Book.find_by_title('Julius Shulman: The Last Decade')
  Book Load (0.3ms)  SELECT "books".* FROM "books" WHERE "books"."title" = 'Julius Shulman: The Last Decade' LIMIT 1
 => #<Book id: 3, title: "Julius Shulman: The Last Decade", created_at: "2012-05-07 07:36:41", updated_at: "2012-05-07 07:36:41"> 
1.9.3p194 :002 > book.authors
  Author Load (0.2ms)  SELECT "authors".* FROM "authors" WHERE "authors"."book_id" = 3
 => [#<Author id: 3, book_id: 3, first_name: "Thomas", last_name: "Schirmbock", created_at: "2012-05-07 07:36:41", updated_at: "2012-05-07 07:36:41">, #<Author id: 4, book_id: 3, first_name: "Julius", last_name: "Shulman", created_at: "2012-05-07 07:36:41", updated_at: "2012-05-07 07:36:41">, #<Author id: 5, book_id: 3, first_name: "Jürgen", last_name: "Nogai", created_at: "2012-05-07 07:36:41", updated_at: "2012-05-07 07:36:41">] 
1.9.3p194 :003 > book.authors.destroy_all
   (0.1ms)  begin transaction
  SQL (5.5ms)  DELETE FROM "authors" WHERE "authors"."id" = ?  [["id", 3]]
  SQL (0.0ms)  DELETE FROM "authors" WHERE "authors"."id" = ?  [["id", 4]]
  SQL (0.0ms)  DELETE FROM "authors" WHERE "authors"."id" = ?  [["id", 5]]
   (3.2ms)  commit transaction
 => [#<Author id: 3, book_id: 3, first_name: "Thomas", last_name: "Schirmbock", created_at: "2012-05-07 07:36:41", updated_at: "2012-05-07 07:36:41">, #<Author id: 4, book_id: 3, first_name: "Julius", last_name: "Shulman", created_at: "2012-05-07 07:36:41", updated_at: "2012-05-07 07:36:41">, #<Author id: 5, book_id: 3, first_name: "Jürgen", last_name: "Nogai", created_at: "2012-05-07 07:36:41", updated_at: "2012-05-07 07:36:41">] 
1.9.3p194 :004 > book.authors
 => [] 
1.9.3p194 :005 > exit
MacBook:bookshelf xyz$

Optionen

Ich will an dieser Stelle nicht auf alle möglichen Optionen eingehen. Aber ein paar sind so praktisch und werden von mir in jedem Projekt benutzt, dass ich sie hier vorstellen möchte. Für alle anderen verweise ich auf die Ruby-on-Rails-Doku, die Sie im Internet unter http://rails.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html bzw. auf Ihrem System auf der Shell mit ri ActiveRecord::Associations::ClassMethods aufrufen können.

belongs_to

Die meiner Meinung nach wichtigste Option für belongs_to ist:
  • :touch => :true
    Damit wird bei einer Veränderung eines Author automatisch das Feld updated_at des Eintrags in der Tabelle Book auf die aktuelle Uhrzeit gesetzt. Das sähe in der app/models/author.rb folgendermaßen aus:
    class Author < ActiveRecord::Base
      attr_accessible :book_id, :first_name, :last_name
    
      belongs_to :book, :touch => true
    end
Auch hier sollten Sie einmal auf die Liste aller möglichen Optionen einen kurzen Blick werfen. Dazu in der Shell ri ActiveRecord::Associations::ClassMethods#belongs_to aufrufen.

has_many

Die meiner Meinung nach wichtigsten Optionen für has_many:
  • :order => :last_name
    Wer die Autoren nach den Nachnamen sortieren will, kann dies mit folgender app/models/book.rb erreichen:
    class Book < ActiveRecord::Base
      attr_accessible :title
    
      has_many :authors, :order => :last_name
    end
    Erstellen wir als Beispiel ein neues Buch mit neuen Autoren und schauen uns an, wie ActiveRecord diese dann sortiert:
    MacBook:bookshelf xyz$ rails console
    Loading development environment (Rails 3.2.3)
    1.9.3p194 :001 > Book.create(:title => 'Test').authors.create([{:last_name => 'Z'}, {:last_name => 'A'}])
       (0.1ms)  begin transaction
      SQL (5.9ms)  INSERT INTO "books" ("created_at", "title", "updated_at") VALUES (?, ?, ?)  [["created_at", Mon, 07 May 2012 07:45:34 UTC +00:00], ["title", "Test"], ["updated_at", Mon, 07 May 2012 07:45:34 UTC +00:00]]
       (3.6ms)  commit transaction
       (0.1ms)  begin transaction
      SQL (0.7ms)  INSERT INTO "authors" ("book_id", "created_at", "first_name", "last_name", "updated_at") VALUES (?, ?, ?, ?, ?)  [["book_id", 8], ["created_at", Mon, 07 May 2012 07:45:34 UTC +00:00], ["first_name", nil], ["last_name", "Z"], ["updated_at", Mon, 07 May 2012 07:45:34 UTC +00:00]]
       (1.1ms)  commit transaction
       (0.1ms)  begin transaction
      SQL (0.4ms)  INSERT INTO "authors" ("book_id", "created_at", "first_name", "last_name", "updated_at") VALUES (?, ?, ?, ?, ?)  [["book_id", 8], ["created_at", Mon, 07 May 2012 07:45:34 UTC +00:00], ["first_name", nil], ["last_name", "A"], ["updated_at", Mon, 07 May 2012 07:45:34 UTC +00:00]]
       (0.9ms)  commit transaction
     => [#<Author id: 12, book_id: 8, first_name: nil, last_name: "Z", created_at: "2012-05-07 07:45:34", updated_at: "2012-05-07 07:45:34">, #<Author id: 13, book_id: 8, first_name: nil, last_name: "A", created_at: "2012-05-07 07:45:34", updated_at: "2012-05-07 07:45:34">] 
    1.9.3p194 :002 > Book.last.authors                                                Book Load (0.4ms)  SELECT "books".* FROM "books" ORDER BY "books"."id" DESC LIMIT 1
      Author Load (0.4ms)  SELECT "authors".* FROM "authors" WHERE "authors"."book_id" = 8 ORDER BY last_name
     => [#<Author id: 13, book_id: 8, first_name: nil, last_name: "A", created_at: "2012-05-07 07:45:34", updated_at: "2012-05-07 07:45:34">, #<Author id: 12, book_id: 8, first_name: nil, last_name: "Z", created_at: "2012-05-07 07:45:34", updated_at: "2012-05-07 07:45:34">] 
    1.9.3p194 :003 > exit
    MacBook:bookshelf xyz$ 
    Und falls umgekehrt sortiert werden soll:
    has_many :authors, :order => 'title DESC'
  • :dependent => :destroy
    Wird ein Buch gelöscht, dann ist es oft sinnvoll, dass auch alle von diesem Buch abhängigen (dependent) Autoren automatisch mitgelöscht werden. Das kann mit :dependent => :destroy in der app/models/book.rb realisiert werden:
    class Book < ActiveRecord::Base
      attr_accessible :title
    
      has_many :authors, :dependent => :destroy
    end
    Im folgenden Beispiel löschen wir das erste Buch in der Datenbank-Tabelle. Dabei werden automatisch alle Autoren dieses Buches ebenfalls gelöscht:
    MacBook:bookshelf xyz$ rails console
    Loading development environment (Rails 3.2.3)
    1.9.3p194 :001 > Book.first
      Book Load (0.2ms)  SELECT "books".* FROM "books" LIMIT 1
     => #<Book id: 1, title: "Homo faber", created_at: "2012-05-07 07:44:53", updated_at: "2012-05-07 07:44:53"> 
    1.9.3p194 :002 > Book.first.authors
      Book Load (0.3ms)  SELECT "books".* FROM "books" LIMIT 1
      Author Load (0.2ms)  SELECT "authors".* FROM "authors" WHERE "authors"."book_id" = 1
     => [#<Author id: 1, book_id: 1, first_name: "Max", last_name: "Frisch", created_at: "2012-05-07 07:44:53", updated_at: "2012-05-07 07:44:53">] 
    1.9.3p194 :003 > Book.first.destroy
      Book Load (0.3ms)  SELECT "books".* FROM "books" LIMIT 1
       (0.1ms)  begin transaction
      Author Load (0.2ms)  SELECT "authors".* FROM "authors" WHERE "authors"."book_id" = 1
      SQL (4.6ms)  DELETE FROM "authors" WHERE "authors"."id" = ?  [["id", 1]]
      SQL (0.2ms)  DELETE FROM "books" WHERE "books"."id" = ?  [["id", 1]]
       (3.8ms)  commit transaction
     => #<Book id: 1, title: "Homo faber", created_at: "2012-05-07 07:44:53", updated_at: "2012-05-07 07:44:53"> 
    1.9.3p194 :004 > Author.exists?(1)
      Author Exists (0.2ms)  SELECT 1 FROM "authors" WHERE "authors"."id" = 1 LIMIT 1
     => false 
    1.9.3p194 :005 > exit
    MacBook:bookshelf xyz$

    Wichtig

    Bitte denken Sie immer an den Unterschied zwischen den Methoden destroy (siehe „destroy“) und delete (siehe „delete“). Diese Abhängigkeit funktioniert nur mit der Methode destroy.
  • :has_many .. :through
    Hier muss ich etwas weiter ausholen: Bei unserem Buch-Autoren-Beispiel wird Ihnen aufgefallen sein, dass wir Autoren im Zweifelsfall mehrfach in der authors-Tabelle eintragen. Im Sinne einer guten Normalisierung ist das nicht. Es wäre schöner, jeden Autor nur einmal in der authors-Tabelle einzutragen und die Verknüpfung mit den Büchern über eine Zwischentabelle zu regeln. Genau dafür gibt es has_many , :through => .
    Fangen wir bei null mit einer neuen Rails-Applikation an:
    MacBook:~ xyz$ rails new bookshelf2
    [...]
    MacBook:~ xyz$ cd bookshelf2
    MacBook:bookshelf2 xyz$
    Jetzt legen wir wieder die Modelle für Book und Author an. Beim Author lassen wir aber die book_id weg:
    MacBook:bookshelf2 xyz$ rails generate model book title
          invoke  active_record
          create    db/migrate/20120507085137_create_books.rb
          create    app/models/book.rb
          invoke    test_unit
          create      test/unit/book_test.rb
          create      test/fixtures/books.yml
    MacBook:bookshelf2 xyz$ rails generate model author first_name last_name
          invoke  active_record
          create    db/migrate/20120507085158_create_authors.rb
          create    app/models/author.rb
          invoke    test_unit
          create      test/unit/author_test.rb
          create      test/fixtures/authors.yml
    MacBook:bookshelf2 xyz$
    Zum Schluss erstellen wir ein Model Authorship, das ein Feld book_id und ein Feld author_id enthält:
    MacBook:bookshelf2 xyz$ rails generate model authorship book_id:integer author_id:integer
          invoke  active_record
          create    db/migrate/20120507085358_create_authorships.rb
          create    app/models/authorship.rb
          invoke    test_unit
          create      test/unit/authorship_test.rb
          create      test/fixtures/authorships.yml
    MacBook:bookshelf2 xyz$
    Danach ein rake db:migrate:
    MacBook:bookshelf2 xyz$ rake db:migrate
    [...]
    MacBook:bookshelf2 xyz$ 
    Authorship verknüpfen wir per belongs_to mit Book und Author in der Datei app/models/authorship.rb:
    class Authorship < ActiveRecord::Base
      attr_accessible :author_id, :book_id
    
      belongs_to :author
      belongs_to :book
    end
    In der app/models/book.rb verknüpfen wir jetzt mit has_many die books mit den authorships und danach mit has_many :authors, :through => :authorships die authors über die authoships mit den books:
    class Book < ActiveRecord::Base
      attr_accessible :title
    
      has_many :authorships
      has_many :authors, :through => :authorships
    end
    Um den umgekehrten Weg auch gehen zu können, fügen wir in der app/models/author.rb das gleiche Konstrukt ein:
    class Author < ActiveRecord::Base
      attr_accessible :first_name, :last_name
    
      has_many :authorships
      has_many :books, :through => :authorships
    end
    
    Jetzt legen wir ein Buch an:
    MacBook:bookshelf2 xyz$ rails console
    Loading development environment (Rails 3.2.3)
    1.9.3p194 :001 > book = Book.create(:title => 'Homo faber')
       (0.1ms)  begin transaction
      SQL (4.8ms)  INSERT INTO "books" ("created_at", "title", "updated_at") VALUES (?, ?, ?)  [["created_at", Mon, 07 May 2012 09:34:34 UTC +00:00], ["title", "Homo faber"], ["updated_at", Mon, 07 May 2012 09:34:34 UTC +00:00]]
       (3.8ms)  commit transaction
     => #<Book id: 1, title: "Homo faber", created_at: "2012-05-07 09:34:34", updated_at: "2012-05-07 09:34:34"> 
    1.9.3p194 :002 > 
    Dieses Buch hat noch keinen Autor:
    1.9.3p194 :002 > book.authors
      Author Load (0.2ms)  SELECT "authors".* FROM "authors" INNER JOIN "authorships" ON "authors"."id" = "authorships"."author_id" WHERE "authorships"."book_id" = 1
     => [] 
    1.9.3p194 :003 > 
    Mit den Methoden build oder create kann ich einen Autor für dieses Buch anlegen:
    1.9.3p194 :003 > book.authors.create(:first_name => 'Max', :last_name => 'Frisch')
       (0.1ms)  begin transaction
      SQL (0.7ms)  INSERT INTO "authors" ("created_at", "first_name", "last_name", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", Mon, 07 May 2012 09:36:33 UTC +00:00], ["first_name", "Max"], ["last_name", "Frisch"], ["updated_at", Mon, 07 May 2012 09:36:33 UTC +00:00]]
      SQL (0.4ms)  INSERT INTO "authorships" ("author_id", "book_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["author_id", 1], ["book_id", 1], ["created_at", Mon, 07 May 2012 09:36:33 UTC +00:00], ["updated_at", Mon, 07 May 2012 09:36:33 UTC +00:00]]
       (2.6ms)  commit transaction
     => #<Author id: 1, first_name: "Max", last_name: "Frisch", created_at: "2012-05-07 09:36:33", updated_at: "2012-05-07 09:36:33"> 
    1.9.3p194 :004 > 
    Jetzt haben wir folgende Einträge in den verschiedenen Datenbank-Tabellen:
    1.9.3p194 :004 > Book.all
      Book Load (0.3ms)  SELECT "books".* FROM "books" 
     => [#<Book id: 1, title: "Homo faber", created_at: "2012-05-07 09:34:34", updated_at: "2012-05-07 09:34:34">] 
    1.9.3p194 :005 > Author.all
      Author Load (0.3ms)  SELECT "authors".* FROM "authors" 
     => [#<Author id: 1, first_name: "Max", last_name: "Frisch", created_at: "2012-05-07 09:36:33", updated_at: "2012-05-07 09:36:33">] 
    1.9.3p194 :006 > Authorship.all
      Authorship Load (0.3ms)  SELECT "authorships".* FROM "authorships" 
     => [#<Authorship id: 1, book_id: 1, author_id: 1, created_at: "2012-05-07 09:36:33", updated_at: "2012-05-07 09:36:33">] 
    1.9.3p194 :007 >
    ActiveRecord hat automatisch die Tabelle authorships mit den Verknüpfungen vom Book zum Author gefüllt.
    Wir können uns mit Book.first.authors alle Autoren des ersten Buches anzeigen lassen:
    1.9.3p194 :007 > Book.first.authors
      Book Load (0.2ms)  SELECT "books".* FROM "books" LIMIT 1
      Author Load (0.2ms)  SELECT "authors".* FROM "authors" INNER JOIN "authorships" ON "authors"."id" = "authorships"."author_id" WHERE "authorships"."book_id" = 1
     => [#<Author id: 1, first_name: "Max", last_name: "Frisch", created_at: "2012-05-07 09:36:33", updated_at: "2012-05-07 09:36:33">] 
    1.9.3p194 :008 >
    Manchmal ist das create- und build-Konstrukt etwas umständlich. Für diesen Fall können Sie auch einzelne Objekte oder ein Array von Objekten mit der Methode << verknüpfen. Wenn wir z. B. die in diesem Kapitel benutzte db/seeds.rb auf die von uns jetzt erstellte has_many ... through-Verknüpfung umstellen wollen, so sähe die Datei so aus:
    # ruby encoding: utf-8
    
    Book.create(:title => 'Homo faber').authors << Author.find_or_create_by_first_name_and_last_name('Max', 'Frisch')
    Book.create(:title => 'Der Besuch der alten Dame').authors << Author.find_or_create_by_first_name_and_last_name('Friedrich', 'Dürrenmatt')
    Book.create(:title => 'Julius Shulman: The Last Decade').authors  << [
      Author.find_or_create_by_first_name_and_last_name('Thomas', 'Schirmbock'),
      Author.find_or_create_by_first_name_and_last_name('Julius', 'Shulman'),
      Author.find_or_create_by_first_name_and_last_name('Jürgen', 'Nogai')
      ]
    Book.create(:title => 'Julius Shulman: Palm Springs').authors << [
      Author.find_or_create_by_first_name_and_last_name('Michael', 'Stern'),
      Author.find_or_create_by_first_name_and_last_name('Alan', 'Hess')
      ]
    Book.create(:title => 'Photographing Architecture and Interiors').authors << [
      Author.find_or_create_by_first_name_and_last_name('Julius', 'Shulman'),
      Author.find_or_create_by_first_name_and_last_name('Richard', 'Neutra')
      ]
    Book.create(:title => 'Der Zauberberg').authors << Author.find_or_create_by_first_name_and_last_name('Thomas', 'Mann')
    Book.create(:title => 'In einer Familie').authors << Author.find_or_create_by_first_name_and_last_name('Heinrich', 'Mann')
    Durch die Benutzung von find_or_create können wir hier sicherstellen, dass ein Autor nicht zweimal angelegt wird.
    has_many ... through ist ein sehr kraftvolles Tool in der Hand eines guten Programmierers. Ein weiteres Beispiel zur Benutzung von has_many ... through finden Sie in Abschnitt 4.9, „Many-to-Many – n:n-Verknüpfung“.
Es gibt natürlich noch eine Menge anderer Optionen. Diese können Sie sich mit ri ActiveRecord::Associations::ClassMethods#has_many auf der Shell ausgeben lassen.


[25] Falls Sie sich für den theoretischen Hintergrund zu Joins interessieren, finden Sie weitere Informationen unter: http://de.wikipedia.org/wiki/SQL#Abfrage_mit_verkn.C3.BCpften_Tabellen, http://en.wikipedia.org/wiki/Join_(SQL), http://de.wikipedia.org/wiki/Relationale_Algebra#Join.