10.10 Assoziationen 

Eins-zu-viele-Assoziationen (1:n) 

Meistverwendete Assoziation
Die Eins-zu-viele-, oder auch 1:n-Assoziationen (oder auch Relationen) sind die meistverwendete Assoziation. Hierbei steht ein Objekt einer Klasse A in Beziehung zu »beliebig vielen« Objekten einer Klasse B, und ein Objekt der Klasse B steht zu einem Objekt der Klasse A in Beziehung. Man kann auch sagen, dass ein Objekt der Klasse A viele Objekte der Klasse B hat und ein Objekt der Klasse B zu einem Objekt der Klasse A gehört .
Die Eins-zu-viele-Assoziation wird in Diagrammen (UML) oft mit einer Verbindungslinie mit den Bezeichnungen 1 und * dargestellt. Um zu verdeutlichen, dass ein Objekt höchstens eine Beziehung hat, wird statt 1 auch 0..1 geschrieben.
Abbildung 1:n-Assoziation
In der Praxis gibt es zahlreiche Beispiele:
- Ein Land hat viele Flughäfen, und ein Flughafen gehört zu einem Land.
- Eine Wohnung hat viele Möbel, aber ein Möbelstück gehört zu einer Wohnung.
- Ein Sonnensystem hat viele Planeten, und ein Planet gehört zu einem Sonnensystem.
- Ein Mensch hat viele Organe, und ein Organ gehört zu einem Menschen.
- Ein Kunde hat viele Bestellungen, und eine Bestellung gehört zu einem Kunden.
Mit »beliebig vielen« ist auch keinmal eingeschlossen, z. B. kann ein Kunde auch keine Bestellung haben.
Das folgende Diagramm zeigt die Assoziation zwischen der Klasse Country und der Klasse Airport aus unserer Beispielapplikation railsair (siehe Kapitel 6).
Abbildung Country-Airport-Assoziation
Zwei Tabellen
Zur Implementierung einer Eins-zu-viele-Assoziation in einer relationalen Datenbank werden zwei Tabellen benötigt. Eine Tabelle (die »Viele«-Tabelle) enthält einen sogenannten Fremdschlüssel , der einen Verweis auf einen Datensatz einer anderen (oder der gleichen) Tabelle (die »Eins«-Tabelle) darstellt.
Theoretisch ist ein Fremdschlüssel ein Tabellenfeld, das den Wert eines Primärschlüssels enthält, zu dem die Assoziation hergestellt wird. Ein Primärschlüssel ist ein Tabellenfeld, das jeden Datensatz der Tabelle eindeutig identifiziert.
Konventionen
Das Framework ActiveRecord hat folgende wichtige Konventionen:
- Primärschlüssel sind ganzzahlige Felder mit dem Namen id .
- Der Name des Fremdschlüssels setzt sich aus dem Namen der Tabelle, auf die er verweist, und der Bezeichnung id zusammen, wie z. B. country_id .
- Tabellen werden anders als Klassen im Plural benannt.
Ein Land (Tabelle countries) hat z. B. viele Flughäfen (Tabelle airports). Die Tabelle airports enthält deshalb den Fremdschlüssel country_id.
Abbildung Country-Airport-Assoziation
Erstellung der Tabellen und Model-Klassen
Model-Generator
Für die Erstellung der Models können wir den model -Generator verwenden. Alternativ kann auch der scaffold -Generator verwendet werden, der zusätzlich auch die zugehörigen Controller und Views erzeugt. Die Angabe der Felder für die Tabelle ist optional. Sie können die Felder auch manuell in der Migration-Datei definieren.
Im Folgenden verwenden wir den model -Generator. Als Parameter übergeben wir den Namen des Models und die Tabellenfelder-Datentyp-Paare.
Listing Erstellung des Country-Models
ruby script/generate model country code:string name:string
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/country.rb
create test/unit/country_test.rb
create test/fixtures/countries.yml
create db/migrate
create db/migrate/001_create_countries.rb
Listing Erstellung des Airport-Models
ruby script/generate model airport code:string name:string \
country_id:integer
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/airport.rb
create test/unit/airport_test.rb
create test/fixtures/airports.yml
exists db/migrate
create db/migrate/002_create_airports.rb
Migration
Neben den Klassen und Test-Klassen werden die folgenden Migration-Dateien generiert, die wir vor der Ausführung noch anpassen können.
Listing Migrations für countries und airports
# Migration 001_create_countries class CreateCountries < ActiveRecord::Migration def self.up create_table :countries do |t| t.string :code t.string :name t.timestamps end end def self.down drop_table :countries end end # Migration 002_create_airports class CreateAirports < ActiveRecord::Migration def self.up create_table :airports do |t| t.string :code t.string :name t.integer :country_id t.timestamps end end def self.down drop_table :airports end end
rake db:migrate
Die Tabellen countries und airports werden in der Datenbank erstellt, wenn die Migration-Dateien mit dem Rake-Task rake db:migrate ausgeführt werden.
Test in der Konsole
Nachdem die Migrations ausgeführt und die Tabellen angelegt wurden, können die Model-Klassen (z. B. in der Konsole) verwendet werden. Im folgenden Code-Beispiel wird das Land Germany angelegt und ihm zwei Flughäfen zugeordnet. Da wir uns auch um das Setzen der Fremdschlüssel kümmern müssen, ist das sehr umständlich.
# Erstellen von Objekten germany = Country.create :code=>'DE', :name=>'Germany' air_muc = Airport.create :code=>'MUC', :name=>'Munich' air_dus = Airport.create :code=>'DUS', :name=>'Düsseldorf' # Zuordnung air_muc.country_id = germany.id air_dus.country_id = germany.id air_muc.save air_dus.save # Suchen nach allen deutschen Flughäfen: german_airports = Airport.find(:all, :conditions=>{:countr_id=>germany.id})
Auch die Suche muss mit Hilfe von :conditions erfolgen und ist deshalb auch sehr umständlich. Aber wie Sie sicher schon vermutet haben, stellt Rails dazu eine sehr praktische Lösung zur Verfügung, die wir Ihnen im nächsten Abschnitt vorstellen.
Definieren der Assoziation in den Models
Damit ActiveRecord sich um die Fremdschlüssel automatisch kümmert, muss in den Model-Klassen in Form einer Deklaration angegeben werden, wie und mit welcher anderen Klasse die Klasse in Relation steht.
has_many, belongs_to
Um eine Eins-zu-viele-Assoziation abzubilden, werden die Methoden has_many (»hat viele«) und belongs_to (»gehört zu«) verwendet. In der Klasse mit dem Fremdschlüssel (in unserem Beispiel die Klasse Airport) wird die belongs_to -Methode verwendet, und in der Klasse, zu der die Assoziation besteht (in unserem Beispiel die Klasse Country), wird die has_many -Methode verwendet:
Listing Model-Klassen Country und Airport
# Datei app/models/country.rb class Country < ActiveRecord::Base has_many :airports end # Datei app/models/airport.rb class Airport < ActiveRecord::Base belongs_to :country end
Unser umständliches Beispiel von eben kann nun vereinfacht werden:
# Erstellen von Objekten germany = Country.create :code=>'DE', :name=>'Germany' air_muc = Airport.new :code=>'MUC', :name=>'Munich' air_dus = Airport.new :code=>'DUS', :name=>'Düsseldorf' # Zuordnung germany.airports << air_muc germany.airports << air_dus # Suchen nach allen deutschen Flughäfen. germany.airports.find(:all)
Zugeordnete Objekte werden automatisch gespeichert |
Die Airport-Objekte haben wir nur mit der Methode new erstellt und nicht gespeichert. Bei der Zuordnung mit +<<+ werden die Fremdschlüssel der Airport-Objekte gesetzt und die Objekte automatisch gespeichert. Voraussetzung dafür ist, dass das Country-Objekt bereits gespeichert wurde. Hätten wir die Airport-Objekte mit der create -Methode erstellt, hätte die Zuordnung zu einem erneuten Speichern geführt, was zwar kein Problem darstellen würde, jedoch für die Performance nicht von Vorteil ist. |
Löschen
Wenn zu einem Objekt (z. B. dem Country-Objekt germany) mehrere Objekte (z. B. die Airport-Objekte dus und muc) in Relation stehen, dann stellt sich die Frage, was passiert, wenn das Country-Objekt (germany) gelöscht wird.
:dependent
Dies hängt von der Option :dependent der has_many -Methode ab, die die Werte :nullify (Standardeinstellung), :destroy oder delete_all annehmen kann.
- :dependent => :nullify (Standard)
Wenn diese Option gesetzt ist, werden beim Löschen eines Objektes (z. B. germany) die assoziierten Objekte nicht gelöscht, sondern es wird lediglich der Eintrag der ID-Nummer aus dem Fremdschlüssel entfernt.# app/models/country.rb class Country < ActiveRecord::Base has_many :airports end # Beispielaufruf in der script/console germany = Country.create(:code=>'DE', :name=>'Germany') gemany.airports << Airport.new(:code=>'MUC', :name=>'Munich') Airport.find_by_code('DE').id # => 1 germany.destroy Airport.find_by_code('DE').id # => nil
- :dependent => :destroy
In diesem Fall werden alle assoziierten Objekte gelöscht, indem auf jedem assoziierten Objekt, die destroy -Methode aufgerufen wird:# app/models/country.rb class Country < ActiveRecord::Base has_many :airports, :dependent => :destroy end # Beispielaufruf in der script/console germany = Country.create(:code=>'DE', :name=>'Germany') gemany.airports << Airport.new(:code=>'MUC', :name=>'Munich') germany.destroy Airport.find_by_code('DE').nil? # => true
- :delete_all
Diese Option löscht auch wie bei der Option :destroy die assoziierten Objekte, jedoch direkt mit dem SQL-Befehl DELETE, ohne dass die destroy -Methode auf den assoziierten Objekten aufgerufen wird. Dies geht zwar in der Regel schneller, aber es werden dann gegebenenfalls weitere Abhängigkeiten der assoziierten Objekte nicht berücksichtigt.
Im nächsten Abschnitt wird detailliert gezeigt, was der Aufruf von belongs_to und has_many hinter den Kulissen von Rails bewirkt.
Hinzugefügte Methoden von belongs_to
belongs_to
Durch die Deklaration bzw. den Aufruf von belongs_to :country werden nachfolgende Methoden dynamisch der Klasse Airport hinzugefügt. Dies ist wegen der Flexibilität von Ruby möglich:
- country
Liefert das assoziierte Objekt, in diesem Fall das Land, das dem Flughafen zugeordnet ist. Wenn true übergeben wird, wird ActiveRecord angewiesen, das assoziierte Objekt erneut zu laden. Dies ist sinnvoll, wenn sichergestellt werden soll, dass die aktuellen Daten geladen werden.airport = Airport.find(1) puts airport.country.name
- country=
Hiermit kann die Assoziation festgelegt werden. In diesem Fall wird das Land des Flughafens bestimmt.dus = Airport.new dus.name = "Airport Düsseldorf" dus.country = Country.find_by_code('DE')
- build_country
Hiermit wird direkt ein neues assoziiertes Objekt erstellt, jedoch noch nicht gespeichert.airport = Airport.find(12) germany = airport.build_country(:name => "Germany") germany.save
- create_country
Hiermit wird direkt ein neues assoziiertes Objekt erstellt und auch gespeichert.airport = Airport.find(12) germany = airport.create_country(:name => "Germany")
- country.nil?
Liefert true zurück, wenn es kein passendes assoziiertes Objekt gibt.fra = Airport.create(:name => "Frankfurt a.M. Airport") fra.country.nil? # => true
Hinzugefügte Methoden von has_many
has_many
Durch die Deklaration bzw. den Aufruf von has_many :airports werden folgende Methoden der Klasse Country hinzugefügt:
- airports
Liefert das Array aller assoziierten Objekte. In diesem Fall werden alle Flughäfen zu dem Land zurückgeliefert.country = Country.find_by_DE("DE") puts "Folgende Flughäfen hat Deutschland:" country.airports.each do |airport| puts airport.name end
- airports <<
Fügt ein oder mehrere Objekte zu der Assoziation hinzu. Im folgenden Beispiel werden Flughäfen dem Land zugeordnet:airport1 = Airport.create(:code => "DUS") airport2 = Airport.create(:code => "MUC") country = Country.find_by_code("DE") country.airports << airport1 country.airports << airport2 # oder in einer Zeile: country.airports << [airport1, airport2]
- airports.create
Mit der Methode create auf dem assoziierten Objekt kann gleichzeitig das Objekt erstellt und zugeordnet werden. Als Ergebnis wird das erstellte Objekt zurückgeliefert:berlin = germany.airports.create(:code=>'SXF', :name=>'Berlin Schoenefeld') => #<Airport id: 4, code: "SXF", name: "Berlin Schoenefeld", country_id: 1, created_at: "2008-01-23 22:03:25", updated_at: "2008-01-23 22:03:25»
- airports.build
Wie create
Die Methode build funktioniert wie die create -Methode, nur dass das Objekt nicht gespeichert wird.
- airports=
Vorsicht, denn hier werden alle bereits assoziierten Objekte entfernt und die angegebenen Objekte (als Array) neu zugeordnet.germany = Country.find_by_code(:code=>'DE') germany.airports.size # => 2 germany.airports = [Airport.new(:code=>'TXL')] germany.airports.size # => 1
- airports.find
Die find -Methode gibt alle assozierten Objekte zurück, die dem angegebenen Suchkriterium entsprechen (siehe Abschnitt 10.6).germany = Country.find_by_code("DE") # Suche nach allen deutschen Flughäfen, deren Name # mit 'B' beginnt germany.airports.find(:all, :conditions=>"name like 'B%'")
- airports.size
Die Methode liefert die Anzahl der assoziierten Objekte (airports) zurück.country = Country.find_by_code("DE") country.airports.size # => 2
- airports.emtpy?
Liefert true, wenn es keine assoziierten Objekte (airports) gibt.france = Country.create(:code=>'FR') france.airports.empty? # => true
- airports.delete
Die Methode entfernt die Assoziation zu den assoziierten Objekten (airports), d. h., die Einträge der Fremdschlüssel (country_id) werden auf leer (NULL) gesetzt. Die assoziierten Objekte (airports) werden jedoch nicht gelöscht, außer die Methode belongs_to wird mit der Option :dependent=>:destroy aufgerufen.country = Country.find_by_code("DE") country.airports.size # => 2 country.airports.delete country.airports.size # => 0
Eins-zu-eins-Assoziationen (1:1) 

