Rails 5.1 Consulting und Schulung vom Autor:
www.wintermeyer-consulting.de/rails/

4.11. Polymorphe Assoziationen (polymorphic associations)

Schon das Wort "polymorph" lässt einen angespannt werden. Was kann damit gemeint sein? Auf http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html steht dazu: Polymorphic associations on models are not restricted on what types of models they can be associated with. Na, das ist ja jetzt klar wie Kloßbrühe! ;-)
Ich zeige Ihnen ein Beispiel, in dem wir ein Model für Autos (Car) und ein Model für Fahrräder (Bike) erstellen. Um ein Auto oder ein Fahrrad zu beschreiben, verwenden wir ein Model zum Auszeichnen (Tag). Ein Auto und ein Fahrrad kann beliebig viele tags haben. Die Applikation:
MacBook:~ xyz$ rails new example
[...]
MacBook:~ xyz$ cd example 
MacBook:example xyz$
Jetzt die drei benötigten Modele:
MacBook:example xyz$ rails generate model car name
[...]
MacBook:example xyz$ rails generate model bike name
[...]
MacBook:example xyz$ rails generate model tag name taggable_type taggable_id:integer
[...]
MacBook:example xyz$ rake db:migrate
[...]
MacBook:example xyz$
Car und Bike sind klar. Bei Tag benutzen wir die Felder taggable_type und taggable_id, um ActiveRecord eine Möglichkeit zu geben, die Zuordnung für die polymorphic association abzuspeichern. Dies müssen wir im Model entsprechend eintragen.
app/models/tag.rb
class Tag < ActiveRecord::Base
  attr_accessible :name, :taggable_id, :taggable_type

  belongs_to :taggable, :polymorphic => true
end
app/models/car.rb
class Car < ActiveRecord::Base
  attr_accessible :name

  has_many :tags, :as => :taggable  
end
app/models/bike.rb
class Bike < ActiveRecord::Base
  attr_accessible :name

  has_many :tags, :as => :taggable
end
Wir benutzen bei Car und Bike ein zusätzliches :as => :taggable bei der Definition von has_many. Bei Tag benutzen wir belongs_to :taggable, :polymorphic => true, um ActiveRecord die polymorphic association anzuzeigen.

Tipp

Das Suffix able (bar) beim Namen taggable ist Rails-üblich, muss aber nicht sein. Wir brauchen ja zum Verknüpfen jetzt nicht nur die ID des Eintrags, sondern müssen auch noch wissen, um welches Model es sich eigentlich handelt. Da macht der Begriff taggable_type halbwegs Sinn.
Gehen wir mal in die Console und legen ein Auto und ein Fahrrad an:
MacBook:example xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > golf = Car.create(:name => 'VW Golf')
   (0.1ms)  begin transaction
  SQL (5.2ms)  INSERT INTO "cars" ("created_at", "name", "updated_at") VALUES (?, ?, ?)  [["created_at", Tue, 08 May 2012 07:20:45 UTC +00:00], ["name", "VW Golf"], ["updated_at", Tue, 08 May 2012 07:20:45 UTC +00:00]]
   (2.4ms)  commit transaction
 => #<Car id: 1, name: "VW Golf", created_at: "2012-05-08 07:20:45", updated_at: "2012-05-08 07:20:45"> 
1.9.3p194 :002 > mountainbike = Bike.create(:name => 'Mountainbike')
   (0.1ms)  begin transaction
  SQL (0.6ms)  INSERT INTO "bikes" ("created_at", "name", "updated_at") VALUES (?, ?, ?)  [["created_at", Tue, 08 May 2012 07:20:51 UTC +00:00], ["name", "Mountainbike"], ["updated_at", Tue, 08 May 2012 07:20:51 UTC +00:00]]
   (3.0ms)  commit transaction
 => #<Bike id: 1, name: "Mountainbike", created_at: "2012-05-08 07:20:51", updated_at: "2012-05-08 07:20:51"> 
