Ruckusing - PHP entwickeln mit Datenbank-Migrationen
- Collaboration-Software%
- Enterprise Portals%
- Lifecycle-Management%
- Interface-Design50%
- Web Applications50%
Ein Programmierer verändert oft nicht nur den Source-Code, sondern auch die Datenbank.
Die Source-Code-Änderung checkt er ins Repository ein. Aber wie verfährt er mit der Datenbank-Änderung?
Möglichst unabhängig Änderungen an dem Datenbank-Schema vorzunehmen, nachvollziehbar nach zuhalten und im Team bekannt zu machen, sind Herausforderungen, die es in Softwareprojekten zu meistern gilt. Dieser Artikel beleuchtet unterschiedliche Vorgehensweisen und stellt Ruckusings Migrations als interessante Lösung vor.
Datenbankserver in der Entwicklung:
Einer je Entwickler vs. einer fürs Team
Üblich ist es, dass ein Team den Source-Code in einem Repository verwaltet und jeder Entwickler stets mit einer lokale Arbeitskopie auf seinen Computer, auf dem er einen eigenen Webserver betreibt, arbeitet, um zum Beispiel Funktionalitäten auszuprobieren.
Um die Eingangs gestellte Frage 'Wie verfährt ein Programmierer mit Datenbank-Änderungen?' zu beantworten, sollte man zwei unterschiedliche Ansätze in der Entwicklung betrachten: Zum einen das alle Entwickler mit einem zentralen Datenbankserver arbeiten, zum anderen das jeder einen lokalen Datenbank-Server zur Entwicklung auf seinen Computer betreibt.
Bei ersterem stellt sich die Frage wer Schema-Änderungen durchführen darf, jeder Entwickler oder nur ausgewählte, und vor allem wie werden fehlerhafte Änderungen rückgängig gemacht. Ein Zurückspringen zu einer alten Datenbankversion ist nicht so leicht möglich, wie das Zurückspringen zu einer alten Source-Code-Version mittels Repository. Schnell wird deutlich das viel Handarbeit und Koordination durch Absprachen nötig ist. Eine separate Dokumentation der Schema-Änderungen muss sehr sorgfältig geführt werden, um die Änderungen später ins Produktivsystem zu übertragen. Ein großes Manko bei diesem Ansatz ist, abgesehen von dem aufwändigen Prozess der Schema-Änderungen, das Entwickler beim Ausprobieren von Features sich schnell gegenseitig blockieren, da sie wohl möglich an den gleichen Datensätze arbeiten. Selbst in einem Team mit guter Kommunikation führt dies schnell zu Frustration.
Wenn jeder Entwickler einen lokalen Datenbank-Server betreibt, ist das zuletzt beschriebene große Manko bereits ausgeschlossen. Allerdings ergeben sich andere Herausforderungen: Wie können Schema-Änderungen eines Programmierers an die anderen übermittelt werden? Wie kann sichergestellt werden, dass der Source-Code stets an einer Version des Datenbank-Schemas gebunden ist? Sind die Änderungen nachvollziehbar? Und gegebenenfalls leicht rückgängig machbar?
Bei all diesen Problemen hilft Ruckusing. Zusätzlich hilft es bei der wichtigen Frage: Wie spiele ich eigentlich alle Änderungen des Datenbank-Schema bei einem neuem Stable-Release ins Produktivsystem ein und das ohne Daten zu verlieren?
Wie arbeitet Ruckusing?
Möchte ein Entwickler das Datenbankschema ändern, schreibt er eine sogenannte Migration. In dieser Migration, die eine sehr simple PHP-Klasse ist, beschreibt er Änderungen an der Datenbank. Zusätzlich beschreibt er wie diese Änderung wieder rückgängig gemacht werden kann. Diese Beschreibung kann sowohl in PHP selbst als auch in SQL erfolgen. Der Vorteil es mit Ruckusing-Migrationen in PHP zu machen ist, dass die Migration unabhängiger vom eigentlichen Datenbanksystem ist. Adapter-Klassen für mehrere Datenbanksysteme sind möglich. Die Migrations können mittels Ruckusing über die Konsole in der Datenbank eingespielt werden. Dabei spielt Ruckusing standardmäßig nur neue Migrations ein; dies wird über eine Datenbank-Versionsnummer aufgelöst. Um dies zu Realisieren ist eine Migration einer Versionsnummer zugeordnet und die Datenbank enthält eine Tabelle in der die aktuell eingespielte Versionsnummer gespeichert ist.
Zum besseren Verständnis des Aufbaus einer Migration, betrachten wir erst einmal die Beispiel-Migration '001_CreateUsersTable.php', die mit Ruckusing mitgeliefert wird.
class CreateUsersTable extends Ruckusing_BaseMigration {
public function up() {
$t = $this->create_table('users');
$t->column('age', 'integer');
$t->column('name', 'string');
$t->finish();
}
public function down() {
$this->drop_table('users');
}
}
Dem Dateinamen ist die Versionsnummer 1 und der Klassenname zu entnehmen, der die Änderung an der Datenbank beschreiben soll. In der up-Funktion wird durch Aufruf der Migrations-Methode eine Tabelle 'users' mit den Spalten 'age' und 'name' erzeugt und in der down-Funktion wird diese Tabelle 'users' gelöscht, also die Änderungen aus der up-Funktion rückgängig gemacht.
Installation und erstes Ausprobieren
Ruckusing ist OpenSource und steht auf der Projekthomepage zur Verfügung. Um es zum Beispiel in einer Web-Applikation, aufbauend auf dem Zend Framework, auszuprobieren, empfiehlt es sich Ruckusing in den Ordner library/ruckusing zu kopieren. Darauf muss die Datenbank-Konfiguration in library/ruckusing/config/database.inc.php überarbeitet werden.
Als nächstes sollte man manuell die Datenbanken für die drei Environments 'development', 'production' und 'test' anlegen, zum Beispiel mit Hilfe von phpmyadmin.
Ausgehend vom Projekt-Verzeichnis kann man nun mit
php library/ruckusing/main.php db:setup
die Datenbank initialisieren. Das heißt, in der Datenbank wird - falls sie nicht existiert - eine Tabelle 'schema_info' angelegt in der die Versionsnummer ablegt wird.
Im Verzeichnis library/ruckusing/db/migrate liegen schon sieben Beispiel-Migrationen, die sich mit
php library/ruckusing/main.php db:migrate
einspielen lassen.
Alle Aufrufe werden standardmäßig auf die Development-Environment ausgeführt, möchte man sie auf der Production-Environment ausführen, muß man ENV=production als Parameter übergeben, z.B.
php library/ruckusing/main.php db:migrate ENV=production
Analog funktioniert das mit ENV=test.
Möchte man nun eine weitere Migration hinzufügen, kann man in dem Migration-Ordner eine neue Klasse anlegen, aber am einfachsten läßt man sich deren Gerüst mit Hilfe von
php library/ruckusing/generate.php add_email_to_users
erzeugen.
In der Datei 'library/ruckusing/db/migrate/008_AddEmailToUsers.php' findet sich nun folgendes Klassengerüst:
class AddEmailToUsers extends Ruckusing_BaseMigration {
public function up() {
}//up()
public function down() {
}//down()
}
In der up-Funktion kann die Migration mit den Migrations-Methoden beschrieben werden. In der down-Funktion sollte beschrieben werden, wie dies wieder rückgängig wird.
class AddEmailToUsers extends Ruckusing_BaseMigration {
public function up() {
$this->add_column('users', 'email', 'string');
}//up()
public function down() {
$this->remove_column('users', 'email');
}//down()
}
Durch Verwendung der Methode execute() lässt sich hier auch direkt SQL einbinden, dies sollte man jedoch -wenn möglich- vermeiden, da dies bei einem späteren Wechsel auf ein anderes Datenbankmanagementsystem sich als aufwändig erweisen kann.
Folgende Methoden können in Migrations verwendet werden:
// Erzeuge Datenbank 'mein_projekt'
$this->create_database("mein_projekt");
// Lösche Datenbank 'mein_projekt'
$this->drop_database("mein_projekt");
// Beginnt die Erzeugung einer Tabelle 'tabelle1' und fügt zwei
// Spalten 'vorname' und 'nachname' des Typs 'String' hinzu.
// Mit finish() wird die Erzeugung abgeschlossen.
$this->create_table("tabelle1") ;
$users->column("vorname","string");
$users->column("nachname","string");
$users->finish();
// Löscht die Tabelle 'tabelle1'.
$this->drop_table("tabelle1");
// Bennent die Tabelle 'tabelle1' in 'besserer_name' um.
$this->rename_table("tabelle1", "besserer_name");
// Ändert bei der Tabelle 'person' den Spalten-Typ der Spalte
// 'vorname' in den Typ 'String' mit einer
// Längenbegrenzung von 128 Zeichen.
$this->change_column("person", "vorname", "string",
array('limit' => 128) );
// Fügt der Tabelle 'person' einen Index auf die Spalte 'nr' hinzu,
// der Index hat den Namen 'index_name' und erhält
// zusätzlich das Unique-Constraint.
$this->add_index("person", "nr",
array('unique' => true, 'name' => 'index_name'));
// Der Index auf der Spalte 'email' in der Tabelle 'users'
// wird entfernt.
$this->remove_index("users", "email");
// Mit diesen Methoden lassen sich direkt SQL-Abfragen ausführen.
$this->execute("UPDATE ... ");
$this->select("...");
$this->select_all("...");
Die Spalten-Typen werden wie folgt in MySQL abgebildet:
|
Spalten-Typ |
MySQL-Spalten-Typ |
|---|---|
|
String |
varchar (255) |
|
Text |
text |
|
integer |
int (11) |
|
float |
float |
|
decimal |
decimal |
|
datetime |
datetime |
|
time |
time |
|
date |
date |
|
binary |
blob |
|
boolean |
tinyint (1) |
Wenn man bei den Migrations Destruktivität in der up-Funktion vermeidet, wie zum Beispiel beim Hinzufügen einer Spalte anstatt add_column() zuerst drop_table() mit anschließenden create_table() zu verwenden, können diese Migrations ohne Datenverlust im Produktivsystem eingespielt werden. Falls diese von allen Entwicklern bereits verwandt wurden, was in Teams eigentlich der Fall sein sollte, sind diese vor dem Deployment bereits mehrfach ausgeführt worden, so dass es zu keinen bösen Überraschungen kommen sollte.
Durch Aufruf von migrate wird nun die neue Migration eingespielt.
$ php library/ruckusing/main.php db:migrate Started: 2010-05-19 8:20am UTC [db:migrate]: Migrating UP to: 9 (current version: 7) ========= AddEmailsToUsers ======== (0.01) Finished: 2010-05-19 8:20am UTC
Einen Überblick über alle Tasks der main.php, abgesehen von den bereits erwähnten db:setup und db:migrate, gibt folgende Tabelle:
|
Task |
Beschreibung |
|---|---|
|
db:setup |
Initialisieren der Datenbank. Erzeugt die Tabelle schema_info, die die aktuelle Versionsnummer enthält. |
|
db:migrate |
Migriert – falls nötig – die Datenbank zur aktuellsten Version. |
|
db:migrate VERSION=6 |
Migriert die Datenbank up bzw. down zur Version 6 |
|
db:schema |
Speichert das aktuelle Datenbankschema als SQL in eine Textdatei. |
|
db:version |
Gibt die aktuelle Versionsnummer der Datenbank aus. |
|
db:setversion |
Überschreibt die Datenbank-Versionsnummer hart. (Vorsicht!) |
|
Um Tasks auf ein Environment zu beziehen, kann ENV=production oder ENV=test als Parameter übergeben werden. Der Standard-Wert ist ENV=development. |
|
Wissenswertes
Ruckusing orientiert sich stark an ActiveRecord::Migrations von Ruby on Rails, daher kommt wohl auch die für PHP-Entwickler ungewohnte Konvention die Funktionsnamen auch in Klassen mit einem Underscore zu trennen anstatt CamelCase zu verwenden.
Ruckusing ist sehr modular aufgebaut. Von Haus aus unterstützt Ruckusing (zumindest in der aktuellen Version 2.93) zwar nur MySQL, es läßt sich aber durch eigene Adapter-Klassen schnell auf andere Datenbanken erweitern. Des weiteren lässt es sich durch die Implementation des Interface Ruckusing_iTask ebenfalls sehr leicht um weitere Tasks erweitern.
Die Einbindung von Ruckusing in einer Zend-Framework-Applikation kann um einiges eleganter gestaltet werden, als es hier erfolgt ist. Wünschenswert wäre, daß
-
die Migrations direkt im Projektordner unter db/migrate liegen.
-
die Datenbank-Konfiguration nicht für die Applikation und Ruckusing redundant bearbeitet werden müssen.
-
der Aufruf der main.php und generate.php verkürzt entweder, durch ein alias oder eines weiteren Skripts
-
alternativ wäre für viele Entwicklungs-Teams eine Integration ins Project-Build-System Phing interessant, dies könnte wie bei dbdeploy über eine eigene Task gehen
Fazit
Mit Ruckusing werden Datenbank-Änderungen Paketweise in einzelne Dateien abgelegt und können so zusammen mit den Source-Code-Änderungen ins Repository commited werden. Andere Entwickler spielen nach dem Aktualisieren der Arbeitskopie vom Repository einfach neue Migrations durch einen Aufruf ein. Die mitgebrachten Tools erleichtern durch das automatisierte Anlegen der Migrations die Arbeit. Datenbankänderungen sind nun wie Source-Code-Änderungen im Repository einfach nachvollziehbar, sowie leicht revidierbar.