Höchstens eins
Eine Eins-zu-eins(1:1)-Assoziation ist genau genommen eine Eins-zu-viele-Assoziation, bei der die Mengenangabe »viele« auf höchstens eins reduziert ist.
- Ein Passagier hat (höchstens) eine Bonuskarte (einer Fluggesellschaft).
- Eine Firma hat (höchstens) einen Vorstand.
- Ein Land hat (höchstens) einen Regierungschef.
Im folgenden Beispiel wird jedem Passagier höchstens eine Bonuskarte zugeordnet. Das bedeutet, dass ein Passagier entweder keine oder eine Bonuskarte besitzt.
Abbildung Klassendiagramm einer 1:1-Assoziation
In einer der beiden Tabellen wird wie bei einer Eins-zu-viele-Assoziation ein Fremdschlüssel benötigt. In unserem Beispiel wird der Fremdschlüssel passenger_id in der Tabelle bonus_cards angelegt.
Abbildung Tabellen passengers und bonus_cards
Erstellung der Tabellen und Model-Klassen
Model-Generator
Zu jeder Tabelle gibt es eine Model-Klasse. Zur Generierung der Tabellen und Model-Klassen kann auch hier der model -Generator verwendet werden:
Listing Generierung der Model-Klassen
ruby script/generate model Passenger firstname:string \ lastname:string ruby script/generate model BonusCard points:integer \ passenger_id:integer
rake db:migrate
Vor Ausführung der Migration-Dateien mit dem Befehl rake db:migrate können Sie bei Bedarf die Migration-Dateien vorher anpassen, um z. B. weitere Felder hinzuzufügen.
Listing Ausführen der Migrations
rake db:migrate == 1 CreatePassengers: migrating ================== -- create_table(:passengers) -> 0.0029s == 1 CreatePassengers: migrated (0.0030s) ========= == 2 CreateBonusCards: migrating ================== -- create_table(:bonus_cards) -> 0.0029s == 2 CreateBonusCards: migrated (0.0031s) =========
Definieren der Assoziationen in den Models
has_one, belongs_to
Die Definition der Assoziation erfolgt bei einer Eins-zu-eins-Assoziation mit den Methoden has_one und belongs_to . In der Model-Klasse, deren Tabelle den Fremdschlüssel enthält, wird die Methode belongs_to verwendet. In der Passenger-Tabelle verwenden wir die Methode has_one mit der Option :dependent=>:destroy, da beim Löschen eines Passagiers aus der Datenbank auch die zugehörige BonusCard gelöscht werden soll.
Listing Model-Klassen Passenger und BonusCard
# Datei app/models/passenger.rb class Passenger < ActiveRecord::Base has_one :bonus_card, :dependent => :destroy end # Datei app/models/bonus_card.rb class BonusCard < ActiveRecord::Base belongs_to :passenger end
Daten zuordnen
Im folgenden Beispiel wird gezeigt, wie einem Passagier eine Bonuskarte zugeordnet wird.
# Anlegen eines Passagiers lee = Passenger.create(:firstname=>'Lee', :lastname=>'Adama') # Überprüfen, ob Lee keine BonusCard hat lee.bonus_card.nil? # => true # Anlegen einer BonusCard card = BonusCard.new(:points=>100) # Zuweisen der BonusCard # Es wird automatisch der Fremdschlüssel gesetzt # und das Objekt card gespeichert. lee.bonus_card = card # Überprüfen, ob Lee eine BonusCard hat lee.bonus_card.nil? # => false # Abfrage der Punkte puts lee.bonus_card.points => 100
BonusCard-Objekt
Um ein BonusCard-Objekt zu erstellen und gleichzeitig einem Passagier zuzuordnen, können die Methoden build_bonus_card und create_bonus_card verwendet werden. Die erste Methode erstellt ein BonusCard-Objekt, ohne es zu speichern, und die zweite Methode speichert das Objekt in der Datenbank.
# Anlegen eines Passagiers william = Passenger.create(:firstname=>'William', \ :lastname=>'Adama') # Anlegen der BonusCard für den Passagier William william.create_bonus_card(:points => 200) # Abfrage der Punkte puts william.bonus_card.points # => 200
Viele-zu-viele-Assoziationen (n:m) 