1.9.3p194 :003 >
Jetzt definieren wir jeweils ein Tag mit der Farbe des entsprechenden Objektes:
1.9.3p194 :004 > golf.tags.create(:name => 'blau')
   (0.1ms)  begin transaction
  SQL (0.6ms)  INSERT INTO "tags" ("created_at", "name", "taggable_id", "taggable_type", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", Tue, 08 May 2012 07:23:21 UTC +00:00], ["name", "blau"], ["taggable_id", 1], ["taggable_type", "Car"], ["updated_at", Tue, 08 May 2012 07:23:21 UTC +00:00]]
   (1.2ms)  commit transaction
 => #<Tag id: 1, name: "blau", taggable_type: "Car", taggable_id: 1, created_at: "2012-05-08 07:23:21", updated_at: "2012-05-08 07:23:21"> 
1.9.3p194 :005 > mountainbike.tags.create(:name => 'schwarz')
   (0.1ms)  begin transaction
  SQL (0.6ms)  INSERT INTO "tags" ("created_at", "name", "taggable_id", "taggable_type", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", Tue, 08 May 2012 07:27:11 UTC +00:00], ["name", "schwarz"], ["taggable_id", 1], ["taggable_type", "Bike"], ["updated_at", Tue, 08 May 2012 07:27:11 UTC +00:00]]
   (3.1ms)  commit transaction
 => #<Tag id: 2, name: "schwarz", taggable_type: "Bike", taggable_id: 1, created_at: "2012-05-08 07:27:11", updated_at: "2012-05-08 07:27:11"> 
1.9.3p194 :006 >
Beim Golf fügen wir noch ein weiteres Tag hinzu:
1.9.3p194 :006 > golf.tags.create(:name => 'Automatik')
   (0.1ms)  begin transaction
  SQL (1.3ms)  INSERT INTO "tags" ("created_at", "name", "taggable_id", "taggable_type", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", Tue, 08 May 2012 07:28:12 UTC +00:00], ["name", "Automatik"], ["taggable_id", 1], ["taggable_type", "Car"], ["updated_at", Tue, 08 May 2012 07:28:12 UTC +00:00]]
   (3.7ms)  commit transaction
 => #<Tag id: 3, name: "Automatik", taggable_type: "Car", taggable_id: 1, created_at: "2012-05-08 07:28:12", updated_at: "2012-05-08 07:28:12"> 
1.9.3p194 :007 > 
Schauen wir uns jetzt alle Tag-Einträge an:
1.9.3p194 :007 > Tag.all
  Tag Load (0.4ms)  SELECT "tags".* FROM "tags" 
 => [#<Tag id: 1, name: "blau", taggable_type: "Car", taggable_id: 1, created_at: "2012-05-08 07:23:21", updated_at: "2012-05-08 07:23:21">, #<Tag id: 2, name: "schwarz", taggable_type: "Bike", taggable_id: 1, created_at: "2012-05-08 07:27:11", updated_at: "2012-05-08 07:27:11">, #<Tag id: 3, name: "Automatik", taggable_type: "Car", taggable_id: 1, created_at: "2012-05-08 07:28:12", updated_at: "2012-05-08 07:28:12">] 
1.9.3p194 :008 > 
Und jetzt alle Tags des Golfs:
1.9.3p194 :008 > golf.tags
  Tag Load (0.3ms)  SELECT "tags".* FROM "tags" WHERE "tags"."taggable_id" = 1 AND "tags"."taggable_type" = 'Car'
 => [#<Tag id: 1, name: "blau", taggable_type: "Car", taggable_id: 1, created_at: "2012-05-08 07:23:21", updated_at: "2012-05-08 07:23:21">, #<Tag id: 3, name: "Automatik", taggable_type: "Car", taggable_id: 1, created_at: "2012-05-08 07:28:12", updated_at: "2012-05-08 07:28:12">] 
1.9.3p194 :009 > 
Natürlich können Sie sich auch anzeigen lassen, zu welchem Objekt das letzte Tag gehört:
1.9.3p194 :009 > Tag.last.taggable
  Tag Load (0.3ms)  SELECT "tags".* FROM "tags" ORDER BY "tags"."id" DESC LIMIT 1
  Car Load (0.2ms)  SELECT "cars".* FROM "cars" WHERE "cars"."id" = 1 LIMIT 1
 => #<Car id: 1, name: "VW Golf", created_at: "2012-05-08 07:20:45", updated_at: "2012-05-08 07:20:45"> 
1.9.3p194 :010 > exit
MacBook:example xyz$
Polymorphic associations sind immer praktisch, wenn man die Datenbankstruktur normalisieren will. Wir hätten in diesem Beispiel ja auch ein Model CarTag und BikeTag definieren können, aber da Tag für beide gleich ist, macht hier eine polymorphic association mehr Sinn.

Anmerkung

Polymorphic Associations sind sehr praktisch. Man sollte aber auch immer daran denken, dass sie mehr Last auf der Datenbank erzeugen als eine normale 1:n-Verknüpfung. Normalerweise macht das den Bock nicht fett, aber man sollte es bei der Planung im Hinterkopf haben.