Ruby on Rails: ActiveRecord: Migrationen
<< Architektur | Startseite/Inhaltsverzeichnis | CRUD >>
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
BearbeitenWie 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
BearbeitenWir 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
BearbeitenWie 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
BearbeitenIn 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
BearbeitenWo 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
BearbeitenDie 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
BearbeitenAuch 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
BearbeitenMit 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
BearbeitenManchmal 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
BearbeitenWenn 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
BearbeitenStatt 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.