Rails 5.1 Consulting und Schulung vom Autor:
www.wintermeyer-consulting.de/rails/

4.16. Migrations (Migrationen)

SQL-Datenbank-Tabellen werden in Rails mit Migrations generiert, und sie können ebenso mit Migrations verändert werden. Wenn Sie mit rails generate model ein Model anlegen, dann wird automatisch im Verzeichnis db/migrate/ eine entsprechende Migration-Datei angelegt. Ich zeige Ihnen das Prinzip an einer Shop-Applikation. Legen wir die mal an:
MacBook:~ xyz$ rails new shop
[...]
MacBook:~ xyz$ cd shop
MacBook:shop xyz$
Jetzt legen wir ein Product-Model an:
MacBook:shop xyz$ rails generate model product name 'price:decimal{7,2}' weight:integer in_stock:boolean expiration_date:date
      invoke  active_record
      create    db/migrate/20120508160146_create_products.rb
      create    app/models/product.rb
      invoke    test_unit
      create      test/unit/product_test.rb
      create      test/fixtures/products.yml
MacBook:shop xyz$
Es wurde die Migrations-Datei db/migrate/20120508160146_create_products.rb angelegt. Wir schauen wir uns die genauer an:
class CreateProducts < ActiveRecord::Migration
  def change
    create_table :products do |t|
      t.string :name
      t.decimal :price, :precision => 7, :scale => 2
      t.integer :weight
      t.boolean :in_stock
      t.date :expiration_date

      t.timestamps
    end
  end
end
Die Klassenmethode change erstellt und löscht bei einem Rollback die Datenbank-Tabelle. Die Migrationsdateien haben im Dateinamen die aktuelle Uhrzeit eingebettet und werden bei einer Migration (also dem Aufruf von rake db:migrate) in der chronologischen Reihenfolge abgearbeitet.
MacBook:shop xyz$ rake db:migrate
==  CreateProducts: migrating =================================================
-- create_table(:products)
   -> 0.0017s
==  CreateProducts: migrated (0.0018s) ========================================

MacBook:shop xyz$
Dabei werden immer nur die Migrations abgearbeitet, die noch nicht ausgeführt wurden. Wenn wir jetzt noch einmal rake db:migrate aufrufen, passiert nichts, da die entsprechende Migration ja schon ausgeführt wurde:
MacBook:shop xyz$ rake db:migrate
MacBook:shop xyz$
Wenn wir aber die Datenbank mit rm manuell löschen und danach wieder rake db:migrate aufrufen, wird die Migration wiederholt:
MacBook:shop xyz$ rm db/development.sqlite3 
MacBook:shop xyz$ rake db:migrate
==  CreateProducts: migrating =================================================
-- create_table(:products)
   -> 0.0017s
==  CreateProducts: migrated (0.0018s) ========================================

MacBook:shop xyz$ 
Nach einiger Zeit merken wir, dass wir bei vielen Produkten nicht nur das Gewicht (height), sondern auch die Höhe (height) abspeichern möchten. Wir benötigen also ein weiteres Datenbank-Feld . Dafür gibt es eine leicht zu merkende Syntax rails generate migration add_*:
MacBook:shop xyz$ rails generate migration add_height_to_product height:integer
      invoke  active_record
      create    db/migrate/20120508160821_add_height_to_product.rb
MacBook:shop xyz$ 
In der Migrationsdatei db/migrate/20120508160821_add_height_to_product.rb findet sich wieder eine change-Methode:
class AddHeightToProduct < ActiveRecord::Migration
  def change
    add_column :products, :height, :integer
  end
end
Mit rake db:migrate können wir die neue Migration einspielen:
MacBook:shop xyz$ rake db:migrate
==  AddHeightToProduct: migrating =============================================
-- add_column(:products, :height, :integer)
   -> 0.0049s
==  AddHeightToProduct: migrated (0.0050s) ====================================

MacBook:shop xyz$
In der Console können wir uns das neue Feld anschauen. Es wurde nach dem Feld updated_at angefügt:
MacBook:shop xyz$ rails console
Loading development environment (Rails 3.2.3)
1.9.3p194 :001 > Product
 => Product(id: integer, name: string, price: decimal, weight: integer, in_stock: boolean, expiration_date: date, created_at: datetime, updated_at: datetime, height: integer) 