Bei der Viele-zu-viele-Assoziation, oder auch n:m-Assoziation (gelesen »n zu m«), steht ein Objekt einer Klasse A in Beziehung zu »beliebig vielen« Objekten einer Klasse B, und ein Objekt der Klasse B wiederum steht in Beziehung zu »beliebig vielen« Objekten der Klasse A. Die Mengenangabe »beliebig viele« schließt auch keinmal ein.
Abbildung n:m-Assoziation
Beispiele für Viele-zu-viele-Assoziationen sind:
- Ein Produkt gehört zu vielen Kategorien, und eine Kategorie hat viele Produkte.
- Ein Flug hat viele Passagiere, und eine Passagier ist öfter geflogen.
- Eine Playlist eines MP3-Players hat viele Songs, und ein Song kann in mehreren Playlisten vorkommen.
Join-Tabelle
Für die Umsetzung einer Viele-zu-viele-Assoziation in einem relationalen Datenbanksystem wird neben den Tabellen für die Objekte der Klasse A und der Klasse B noch eine dritte Tabelle benötigt, die die Fremdschlüssel enthält, um die Assoziation herzustellen. Diese Tabelle wird auch als Join-Tabelle bezeichnet.
Es gibt es zwei Techniken, eine Viele-zu-viele-Assoziation zu erstellen:
- Join-Tabelle ohne ein Model
Die Join-Tabelle wird nur für die Verwaltung der Assoziation verwendet. Sie enthält neben den Fremdschlüsseln keine weiteren Felder. Es wird keine Model-Klasse für diese Join-Tabelle angelegt.
- Join-Tabelle mit einem Model
Die Join-Tabelle enthält nicht nur die Fremdschlüssel, sondern auch weitere Felder. Die Join-Tabelle wird einem Model zugeordnet. Somit ist die Join-Tabelle mehr als eine Hilfstabelle.
Join-Tabelle ohne ein Model
Namenskonvention
Im Folgenden sollen Flüge Passagieren zugeordnet werden. Wenn ein Passagier an einem Flug teilnimmt, so wird der Passagier diesem Flug zugeordnet. Für die Zuordnung wird eine Tabelle mit den Fremdschlüsseln flight_id und passenger_id erstellt. Die Benennung der Tabelle muss der Konvention folgen: Name der beiden Tabellen, die mit einem Unterstrich verbunden werden. Wichtig ist, dass die Tabellen in aufsteigender alphabetischer Reihenfolge benannt werden. In unserem Beispiel also flights_passengers.
Abbildung n:m-Assoziation
Erstellung der Tabellen und Models
Zu den Tabellen flights und passengers werden Model-Klassen erstellt. Die Join-Tabelle wird keinem Model zugeordnet. Diese Tabelle wird bei der Programmierung mit ActiveRecord nicht direkt angesprochen. Das Framework kümmert sich um die Verwaltung der Tabellen.
Für die Erstellung der Model-Klassen Flight und Passenger kann der model -Generator eingesetzt werden, der auch die Migration-Dateien generiert, mit denen die entsprechenden Datenbanktabellen erstellt werden.
Migration- Generator
Zur Erstellung der Join-Tabelle wird der migration -Generator eingesetzt.
ruby script/generate migration CreateJoinFlightsPassengers exists db/migrate create db/migrate/005_create_join_flights_passengers.rb
Die generierte Migration-Datei ist bis auf die Klassen- und Methoden-Deklarationen noch leer:
Listing db/migrate/005_create_join_flights_passengers.rb
class CreateJoinFlightsPassengers < ActiveRecord::Migration def self.up end def self.down end end
Wir ergänzen die Migration-Datei wie folgt:
Listing db/migrate/005_create_join_flights_passengers.rb
class CreateJoinFlightsPassengers < ActiveRecord::Migration def self.up create_table :flights_passengers, :id => false do |t| t.integer :flight_id t.integer :passenger_id # ggf. noch weitere Felder definieren end end def self.down drop_table :flights_passgengers end end
:id => false
Mit dem Parameter :id => false wird angegeben, dass kein ID-Feld (Primärschlüssel) generiert werden soll. Da zu der Join-Tabelle keine Model-Klasse erstellt wird, ist dies nicht notwendig.
Definieren der Assoziation in den Models
Bei einer Viele-zu-viele-Assoziation wird in beiden Model-Klassen die Methode has_and_belongs_to_many zur Deklaration der Assoziation verwendet. Als Parameter wird die jeweils andere Model-Klasse im Plural angegeben.
Listing Model-Klasse Flight und Passenger
# Datei app/models/flight.rb class Flight < ActiveRecord::Base has_and_belongs_to_many :passengers end # Datei app/models/passenger.rb class Passenger < ActiveRecord::Base has_and_belongs_to_many :flights end
Konvention
Dank der Konvention muss die Join-Tabelle nicht erwähnt werden. Rails geht davon aus, dass die Tabelle flights_passengers existiert.
Durch den Aufruf der Methode has_and_belongs_to_many werden die gleichen zusätzlichen Methoden definiert wie bei einer Eins-zu-viele-Assoziation durch Aufruf der Methode has_many (siehe Abschnitt 10.10.1).
Beispiele
In den folgenden Beispielen wird der Umgang mit der Viele-zu-viele-Assoziation demonstriert:
# Erstellen eines Fluges flight =Flight.create(:nr=>"RA223", ...) # Erstellung zweier Passagiere lee = Passenger.create(:firstname=>'Lee', ...) william = Passenger.create(:firstname=>'William', ...) # Wie viele Passagiere hat der Flug? flight.passengers.size # => 0 # Passagiere dem Flug zuordnen flight.passengers << lee flight.passengers << william # oder flight.passengers << [lee, william] # Wie viele Passagiere hat der Flug? flight.passengers.size # => 2 # Wie viele Flüge hat Lee lee.flights.size # => 1 # Flugnummer des Fluges von Lee? lee.flights.find(:first).nr # => "RA223" # Ausgabe der Passagierliste flight.passengers.each do |passenger| puts passenger.firstname end # => "Lee" # "William" # Passagierliste als Array flight.passengers.map{|passenger| passenger.firstname} # => ["Lee","William"] # Lee storniert den Flug RA223. # Dazu wird die Assoziation zum Flug gelöst. # Der Flug wird nicht gelöscht! flight = Flight.find_by_nr('RA223') lee.flights.delete(flight) # Anzahl der Passagiere vom Flug ist nun um eins verringert flight.passengers.size # => 1
Join-Tabelle mit einem Model
Mehr als Fremdschlüssel
Die Join-Tabelle enthält normalerweise nur die beiden Fremdschlüssel. Dies reicht in vielen praktischen Fällen jedoch nicht aus. In unserem Beispiel wäre es sinnvoll, in der Join-Tabelle z. B. auch das Buchungsdatum und den Kaufpreis der Buchung festzuhalten. In diesem Fall fungiert die Join-Tabelle nicht mehr als Hilfstabelle, sondern als Tabelle mit einer zugehörigen Model-Klasse.
Die Join-Tabelle erhält dann auch einen »richtigen« Namen. In unserem Beispiel wird die Join-Tabelle bookings genannt.
Abbildung Assoziationen zwischen flights-bookings-passengers
Zwei Eins-zu-viele
Anstatt eine Viele-zu-viele-Assoziation werden nun zwei Eins-zu-viele-Assoziationen verwendet.
Abbildung Klassen-Diagramm
Die Join-Tabelle und die dazugehörige Model-Klasse können nun auch mit dem model -Generator erstellt werden.
Listing Generierung des Booking-Models
ruby script/generate model Booking flight_id:integer \ passenger_id:integer price:float
Die Assoziationen werden in den Model-Klassen durch die Methoden has_many und belongs_to definiert.
Listing Models Flight, Booking und Passenger
# Datei app/models/flight.rb class Flight < ActiveRecord::Base has_many :bookings end # Datei app/models/booking.rb class Booking < ActiveRecord::Base belongs_to :flight belongs_to :passenger end # Datei app/models/passenger.rb class Passenger < ActiveRecord::Base has_many :bookings end
Eine Buchung kann dann wie folgt durchgeführt werden:
# Erstellen eines Fluges flight =Flight.create(:nr=>"RA223", ...) # Erstellung eines Passagiers lee = Passenger.create(:firstname=>'Lee', ...) # Buchung erstellen booking = Booking.new(:price=>299.0, ...) # Buchung dem Passagier und dem Flug zuordnen lee.bookings << booking flight.bookings << booking # Wie viele Buchungen hat der Passagiere Lee? lee.bookings.size # => 1 # Wie viele Buchungen hat der Flug? flight.bookings.size # => 1
Problem
Es gibt jedoch ein Problem. Es ist nicht direkt möglich (ohne Umweg über das Booking -Model), zu einem Flug alle Namen der Passagiere zu ermitteln. Der Ausdruck flight.users ist zur Zeit nicht möglich.
Das Problem lässt sich durch Hinzufügen weiterer Assoziationen in den Models lösen. Zwischen den Models Flight und Passenger definieren wir eine Assoziation mit der Methode has_many mit der Option :through => :bookings . Diese Option gibt den Namen der Join-Tabelle an.
Listing Models Flight und Passenger
# Datei app/models/flight.rb class Flight < ActiveRecord::Base has_many :bookings has_many :passengers, :through => :bookings end # Datei app/models/passenger.rb class Passenger < ActiveRecord::Base has_many :bookings has_many :flights, :through => :bookings end
Jetzt können wir z. B. leicht die Namen der Passagiere eines Fluges ermitteln:
# Ausgabe der Namen mit puts flight.passengers.each do |passenger| puts passenger.firstname end # => "Lee" # oder Erstellung eines Arrays mit den Namen flight.passengers.map{|passenger| passenger.firstname} # => ["Lee"]
Polymorphe Assoziationen 

