Ruby on Rails: ActiveRecord: Migrationen

Aus Wikibooks

ActiveRecord ist die Datebankabstraktionsschicht von Rails. ActiveRecord-Objekte kapseln Daten, die in der Datenbank abgelegt werden. Damit können wir ohne SQL direkt aus Ruby auf die Daten zugreifen und das unabhängig von der verwendeten Datenbank.

Stellen wir uns einmal vor, wir wollen für unser Online-Glossar Worterklärungen in einer Datenbank speichern. Wie legen wir die Datenbank an? Wie speichern wir eine Erklärung und wie finden wir sie wieder? Oder wie finden wir alle Glossareinträge zu Worten, die mit "Sch" beginen?

Das sind die zentralen Operationen von ActiveRecord

  • Tabellen anlegen und ändern mit Migrationen
  • CRUD und "find", die Active-Record Basisoperationen
  • Beziehungen zwischen Objekten modellieren

In diesem Kapitel schauen wir uns die Migrationen an. Migrationen oder Migrations sind der Rails-Weg um Tabellen in der Datenbank anzulegen oder zu ändern. Historisch gesehen und in anderen Programierumgebungen ist das ein Problembereich. Stellen wir uns einmal vor, wir haben ein Programm geschrieben und dabei schrittweise die Datenbank erweitert und verändert. Aus irgendwelchen Gründen will unser Kunde zur letzten Version des Programms zurückkehren. Wie bekommen wir die Datenbank auf den alten Stand? In Rails ist das ganz einfach. Die Migrationen tragen Versionsnumern und sie lassen ein "undo" zu.

Tabelle anlegen und ändern mit Migrationen[Bearbeiten]

Wie gesagt, Migrationen sind der Rails-Weg um Tabellen in der Datenbank anzulegen oder zu ändern. Sie tragen Versionsnumern und sie lassen ein "undo" zu. Schauen wir genauer hin.

Bis Rails 2.0 ist die Versionsnummer eine dreistellige Zahl, die der Entwickler vergibt. In Rails 2.1 wurde das verändert. Die Versionsnummer ist jetzt ein Zeitstempel. Dadurch wird vermieden, dass sich Entwickler, die parallel arbeiten, mit der Versionsnummer in die Quere kommen.

So sieht eine (vor Rails 2.1-)Migration, sagen wir 001_create_somethings.rb, beispielsweise aus:

class CreateSomethings < ActiveRecord::Migration
  def self.up
    create_table 'somethings' do |t|
      t.column 'name', :string
      t.column 'short_description', :text
      t.column 'long_description', :text
    end
  end

  def self.down
    drop_table 'somethings'
  end
end

Ab Version Rails 2.1 sind sogenannte "sexy Migrations" möglich. Dabei können mehrere Felder vom gleichen Typ mit einer Zeile angelegt werden.

Das sieht dann so aus. 20080720171852_create_somethings.rb:

Class CreateSomethings < ActiveRecord::Migration
  def self.up
    create_table :somethings do |t|
      t.string :name
      t.text :short_description, :long_description
    end
  end

  def self.down
    drop_table :somethings
  end
end

Wir sehen, die neue Schreibweise ist kompakter und bei großen Tabellen übersichtlicher. Im Dateinamen finden wir den Zeitstempel 20080720171852, den Rails 2.1 vergibt, wenn wir die Migration beispielsweise mit "script/generate migration migration_name" anlegen.

In allen Datenbanken sind die folgenden Standardtypen verfügbar: :string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary und :boolean. Sie können aber von den Datenbanken unterschiedlich umgesetzt werden. So wird :text von mysql zu text umgesetzt und von Oracle zu clob.

Mit

rake db:migrate

führen wir die Migration aus. Oder um genau zu sein, wir führen alle Migrationensdateien im Ordner db/migrate aus, die seit der letzten Migration hinzugekomen sind.

Wie man Migrationen wieder zurücknimmt und wie man einzelne Migrationen anspricht, kommen wir gleich, wenn wir gesehen haben, was man mit Migrationen alles machen kann.