1.9.3p194 :002 > exit
MacBook:shop xyz$ 

Warnung

Bitte beachten Sie, dass Sie wahrscheinlich in app/models/product.rb das neue Feld in attr_accessible eintragen müssen. Sonst haben Sie keinen Zugriff auf das height Attribute.
Was wäre in dem Fall, dass wir noch einmal den vorherigen Stand der Dinge betrachten möchten? Das ist kein Problem. Auf die vorherige Version können wir mit rake db:rollback leicht zurückgehen:
MacBook:shop xyz$ rake db:rollback
==  AddHeightToProduct: reverting =============================================
-- remove_column("products", :height)
   -> 0.0357s
==  AddHeightToProduct: reverted (0.0358s) ====================================

MacBook:shop xyz$
Jede Migration hat eine eigene Versionsnummer. Die Versionsnummer des aktuellen Status können Sie mit rake db:version herausfinden:
MacBook:shop xyz$ rake db:version
Current version: 20120508160146
MacBook:shop xyz$ 

Wichtig

Bitte beachten Sie, dass alle Versionsnummern und Zeitstempel nur für das hier abgedruckte Beispiel gelten. Wenn Sie das Beispiel nacharbeiten, bekommen Sie natürlich einen für Sie aktuellen Zeitstempel.
Sie finden die entsprechende Version im Verzeichnis db/migrate wieder:
MacBook:shop xyz$ ls db/migrate 
20120508160146_create_products.rb
20120508160821_add_height_to_product.rb
MacBook:shop xyz$
Auf eine bestimmte Migration können Sie mit rake db:migrate VERSION=Versionsnummer gehen. Die Null steht dabei für die nullte Version (also den Start). Probieren wir das mal aus:
MacBook:shop xyz$ rake db:migrate VERSION=0
==  CreateProducts: reverting =================================================
-- drop_table("products")
   -> 0.0007s
==  CreateProducts: reverted (0.0007s) ========================================

MacBook:shop xyz$ 
Die Tabelle wurde mit allen Daten gelöscht. Wir sind wieder zurück auf Los.

Welche Datenbank wird benutzt?

Die Datenbank-Tabelle wird durch die Migration angelegt. Dabei sehen wir, dass Tabellen-Namen automatisch den Plural der Models bekommen (Person vs. people). Aber in welcher Datenbank werden die Tabellen überhaupt angelegt? Das wird in der Konfigurationsdatei config/database.yml definiert:
# SQLite version 3.x
#   gem install sqlite3
#
#   Ensure the SQLite 3 gem is defined in your Gemfile
#   gem 'sqlite3'
development:
  adapter: sqlite3
  database: db/development.sqlite3
  pool: 5
  timeout: 5000

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  adapter: sqlite3
  database: db/test.sqlite3
  pool: 5
  timeout: 5000

production:
  adapter: sqlite3
  database: db/production.sqlite3
  pool: 5
  timeout: 5000
Dort werden im YAML-Format (siehe http://www.yaml.org/ bzw. http://de.wikipedia.org/wiki/YAML) drei verschiedene Datenbanken definiert. Für uns ist erst mal nur die development-Datenbank (erster Eintrag) wichtig. Per default benutzt Rails dort SQLite3. SQLite3 mag nicht die richtige Wahl für die Analyse der weltweit gesammelten Wetterdaten sein, aber für die schnelle und unkomplizierte Entwicklung von Rails-Anwendungen lernt man sie schnell zu schätzen. In der Produktions-Umgebung kann man später immer noch auf große Datenbanken wie MySQL oder PostgreSQL umsteigen.[26]
Um Ihre sicherlich vorhandene Neugierde zu befriedigen, schauen wir uns kurz noch die Datenbank mit dem Command-Line-Tool sqlite3 an:
MacBook:shop xyz$ sqlite3 db/development.sqlite3 
SQLite version 3.7.7 2011-06-25 16:35:41
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .tables
schema_migrations
sqlite> .quit
MacBook:shop xyz$  
Nichts drin. Klar, wir haben ja auch nicht die Migration laufen lassen:
MacBook:shop xyz$ rake db:migrate  
==  CreateProducts: migrating =================================================
-- create_table(:products)
   -> 0.0273s
==  CreateProducts: migrated (0.0274s) ========================================

==  AddHeightToProduct: migrating =============================================
-- add_column(:products, :height, :integer)
   -> 0.0009s
==  AddHeightToProduct: migrated (0.0010s) ====================================

MacBook:shop xyz$ sqlite3 db/development.sqlite3 
SQLite version 3.7.7 2011-06-25 16:35:41
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .tables
products           schema_migrations
sqlite> .schema products
CREATE TABLE "products" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255), "price" decimal(7,2), "weight" integer, "in_stock" boolean, "expiration_date" date, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL, "height" integer);
sqlite> .quit
MacBook:shop xyz$ 
Die Tabelle schema_migrations wird zur Versionierung der Migrationen benutzt. Bei der ersten von Rails durchgeführten Migration wird diese Tabelle angelegt, falls sie noch nicht vorhanden ist.

