Da wir unsere Projekte meist im Team bearbeiten, standen wir schon öfter vor folgendem Problem:
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.
Für die Entwicklung wäre es vermutlich einfach, in Migrationen gar keine Daten zu setzen oder zu ändern. Aber spätestens in der Wartungsphase der Anwendung müssen bei DB-Änderungen auch die Daten angepasst 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! einfach user = User.build und user.save!(false) nutzen? Auch dieser Ansatz funktioniert für die Validierungen, nicht aber bei den Refactorings
Schließlich half uns ein Eintrag in den RailsGuides: Einfach das Model in der Migration selbst definieren:
  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