Migrationen im Detail[Bearbeiten]

  • Migrationen anlegen
  • Tabellen anlegen
  • Tabellen ändern
  • Tabellen löschen

Eine Migration anlegen[Bearbeiten]

Wir können eine Migration mit script/generate migration migration_name explizit anlegen. Beispielsweise für die User-Tabelle:

script/generate migration create_users

Oder wir können sie implizit anlegen, indem wir ein Modell oder ein Applikationsgerüst erzeugen.

script/generate model user

bzw.

script/generate scaffold user

Da wir nichts über die Tabellenfelder gesagt haben, ist die Migration zunächst leer.

class CreateUsers < ActiveRecord::Migration
  def self.up
  end

  def self.down
  end
end

Sie enthält aber schon die Methoden für das Ausführen der Migration (self.up) und für das zurücknehmen (self.down).

Wenn wir noch die Tabellenfelder und ihren Typ als Paare (feld_name:typ) übergeben ist die Migration gefüllt.

script/generate scaffold something name:string description:text size:float rating:integer

Bei Rails 2.1 werden als Standard zusätzlich timestamps eingefügt. Der erzeugte Code sieht dann so aus.

class CreateSomethings < ActiveRecord::Migration
  def self.up
    create_table :somethings do |t|
      t.string :name
      t.text :description
      t.float :size
      t.integer :rating

      t.timestamps
    end
  end

  def self.down
    drop_table :somethings
  end
end

wahrscheinlich werden wir noch die eine oder andere Kleinigkeit ergänzen wollen, beispielsweise

      t.string :name, :limit => 100, :null => false

Aber dazu später mehr.

Eine Tabelle anlegen[Bearbeiten]

Wie wir gerade gesehen haben, legen wir eine Tabelle mit create_table an.

  def self.up
    create_table :somethings do |t|
      t.string :name
      ..
    end
  end

Wenn wir eine Tabelle, in der self.up-Methode anlegen, sollten wir sie in der self.down-Methode wieder löschen.

  def self.down
    drop_table :somethings
  end

Typen für Datenbankfelder[Bearbeiten]

In allen Datenbanken sind die folgenden Standardtypen verfügbar:

  • :string, :text, :binary,
  • :integer, :float, :decimal,
  • :datetime, :timestamp, :time, :date,
  • :boolean

.

Und so werden sie von den Datenbanken umgesetzt:

db/type db2 mysql openbase oracle postgresql sqlite sqlserver sybase
:binary blob(32768) blob object blob bytea blob image image
:boolean decimal(1) tinyint(1) boolean number boolean boolean bit bit
:date date date date date date date datetime datetime
:datetime timestamp datetime datetime date timestamp datetime datetime datetime
:decimal decimal decimal decimal decimal decimal decimal decimal decimal
:float float float float number float float float(8) float(8)
:integer int int(11) integer number(38) integer integer int int
:string varchar(255) varchar(255) char(4096) varchar2(255) note(1) varchar(255) varchar(255) varchar(255)
:text clob(32768) text text clob text text text text
:time time time time date time datetime datetime time
:timestamp timestamp datetime timestamp date timestamp datetime datetime timestamp

Es können auch andere Typen verwendet werden, wenn die Datenbank sie unterstützt. Beispielsweise "polygon" in MySql. Dann verlieren wir aber die Datenbankunabhängigkeit. Deshalb sollten wir so etwas nur tun, wenn es einen triftigen Grund dafür gibt.

Optionen[Bearbeiten]

Wo das sinnvoll ist können wir Optionen ergänzen. Ein Beispiel:

      t.string :name, :limit => 100, :null => false

Verfügbare Optionen:

  • :limit - maximale Spaltenlänge. Bei :string und :text -Spalten ist dies die Anzahl der Buchstaben. Bei :binary und :integer -Spalten die Anzahl der Bytes.
  • :default - Der Standardwert der Spalte. nil bedeutet NULL
  • :null - erlaubt oder verbietet NULL-Werte in der Spalte.
  • :precision - gibt die Genauigkeit bei einer :decimal -Spalte an.
  • :scale - gibt die Skala bei einer :decimal -Spalte an.

Eine Tabelle ändern[Bearbeiten]

Die Befehle create_table und drop_table kennen wir schon:

create_table(name, options) # legt eine Tabelle an, Optionen s.u.
drop_table(name) # löscht eine Tabele

Zusätzlich gibt es einen Befehl um Tabellennamen zu ändern:

rename_table(old_name, new_name) # ändert den Namen der Tabelle

Optionen (bei create_table):

  • :force => true # wenn es schon eine Tabelle mit dem gleichen Namen gibt, wird sie vorher gelöscht.
  • :temporary => true # die Tabelle wird automatisch gelöscht, wenn die Applikation die Verbindung zur Datenbank löst (disconnect).
  • :id => false - erstellt eine Tabelle ohne Primärschlüssel, beispielsweise für eine Join-Tabelle
  • :primary_key => :new_primary_key_name - verwendet den neuen Namen für den Primärschlüssel
  • :options => ".." - spezielle Optionen, z.B.: "auto_increment = 100". Die Optionen dürfen datenbankspezifisch sein.

Üblich ist nur ":force => true".

Tabellenspalten ändern[Bearbeiten]

Auch die Tabellenspalten können wir verändern. Das sind die Befehle dazu:

add_column(table_name, column_name, type, options) # fügt eine Spalte ein
rename_column(table_name, column_name, new_column_name) # benennt eine Spalte anders. 
    # Typ and Inhalt bleiben erhalten.
change_column(table_name, column_name, type, options) # verändert den Typ einer spalte. 
    # Die Parameter sind dieselben, wie bei add_column.
remove_column(table_name, column_name) # Löscht eine Spalte

Und so sehen sie in einer Migration aus:

class ChangeSomethings < ActiveRecord::Migration
  def self.up
    add_column :somethings, :short_description, :string
    rename_column :somethings, :description, :long_description 
  end
 
  def self.down
    remove_column :somethings, :short_description
    rename_column :somethings, :long_description, :description
  end
end

Auch hier gibt es ab Rails 2.1 eine verkürzte ("sexy") Syntax. Mit change_table müssen wir den Tabellennamen nur einmal angeben.

class ChangeSomethings < ActiveRecord::Migration
  def self.up
      change_table :somethings do |t|
          t.string :short_description
          t.rename :description, :long_description
      end
  end
 
  def self.down
      change_table :somethings do |t|
          t.remove :short_description
          t.rename :long_description, :description
      end
  end
end

Folgende changetable-Operationen sind möglich:

  • t.column # die alten, "non-sexy" Migrationen
  • t.remove # Spalte löschen
  • t.index, t.remove_index
  • t.timestamps # fügt zwei Spalten ein: created_at und updated_at
  • t.remove_timestamps # löscht beide Spalten: created_at und updated_at
  • t.change # ändert den Typ einer Spalte (von Typ_1 zu Typ_2)
  • t.change_default # Ändert den Default-Wert einer Spalte
  • t.rename # Nennt eine Spalte um (von Name_1 zu Name_2)
  • t.references # fügt eine foreign-key Spalte ein, Konvention: [column_name]_id
  • t.remove_references # löscht einen foreign key
  • t.belongs_to, t.remove_belongs_to # alias für :references, :remove_references
  • t.string, t.text, t.binary
  • t.integer, t.float, t.decimal
  • t.datetime, t.timestamp, t.time, t.date
  • t.boolean

Daten migrieren[Bearbeiten]

Mit Migrationen können wir auch Daten verändern oder laden. Beispielsweise könnten wir wegen der Rohstoffknappheit alle Preise um 15% anheben.

class IncreasePrice << ActiveRecord::Migration
  def self.up
    Product.update_all("price = price * 1.15") 
  end

  def self.down
    Product.update_all("price = price / 1.15") 
  end
end

Oder vordefinierte Benutzer aus einer Fixture-Datei laden.

  def self.up
    ..
    Fixtures.create_fixtures(directory, "users") 
  end

Irreversible Migrationen[Bearbeiten]

Manchmal schreiben wir Migrationen, die wir nicht zurücknehmen können (bzw. nicht automatisch zurücknehmen können). Solche irreversible oder "destruktive" Migrationen sollten in der down Methode eine ActiveRecord::IrreversibleMigration Exception werfen.

"Up and Down" - Migrationen ausführen[Bearbeiten]

Wenn wir und unsere Kollegen neue Migrationen geschrieben haben, wollen wir sie in der Regel alle ausführen um die Datenbank auf den aktuellen Stand zu bringen. Das machen wir mit

rake db:migrate

Rake führt dann alle anstehenden (pending) Migrationen auf die aktuell eingestellte Datenbank aus und in der schema_migrations Tabelle werden sie als ausgeführt eingetragen.

Wir können aber auch zu einer alten Version downgraden. Dazu müssen wir nur die Versionsnummer angeben. Wie wir schon wissen ist das in Rails 2.1 der Zeitstempel mit dem der Dateiname der Migration beginnt.

rake db:migrate VERSION=20080402122523	

Mit :up und :down Befehlen können wir sogar eine bestimmte Migration ausführen

rake db:migrate:up VERSION=20080402122523

oder zurücknehmen

rake db:migrate:down VERSION=20080402122523

Natürlich können wir irreversible Migrationen nicht zurücknehmen. Aber wenn wir sauber programmiert haben, wirft Rails eine ActiveRecord::IrreversibleMigration Exception und wir wissen, dass wir doch noch einmal manuell an die Datenbank ran müssen.

Statt einer Zusammenfassung[Bearbeiten]

Statt einer Zusammenfassung möchte ich hier die Punkte wiederholen, die gern von Rails Neulingen missverstanden werden, aber nach diesem Kapitel klar sein sollen. Der folgende Abschnitt heißt deshalb: "Sie haben Migrationen nicht verstanden, wenn ..". Auf die falschen Behauptungen folgt eine kurze Erklärung wie Rails wirklich tickt und oft auch eine Erläuterung, warum.

Sie haben Migrationen nicht verstanden, wenn ..[Bearbeiten]

  • .. Sie glauben, dass die self.down()- Methode überflüssig ist.

Migrationen sind nicht ohne Grund versioniert. Manchmal wollen wir zu einer alten Version zurückkehren. Dann brauchen wir die down Methode. In der Regel sollten wir sie testen. Schließlich sind in der Datenbank wichtige Daten und da wollen wir und unsere Kollegen uns darauf verlassen können, dass die Migrationen funktionieren und dass dabei keine vermeidbaren Datenverluste auftreten.

  • .. Sie glauben, dass nach herunter- und wieder heraufmigrieren die Datenbank wieder genau wie vorher aussieht.

Das lässt sich leider nicht immer realisieren. Wenn beim Migrieren Datenfelder oder Tabellen gelöscht werden, kann man mit einer Rück-Migration nur die Felder und Tabellen wieder anlegen. Die Daten, die dort standen, bleiben verloren. Allerdings lassen sich manche Datenverluste vermeiden. So sollten wir, wenn wir etwas umbenennen wollen, die dafür vorgesehenen Funktionen benutzen. Löschen und neu anlegen würde die Daten zerstören.

P.S.: Wenn wir Daten löschen müssen, die wir später wieder brauchen, sollten wir eine konventionelle Lösung in Betracht ziehen: ein Backup der Datenbank.

  • .. Sie glauben, dass sie mit down-Migrationen zu jedem alten Datenbankzustand zurückkehren können.

Leider sind manchmal irreversible oder destruktive Migrationen nötig. Irreversible Migrationen sollten in der down Methode eine ActiveRecord::IrreversibleMigration Exception werfen.

  • .. Sie glauben, dass man durch Migrationen nur die Struktur der Datenbank ändern kann.

Selbstverständlich haben wir auch in Migrationen die gesamte Funktionalität von Ruby on Rails zur Verfügung. Damit können wir, wie oben beschrieben, gezielt Daten anlegen, verändern oder löschen. Beispielsweise können wir Lookup-Tabellen per Migration einspielen. Da die Migrationen alle auch in der Produktionsumgebung eingespielt werden, sollten wir so nur Daten anlegen oder verändern, die wir wirklich in der Produktionsumgebung haben wollen. Für Testdaten gibt es andere Wege.