Index anlegen

Ich gehe davon aus, dass Sie wissen, was ein Index in einer Datenbank ist. Falls nicht, finden Sie auf http://de.wikipedia.org/wiki/Datenbankindex eine kurze Einführung. Kurzversion: Damit werden Suchen für eine bestimmte Tabellen-Spalte schneller.
In unserer Produktdatenbank sollten wir das Feld name in der Tabelle products indizieren. Dafür erstellen wir eine neue Migration:
MacBook:shop xyz$ rails generate migration create_index
      invoke  active_record
      create    db/migrate/20120508162243_create_index.rb
MacBook:shop xyz$
In der Datei db/migrate/20101201224922_create_index.rb legen wir mit add_index in der Methode self.up den Index an, und in der Methode self.down löschen wir ihn mit remove_index wieder:
class CreateIndex < ActiveRecord::Migration
  def up
    add_index :products, :name
  end

  def down
    remove_index :products, :name
  end
end
Mit rake db:migrate wird der Index angelegt:
MacBook:shop xyz$ rake db:migrate
==  CreateIndex: migrating ====================================================
-- add_index(:products, :name)
   -> 0.0009s
==  CreateIndex: migrated (0.0009s) ===========================================

MacBook:shop xyz$  

Tipp

Einen Index können Sie auch direkt beim Generieren des Models mit anlegen. In unserem Fall (einem Index beim Attribute name) sähe der Befehl so aus:
MacBook:shop xyz$ rails generate model product name:string:index 'price:decimal{7,2}' weight:integer in_stock:boolean expiration_date:date
      invoke  active_record
      create    db/migrate/20120508160146_create_products.rb
      create    app/models/product.rb
      invoke    test_unit
      create      test/unit/product_test.rb
      create      test/fixtures/products.yml
MacBook:shop xyz$

Sonstiges

In diesem Anfängerbuch kann ich das Thema Migrations nicht sehr tief behandeln. Es geht um das prinzipielle Verständnis der Mechanik. Es gibt aber ein paar Details, die so wichtig sind, dass ich sie hier besprechen möchte.

Automatisch zugefügte Felder (id, created_at und updated_at)

Rails ist so freundlich, uns bei der Default-Migration automatisch folgende Felder hinzuzufügen:
  • id:integer
    Das ist die Unique-ID des Datensatzes. Das Feld wird von der Datenbank automatisch hochgezählt. Für alle SQL-Fans: NOT NULL AUTO_INCREMENT.
  • created_at:datetime
    Das Feld wird von ActiveRecord automatisch beim Erstellen eines Datensatzes gefüllt.
  • updated_at:datetime
    Das Feld wird bei jeder Veränderung des Datensatzes automatisch an die aktuelle Zeit angepasst.
Man muss diese Felder also nicht beim Generieren des Models selber eintragen.
Am Anfang fragt man sich: Muss das sein? Macht das wirklich Sinn?. Aber mit der Zeit lernt man diese automatischen Felder zu schätzen. Sie wegzulassen, ist meist am falschen Ende gespart.

Weitere Dokumentation

Folgende Webseiten geben Ihnen sehr gute weitere Informationen zum Thema Migration:


[26] Einige Entwickler sind der Ansicht, dass man auf jeden Fall immer mit der gleichen Datenbank entwicklen soll, die man später auch in der Produktion und im Testing benutzt. Andere sagen, dass diese bei der Verwendung von ORM-Abstraktions-Layern nicht notwendig sei. Bitte entscheiden Sie hier selber. Ich programmiere eine Rails-Applikation oft mit SQLite und verwende in der Produktion oft MySQL.