Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Geleitwort des Fachgutachters
Einleitung
1 Einführung
2 Installation
3 Erste Schritte
4 Einführung in Ruby
5 Eine einfache Bookmarkverwaltung
6 Test-Driven Development
7 Rails-Projekte erstellen
8 Templatesystem mit ActionView
9 Steuerzentrale mit ActionController
10 Datenbankzugriff mit ActiveRecord
11 E-Mails verwalten mit ActionMailer
12 Nützliche Helfer mit ActiveSupport
13 Ajax on Rails
14 RESTful Rails und Webservices
15 Rails mit Plug-ins erweitern
16 Performancesteigerung
17 Sicherheit
18 Veröffentlichen einer Rails-Applikation auf einem Server
Ihre Meinung?

Spacer
 <<   zurück
Ruby on Rails 2 von Hussein Morsy, Tanja Otto
Das Entwickler-Handbuch
Buch: Ruby on Rails 2

Ruby on Rails 2
geb., mit DVD
699 S., 39,90 Euro
Rheinwerk Computing
ISBN 978-3-89842-779-1
Online bestellenPrint-Version Jetzt Buch bestellen
* versandkostenfrei in (D) und (A)
Pfeil 10 Datenbankzugriff mit ActiveRecord
  Pfeil 10.1 Einführung
  Pfeil 10.2 Eine ActiveRecord-Model-Klasse generieren
  Pfeil 10.3 Rake-Tasks zum Verwalten von Datenbanken
  Pfeil 10.4 Getter- und Setter-Methoden
  Pfeil 10.5 Erstellen, bearbeiten und löschen
  Pfeil 10.6 Suchen
  Pfeil 10.7 Datenbankschema und Migrations
  Pfeil 10.8 Migration-Skripte
  Pfeil 10.9 Migration-Befehle im Detail
  Pfeil 10.10 Assoziationen
  Pfeil 10.11 Validierung
  Pfeil 10.12 Statistische Berechnungen
  Pfeil 10.13 Callbacks
  Pfeil 10.14 Vererbung


Rheinwerk Computing - Zum Seitenanfang

10.10 Assoziationen  Zur nächsten ÜberschriftZur vorigen Überschrift


Rheinwerk Computing - Zum Seitenanfang

Eins-zu-viele-Assoziationen (1:n)  Zur nächsten ÜberschriftZur vorigen Überschrift

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
Die Angabe des Models erfolgt bei der has_many -Methode im Plural und die bei der belongs_to -Methode im Singular. Dies kann man sich jedoch leicht merken, denn ein Country »hat viele« Airports und ein Airport »gehört zu« einem Country.

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

Rheinwerk Computing - Zum Seitenanfang

Eins-zu-eins-Assoziationen (1:1)  Zur nächsten ÜberschriftZur vorigen Überschrift

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

Rheinwerk Computing - Zum Seitenanfang

Viele-zu-viele-Assoziationen (n:m)  Zur nächsten ÜberschriftZur vorigen Überschrift

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"]

Rheinwerk Computing - Zum Seitenanfang

Polymorphe Assoziationen  Zur nächsten ÜberschriftZur vorigen Überschrift

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.


Rheinwerk Computing - Zum Seitenanfang

Mehrere Assoziationen zum gleichen Model  Zur nächsten ÜberschriftZur vorigen Überschrift

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

Rheinwerk Computing - Zum Seitenanfang

Assoziationen mit Bedingungen  Zur nächsten ÜberschriftZur vorigen Überschrift

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.


Rheinwerk Computing - Zum Seitenanfang

Eine Assoziation um eigene Methoden erweitern  topZur vorigen Überschrift

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.

 <<   zurück
  Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Ruby on Rails 2
Ruby on Rails 2
Jetzt Buch bestellen
 Ihre Meinung?
Wie hat Ihnen das Openbook gefallen?
Ihre Meinung

 Buchtipps
Zum Rheinwerk-Shop: Ruby on Rails 3.1






 Ruby on Rails 3.1


Zum Rheinwerk-Shop: Responsive Webdesign






 Responsive Webdesign


Zum Rheinwerk-Shop: Suchmaschinen-Optimierung






 Suchmaschinen-
 Optimierung


Zum Rheinwerk-Shop: JavaScript






 JavaScript


Zum Rheinwerk-Shop: Schrödinger lernt HTML5, CSS3 und JavaScript






 Schrödinger lernt
 HTML5, CSS3
 und JavaScript


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2008
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern