Ein Entwickler startet ein neues Projekt und legt das Model
User an. Da das Projekt am Anfang steht, hat der User erst mal nur ein Login. In der Migration wird auch gleich ein "admin"-User angelegt. Ein paar Tage später wird entschieden, dass beim User auch Vor- und Nachname benötigt werden. Also legt der Entwickler eine Migration an, die dem User diese Felder hinzufügt und das User-Model erhält Validierungen, damit die Vor- und Nachnamen auch befüllt werden.Nun kommt ein zweiter Entwickler zum Projekt hinzu. Er checkt die Sourcen frisch aus und will seine Entwicklungs-DB migrieren. Schon die erste Migration schlägt fehl, da diese den "admin"-User ohne Vor- und Nachnamen anlegen will. Auch später im Produktivsystem funktioniert diese Migration nicht mehr.
Noch spannender wird es, wenn im Laufe der Zeit durch ein Refactoring ein Model ganz entfernt wurde. Alle vorangehenden Migrationen, die irgendwie auf die Klasse zugreifen wollen, schlagen fehl. Kurzum: Die Anwendung entwickelt sich dynamisch, während die Migrationen zum jeweiligen Anwendungsstand in Stein gemeißelt werden.
Deswegen haben wir über verschiedene Lösungsmöglichkeiten nachgedacht und sind auf folgende Ansätze gestoßen:
- In Migrationen nur reines SQL benutzen? Damit gibt man die Einfachheit von Rails auf. Und gerade die Tatsache, Rails-Code in Migrationen zu schreiben, macht sie doch so attraktiv.
- Alle Migrationen rückwirkend entsprechend des Datenmodels anpassen? Das entspricht nicht dem Konzept der Migrationen. Außerdem ist es zu aufwändig, sobald ein paar mehr Migrationen vorhanden sind.
- Ryan Wilcox schlägt vor, Validierungen mittels
:if => Proc.new {|o| o.responds_to? :checked_attr}deaktivieren, wenn die Datenbank das Attribut nicht kennt. Das funktioniert zwar für die Validierungen. Aber sollen wirklich alle Validierungen mit so einem hässlichen Block erweitert werden, nur um die Migrationen zu unterstützen? - In Migrationen statt
User.create!einfachuser = User.buildunduser.save!(false)nutzen? Auch dieser Ansatz funktioniert für die Validierungen, nicht aber bei den Refactorings
class ChangeUser < ActiveRecord::Migration
class User < ActiveRecord::Base
end
def self.up
...
end
def self.down
...
end
end
So hat die Migration ihr eigenes Model, keine der Validierungen stört hier und die Migration kann auch dann auf die Tabelle zugreifen, wenn das eigentliche Model in der Anwendung schon lang entfernt wurde.Natürlich muss man aufpassen, dass man keine selbstgeschriebenen
User-Methoden in der Migration nutzt. Auch die has_many- und belongs_to-Methoden sind nicht vorhanden, was man beim Setzen einiger Werte beachten muss. Zumindest bei den N:M-Beziehungen ist das Einfügen der habtm-Zeile sinnvoll, da sonst die N:M-Tabelle wirklich von Hand befüllt werden muss.
class CreateUser < ActiveRecord::Migration
class User < ActiveRecord::Base
has_and_belongs_to_many :roles
end
class Role < ActiveRecord::Base
end
def self.up
...
adminUser = User.create! {:login => "admin"}
adminRole = Role.create! {:title => "admin"}
userRole = Role.create! {:title => "user"}
adminUser.role_ids = [adminRole.id, userRole.id]
adminUser.save!
end
def self.down
...
end
end
Hier gibt es noch eine kleine Falle: adminUser.roles = [adminRole, userRole] wird nicht funktionieren, da der Setter den Typ des Objekts prüft. Und Role ist nicht gleich CreateUser::Role.Ansonsten ist das wirklich ein schöner Weg, die Migration vom aktuellen Zustand der Anwendung zu trennen.
Links und Quellen
Artikel in den RailsGuides
Ryan Wilcox' Blog-Eintrag
Kommentar schreiben