Definition
Polymorphe Assoziationen sind Assoziationen, die sich jeweils nicht nur auf Objekte der gleichen Klasse beziehen können, sondern auch auf Objekte verschiedener Klassen. Dies wird dadurch erreicht, dass nicht nur ein Fremdschlüssel, sondern auch der Name der Klasse, auf die sich der Fremdschlüssel bezieht, gespeichert wird.
Anhand einer Buchungs-Applikation demonstrieren wir den Einsatz von polymorphen Assoziationen.
Beispiel
Ein Reiseunternehmen bietet eine Reihe von verschiedenen Reisen, wie z. B. Flüge (Flights), Hotels und Schiffsreisen (Cruises) an, die buchbar sein sollen. Zur Verwaltung der Buchungen wird eine Tabelle bzw. eine Model-Klasse Booking verwendet.
Jede buchbare Reise kann beliebig viele Buchungen besitzen. Deshalb besteht zwischen den buchbaren Reisen und den Buchungen eine Eins-zu-viele-Assoziation.
Abbildung Klassen-Diagramm für Buchungsbeispiel
Wenn wir jedoch versuchen, die Eins-zu-viele-Assoziation zu implementieren, treffen wir auf folgendes Problem:
In der Tabelle mit den Buchungen (bookings) wird ein Fremdschlüssel benötigt, der die ID zur entsprechenden Reise speichert. Jedoch ist dann nicht klar, ob sich der Fremdschlüssel auf einen Flug, ein Hotel oder eine Schiffsreise bezieht.
Die Lösung besteht darin, dass wir in der Buchungstabelle (bookings) nicht nur ein Feld für den Fremdschlüssel erstellen, sondern auch ein Feld für den Namen der Klasse, auf die sich der Fremdschlüssel bezieht.
Es stellt sich jedoch die Frage, wie wir die Felder benennen. Da es sich um Reisen handelt,
nennen wir das Feld mit dem Fremdschlüssel voyage_id und das Feld, das den Namen der Klasse speichert, voyage_type . Auf jeden Fall sollten die Felder dem Muster *_id und *_type entsprechen, um der Konvention in Rails zu genügen.
Benennung von Feldern in polymorphen Assoziationen |
Wir hätten die Felder auch bookable_id und bookable_type nennen können. Die Bezeichnung bookable bezieht sich auf die buchbaren Objekte, die unserem Fall Flüge, Hotels oder Schiffsreisen sein können. Diese Art der Benennung mit *able , wie z. B. commentable oder imageable , wird bei polymorphen Assoziationen gerne verwendet. |
Abbildung Tabellen der polymorphen Assoziation
Generierung der Model-Klassen und Tabellen
rails polymorph
Zur Übung können Sie hierfür ein neues Rails-Projekt mit rails polymorph erstellen. Für die Erstellung der Model-Klassen und der zugehörigen Tabellen setzen wir auch hier wieder den model -Generator ein. Alternativ kann auch der scaffold -Generator benutzt werden. Der Einfachheit halber erstellen wir nur die wichtigsten Felder. In der Tabelle flights erstellen wir z. B. nur ein Feld für die Flugnummer (nr).
Listing Generierung mit dem model-Generator
ruby script/generate model flight nr:string ruby script/generate model hotel name:string rooms:integer ruby script/generate model cruise name:string cabins:integer ruby script/generate model booking firstname:string \ lastname:string voyage_id:string voyage_type:string
In einer realen Applikation würde man in der bookings -Tabelle nicht ein Feld für den Nachnamen und den Vornamen verwenden, sondern ein Feld user_id als Fremdschlüssel einer User-Tabelle verwenden.
Um die Tabellen zu erstellen, führen wir die Migration-Skripte mit dem Befehl rake db:migrate aus.
Definieren der Assoziation in den Models
has_many ..., :as => ...
In den Model-Klassen Flight, Hotel und Cruise deklarieren wir die Assoziation mit der Methode has_many :bookings . Da es sich jedoch um eine polymorphe Assoziation handelt, muss die Option :as=> :voyage hinzugefügt werden. Die Option gibt an, dass es sich bei der Model-Klasse um eine Reise (voyage) handelt, auf die sich dann die Booking -Klasse beziehen kann.
Listing Model-Klasse Flight
class Flight < ActiveRecord::Base has_many :bookings, :as => :voyage end
Listing Model-Klasse Hotel
class Hotel < ActiveRecord::Base has_many :bookings, :as => :voyage end
Listing Model-Klasse Cruise
class Cruise < ActiveRecord::Base has_many :bookings, :as => :voyage end
belongs_to ..., :polymorphic => true
In der Model-Klasse Booking deklarieren wir die Assoziation mit der Methode belongs_to :voyage . Da es sich um eine polymorphe Assoziation handelt, muss zusätzlich die Option :polymorphic => true angegeben werden. Ohne diese Option würde ActiveRecord versuchen, auf die nicht existente Tabelle voyages zuzugreifen.
Listing Model-Klasse Booking
class Booking < ActiveRecord::Base belongs_to :voyage, :polymorphic => true end
Verwendung
Polymorphe Assoziationen werden im Prinzip genauso verwendet wie »normale« Eins-zu-viele-Assoziationen:
flight = Flight.create(:nr => "RH482") hotel = Hotel.create(:name => "Burj Al Arab", :rooms => 202) cruise = Cruise.create(:name => "Queen Mary 2", :cabins => 1310) flight.bookings << Booking.new(:firstname => "Lee", :lastname => "Adama") # Alternativ kann auch direkt die # create-Methode der Assoziation verwendet werden. hotel.bookings.create(:firstname => "Kara", :lastname => "Trace") # Anzahl Gesamt Buchungen bestimmen. Booking.count # => 2 # Anzahl Flug-Buchungen bestimmen flight.bookings.count # => 1 # Auf die erste Reise der ersten Buchung zugreifen voyage = Booking.find(:first).voyage puts voyage.class # => Flight puts voyage.nr # => RH482 # Alle Buchungen durchlaufen und Name der Klasse ausgeben Booking.find(:all).each do |booking| puts booking.voyage.class end # => Flight # => Hotel
Polymorphe Assoziationen auch für Eins-zu-eins-Assoziationen |
Polymorphe Assoziationen können nicht nur bei Eins-zu-viele-Assoziationen ( has_many -Methoden), sondern auch bei Eins-zu-eins-Assoziationen ( has_one -Methoden) verwendet werden. |
Mehrere Assoziationen zum gleichen Model 

