In Ruby on Rails gibt es zwei fundamentale Ansätze eine N:M Beziehung umzusetzen: has_and_belongs_to_many und has_many :through. Beide haben Vor-und Nachteile. Das heißt aber auch, beide Assoziationen haben ihre Berechtigung.
Es hält sich aber hartnäckig das Vorurteil, dass has_many :through grundsätzlich vorzuziehen wäre, weil sie flexibler ist. Es stimmt, sie ist flexibler. Das has_many :through Modell kann bei Bedarf mit weiteren Attributen versehen werden, ohne die Assoziation ändern zu müssen. Allerdings ist das nicht immer notwendig. Zumal has_and_belongs_to_many leichtgewichtiger ist und einen wesentlich geringeren footprint hat.
Die Angst vor einer eventuell notwendigen Strukturänderung sollte keinesfalls der Grund sein, sich für has_many :through zu entscheiden. Angst ist immer ein schlechter Berater.
Anhand eines Beispiels wird gezeigt, wie einfach und natürlich die Migration von einer bestehenden has_and_belongs_to_many Assoziation zu einer has_many :through Assoziation ist.
Ausgehend von Usern, die mehrere Adressen haben können und in jeder Adresse mehrere User wohnen können:
class User < ActiveRecord::Base
has_and_belongs_to_many :addresses
end
class Address < ActiveRecord::Base
has_and_belongs_to_many :users
end
Diese Beziehung ist schnell aufgesetzt, da sie standardgemäß lediglich die beiden Attribute user_id und address_id in einer Zwischentabelle mit dem Namen addresses_users erfordert:
$ rails g migration CreateAddressesUsersJoinTable users addresses && rake db:migrate
Fertig.
Diese bestehende Beziehung im Bedarfsfall in eine has_many :through zu migrieren ist sehr einfach.
1. Das Zwischenmodell
Zunächst das Modell für die Zwischentabelle user_addresses mit zusätzlichen Zeitstempeln:
$ rails g migration UserAddress user:belongs_to address:belongs_to && rake db:migrate
erzeugt und führt diese Migration aus:
class CreateUserAddresses < ActiveRecord::Migration
def change
create_table :user_addresses do |t|
t.belongs_to :user, index: true, foreign_key: true
t.belongs_to :address, index: true, foreign_key: true
t.timestamps null: false
end
end
end
2. Setzen der has_many :through Assoziation
Damit die neue has_many :through Assoziation eindeutig gesetzt werden kann, muss allerdings die alte HABTM Beziehung temporär umbenannt werden (eine Seite der Beziehung reicht). Das bereitet das Kopieren der Daten (Assoziationslinks) vor:
class User < ActiveRecord::Base
has_and_belongs_to_many :deprecated_addresses,
join_table: :addresses_users, class_name: Address,
association_foreign_key: :address_id
has_many :user_addresses
has_many :addresses, through: :user_addresses
end
3. Migrieren der bestehenden Daten
Dieser Schritt ist notwendig, um die Assoziationslinks zu kopieren. Die Migrationsdatei:
$ rails g migration MigrateAddressesUsers
erstellt UserAddress Objekte:
class MigrateAdressesUsersAssociations < ActiveRecord::Migration
def change
User.transaction do
User.find_each do |user|
user.addresses = user.deprecated_addresses
end
end
end
end
Es macht absolut Sinn, die Daten der Zwischentabelle in Batches zu migrieren. Standardmäßig greift sich find_each 1000 Datensätze. Wie umfangreich die Batches sein sollten, hängt natürlich von dem jeweiligen Fall ab.
Die erfolgte Migration der Beziehungen kann nun deployed werden. Das Abräumen der deprecated gewordenen HABTM Beziehung sollte in einem weiteren deploy erfolgen.
5. Löschen der HABTM Tabelle und Beziehung
Im 2. deploy muss die Migrationsdatei zum Löschen der überflüssig gewordenen HABTM Tabelle erstellt werden:
$ rails g migration DropTableAddressesUsers
Sie sollte den Inhalt haben:
class DropTableAddressesUsers < ActiveRecord::Migration
def change
drop_table :addresses_users
end
end
Schließlich die Migration ausführen:
$ rake db:migrate
Und die beiden Modelle ohne die HABTM Beziehungen:
class User < ActiveRecord::Base
has_many :user_addresses
has_many :addresses, through: :user_addresses
end
class Address < ActiveRecord::Base
has_many :user_addresses
has_many :users, through: :user_addresses
end
Zusammenfassung
Der Prozess der Migration einer has_and_belongs_to_many Beziehung zu einer has_many :through Beziehung sieht erstmal umfangreicher aus, als er ist. Einige Schritte sind ohnehin erforderlich, wenn sich von Vornherein für die has_many :through Assoziation entschieden wird. Eigentlich entsteht nur der zusätzliche Aufwand für das Migrieren der Links (Assoziationsdaten). Auch das Aufteilen der Migration in 2 deploys ist nur notwendig, weil die bereits entstandenen Assoziationen gesichert kopiert werden müssen.
Grundsätzlich sollte sich immer für HABTM entschieden werden, es sei denn, es ist wahrscheinlich, dass die Beziehung weitere Attribute haben soll. Denn eine dedizierte HABTM Beziehung ist performanter und eine Migration zu einer has_many :through Beziehung ist unproblematisch, wenn sie denn überhaupt notwendig wird.