Angenommen, wir möchten Flüge (flights) verwalten, die jeweils einen Abflughafen (departure_airport) und einen Zielflughafen (destination_airport) haben. Dann handelt es sich um zwei Assoziationen zur selben Tabelle (airports).
Zwei Fremdschlüssel
Um das Problem zu lösen, werden in der Tabelle flights die beiden Fremdschlüssel departure_airport_id und arrival_airport_id benötigt.
Abbildung Airport-Flight-Tabellen
Zweimal belongs_to
In der Model-Klasse Flight werden dann zwei belongs_to -Methodenaufrufe mit der Option class_name benötigt:
Listing app/models/flights.rb
class Flight < ActiveRecord::Base belongs_to :departure_airport, :class_name => "Airport" belongs_to :arrival_airport, :class_name => "Airport" end
Rails 2.0
In Rails 2.0 wird automatisch erkannt, dass die Fremdschlüssel departure_airport_id und arrival_airport_id verwendet werden sollen. Daher kann man auf die zusätzlichen Optionen :foreign_key => "departure_airport_id" und :foreign_key => "arrival_airport_id" verzichten.
air_dus = Airport.create(:code=>'DUS', :name=>'Düsseldorf') air_muc = Airport.create(:code=>'MUC', :name=>'Munich') flight = Flight.create flight.departure_airport = air_dus flight.arrival_airport = air_muc
Assoziationen mit Bedingungen 

Beispiel
Angenommen, wir möchten zu einem Flughafen speichern können, ob dieser ein regionaler oder ein internationaler Flughafen ist. Dazu fügen wir der Flughafen-Tabelle airports ein zusätzliches Feld international hinzu. Wenn die Tabelle bereits existiert, können wir mit folgender Migration ein weiteres Feld hinzufügen:
ruby script/generate migration AddInternationalToAirports \ international:boolean
Anschließend führen wir mit rake db:migrate das Migration-Skript aus.
Beispiel- Datensätze
Anschließend legen wir in der Rails-Konsole (ruby script/console) ein paar Beispieldatensätze an. Dazu setzen wir für alle bereits vorhandenen Flughäfen das Feld international auf true und fügen einen Regionalflughafen ein.
Airport.update_all(:international=>true) air_mgl = Airport.new(:code=>'MGL', :name=>'Mönchengladbach', :international=> false) germany = Country.find_by_code("DE") germany.airports << air_mgl air_mgl.international? # => false
Um nun zu einem Land (country) alle regionalen Flughäfen zu listen, können wir folgenden Befehl ausführen:
germany.airports.find(:all, :conditions => {:international => false}).each do |airport| puts airport.name end # => Mönchengladbach
Vereinfachung
Analog kann man auf diese Weise auch alle internationalen Flughäfen auflisten. Die Verwendung der Find-Methode mit Optionen macht Ihren Code jedoch sehr unleserlich.
Wäre es nicht besser, wenn wir einfach germany.regional_airports verwenden könnten?
Im Folgenden wird gezeigt, wie statt der find -Methode eigene Methoden definiert werden können.
conditions
Der Aufruf germany.regional_airports suggeriert, dass es eine Assoziation zu einem Model RegionalAirport gibt. Da es dieses Model jedoch nicht gibt, geben wir über Optionen zu der has_many -Methode an, dass das Model Airport verwendet werden soll. Über die Option :conditions wird dann entsprechend die Bedingung angegeben, dass nur Flughäfen mit dem Wert false bzw. true im Feld international zurückgeliefert werden sollen.
Listing app/models/country.rb
class Country < ActiveRecord::Base has_many :airports has_many :international_airports, :class_name => "Airport", :conditions => {:international => true} has_many :regional_airports, :class_name => "Airport", :conditions => {:international => false} end
Auf internationale und regionale Flughäfen kann nun viel einfacher zugegriffen werden:
# Internationale Flughäfen germany.international_airports .each do |airport| puts airport.name end # => Munich # => Düsseldorf germany.regional_airports .each do |airport| puts airport.name end # => Mönchengladbach
Controller
Statt der Schleife mit each würde man im Controller einfach eine Instanzvariable setzen, die dann im Template durchlaufen wird:
Listing Verwendung im Controller
... def index @internationals = germany.international_airports @regionals = germany.regional_airports end ...
Eine alternative Möglichkeit, den Code zu vereinfachen, wird im nächsten Abschnitt behandelt.
Eine Assoziation um eigene Methoden erweitern 

Im letzten Abschnitt wurde gezeigt, wie der Code durch Verwendung einer has_many -Methode in Verbindung mit einer Bedingung vereinfacht werden konnte.
In diesem Abschnitt werden wir das gleiche Ziel mit einer anderen Technik erreichen.
Block definieren
Zu der Methode has_many können wir mit do ... end einen Block erstellen, in dem wir zu den Assoziationen Methoden hinzufügen können:
class Country < ActiveRecord::Base ... has_many :airports do def international(status) find(:all, :conditions=>{:international => status}) end end ... end
Wir können nun die internationalen und regionalen Flughäfen wie folgt abrufen:
# internationale Flughäfen germany.airports.international(true) .each do |airport| puts airport.name end # => Munich # => Düsseldorf germany.international(false) .each do |airport| puts airport.name end # => Mönchengladbach
Im Controller könnte der Aufruf z. B. wie folgt aussehen:
Listing Verwendung im Controller
... def index @internationals = germany.airports.international(true) @regionals = germany.airports.international(false) end ...
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.