Expert Topics

Wie man mit Doctrine-Migrationen in Symfony arbeitet

7 Kommentare // Lesezeit: 13 min.

Dieser Artikel ist Teil der Serie. Der erste Artikel behandelte die Erstellung eines ersten Symfony 2-Projekts, der zweite befasst sich mit Symfony 2 Formularen, und der dritte Artikel beschreibt eine schöne und einfache Möglichkeit, eine REST-API mit dem Symfony-Framework zu implementieren.

Einführung

Nehmen wir an, Sie haben einige Änderungen am Modell Ihres Symfony-Projekts vorgenommen, den Code festgeschrieben, und er wird in der Produktion angewendet. Aber Sie müssen auch die Produktionsdatenbank aktualisieren. Sie könnten den Befehl: php app/console doctrine:schema:update ausführen, aber dann könnten Sie Ihre Daten verlieren (das hängt von den Modelländerungen ab, die Sie vorgenommen haben). Der richtige und datensichere Weg ist die Verwendung von Doctrine-Migrationen. Doctrine kann automatisch eine Migration für Sie erzeugen, aber diese Migration enthält den gleichen SQL-Code wie der Befehl doctrine:schema:update und kümmert sich nicht um die vorhandenen Daten. Um die Daten zu migrieren, müssen Sie die Migrationsdatei ändern, worauf wir später noch eingehen werden. Fangen wir also an.

1. Doctrine-Migrationspaket installieren

Wenn das Doctrine Migrations-Bundle noch nicht in Ihrem Symfony-Projekt enthalten ist, fügen Sie diese Zeile in der Sektion "require" Ihrer composer.json-Datei hinzu:

"doctrine/migrations": "1.0.*@dev"

Passen Sie die Version an, wenn es eine neuere gibt, und führen Sie composer update aus (oder führen Sie composer install aus, wenn Sie Ihre anderen Abhängigkeiten nicht aktualisieren wollen).

Vergessen Sie auch nicht, das Bundle in Ihrer AppKernel-Datei hinzuzufügen:

2. Erzeugen der Migrationsdatei

Nachdem Sie Änderungen am Modell vorgenommen haben (Sie haben einige Felder einiger Entitäten hinzugefügt/gelöscht, oder Sie haben ganze Entitäten hinzugefügt/gelöscht), können Sie die Migrationsdatei erstellen, indem Sie den Befehl ausführen:

php app/console doctrine:migrations:diff 

Sie werden feststellen, dass eine neue Datei zu Ihrem Projekt im Verzeichnis hinzugefügt wird:

app/DoctrineMigrations

Die Datei wird etwa so benannt:

Version20151118233337.php

Sie enthält den Zeitstempel des Erstellungszeitpunkts: VersionJJJJMMDDhhmmss.php. Auf diese Weise sind die Migrationsnamen eindeutig und richtig geordnet, und die Reihenfolge der Ausführung der Migrationen ist wichtig. Aber die Lehre kümmert sich um all das für Sie.

Bevor Sie die Migration durchführen, möchten Sie vielleicht einen Blick in die Datei werfen, um zu prüfen, ob das generierte SQL in Ordnung ist. Wenn alles in Ordnung ist und keine Notwendigkeit besteht, die Daten zu migrieren (wir kommen später darauf zurück), können Sie die Migration durchführen.

Bevor ich fortfahre, möchte ich auf die Methoden der Migrationsklasse eingehen. Standardmäßig enthält die automatisch generierte Migrationsklasse (die in der Migrationsdatei enthalten ist und den gleichen Namen wie die Datei trägt) zwei Methoden: up und down. Die Up-Methode wird ausgeführt, wenn wir zu einer neueren Version migrieren, und die Down-Methode migriert zurück zu einer älteren Version des Projekts. Sie können das im folgenden Beispiel sehen. Ich habe das title Feld zur person Tabelle hinzugefügt. Bei einer Aufwärtsmigration wird das Feld hinzugefügt, bei einer Abwärtsmigration wird es gelöscht.


Braucht Ihr Unternehmen einen Webspezialisten, mit dem es auf Augenhöhe sprechen kann?

 


3. Ausführen der Migration

php app/console doctrine:migrations:migrate

Dies ist der Hauptbefehl, mit dem Sie die Migrationen ausführen werden. Er führt alle neuen (nicht ausgeführten) Migrationen aus. Bevor Sie ihn ausführen, können Sie überprüfen, welche Migrationen neu sind, und viele weitere Informationen erhalten, indem Sie diesen Befehl ausführen:

php app/console doctrine:migrations:status

Es gibt noch weitere Migrationsbefehle:

php app/console doctrine:migrations:generate
php app/console doctrine:migrations:execute
php app/console doctrine:migrations:version
php app/console doctrine:migrations:latest

Aber sie sind für uns jetzt nicht so interessant. Um mehr Informationen über einen Befehl und seine Verwendung zu erhalten, führen Sie den Befehl mit dem Argument --help aus.

4. Migrieren Sie die Daten

Das Beispiel zeigt eine Migration, die die Adressfelder aus der Personentabelle in eine separate Tabelle - Adresse - verschiebt und in der Personentabelle darauf verweist. Die Migration, wie Sie sie oben sehen können, kümmert sich nur um die Datenbankstruktur und nicht um die Daten. Wenn die Anwendung jedoch bereits in der Produktion läuft und Daten enthält, müssen wir auch die Daten migrieren. In diesem Beispiel müssen wir die Adressdaten (Straße, Straßennummer, Ort, ...) kopieren. Wir werden also in diesen drei Schritten vorgehen:

  1. Erstellen Sie die Adresstabelle und legen Sie den Fremdschlüssel address_id in der Personentabelle an.
  2. Kopieren Sie alle Adressdaten aus der Personentabelle in die Adresstabelle.
  3. Löschen Sie die Adressfelder aus der Personentabelle.

Die Schritte 1 und 3 sind bereits vorhanden, wir müssen sie nur trennen und richtig anordnen. Schritt 2 ist der knifflige Teil, und wir müssen ihn implementieren.

Was genau müssen wir in Schritt 2 tun?  Wir müssen die Adressdaten jeder Person nehmen und sie in die Adresstabelle einfügen. Dann müssen wir jede Person mit ihrer Adresse verknüpfen, indem wir die richtige address_id in die Personentabelle einfügen. Das wäre einfach, wenn es ein eindeutiges Adressfeld gibt (wir können address_id nicht verwenden, weil es in der person Tabelle nicht existiert - wir müssen es jetzt einfügen), oder wenn es eine eindeutige Kombination der Adressfelder gibt, dann können wir die Daten durch Ausführen dieser SQL-Abfragen kopieren:

Das Problem ist jedoch, dass es 2 oder mehr Personen mit der gleichen Adresse geben kann. Und jeder von ihnen sollte seine Kopie der Adresse in der address Tabelle haben (sie hätten nur unterschiedliche IDs). Daher kann die obige SQL in diesem Beispiel nicht verwendet werden.

Wir können die Daten kopieren, indem wir die person Tabelle iterieren, und in jeder Iteration lesen/wählen wir die Adressdaten aus der person aus, fügen die Daten in die address tabelle ein, holen die address.id und aktualisieren sie in der person Tabelle in derselben Zeile, aus der wir die Daten gelesen haben. Wie ist das zu implementieren?

Wir können Iterationen in SQL machen, aber wir müssen eine gespeicherte Prozedur erstellen, um die Aufgabe zu erledigen, und wir können die Iterationen auch in PHP machen. Das Problem mit dem PHP-Ansatz ist, dass wir nicht mit unseren Daten innerhalb der up-Funktion arbeiten können, weil der gesamte SQL-Code, der durch die $this->addSql-Methode hinzugefügt wird, am Ende der up-Funktion ausgeführt wird. Die Lösung ist einfach:

Wir werden die postUp-Funktion verwenden, die nach der up-Funktion ausgeführt wird. Auf diese Weise wird sichergestellt, dass alle Tabellen bereits erstellt sind, wenn wir die Datenmigration durchführen. Daher sollte auch der SQL-Prozedur-Ansatz in die postUp-Funktion implementiert werden. So sollte es aussehen:

4.1 Migration der Daten mit Hilfe von mySQL Stored Procedure

Wir haben diese 3 Teilschritte durchgeführt:

  1. Erstellen Sie die gespeicherte Prozedur.
  2. Führen Sie es aus (call)
  3. Löschen Sie es (drop)

Wie Sie sehen können, verwenden wir in der Prozedur einen CURSOR, um nur Adressfelder auszuwählen, und iterieren dann die Auswahl (in einer Schleife), solange Daten verfügbar sind. Innerhalb jeder Iteration fügen wir die Daten in die Adresstabelle ein, holen dann die address.id mit der Funktion LAST_INSERT_ID() und aktualisieren sie in der Personentabelle.

Lesen Sie hier mehr über mySQL Cursors.

4.2 Verwendung von PHP für die Verarbeitung der Daten in der postUp-Methode

In der postUp-Methode können wir sogar den Entitymanager verwenden, aber zuerst müssen wir ihn aus dem Kontext abrufen. Wir müssen also die kontextbezogene Migration durchführen:

Dies ist ein anderes Beispiel als zuvor. Wir machen die Unternehmenstabelle aus dem String-Feld work, das in der Kundentabelle definiert ist. Und diese Kunden sind eigentlich Angestellte des Unternehmens (das macht jetzt nicht viel Sinn, weil Sie in diesen Beispielen kein vollständiges Modell sehen, aber akzeptieren Sie es einfach so, wie es ist).

Ich muss gleich sagen, dass dieses Beispiel einen Fehler hat! Das work Feld existiert nicht mehr in der Customer Klasse, und wir versuchen, es zu erhalten. Wir können uns also nicht auf Entitäten - Objekte - verlassen, da ihr Code mit dem aktuellen Datenbankstatus inkonsistent sein kann. Ein weiteres Beispiel für die Inkonsistenz zwischen dem Code und der Migration ist, wenn die Migration in der Zukunft ausgeführt wird. Nehmen wir an, ein Benutzer erstellt eine neue Datenbank. Wenn er den Befehl migrate ausführt, werden alle Migrationen ausgeführt, und der Code - Entitäten sind von der neuesten Version und einige der Entitäten/Felder, die in der Migration existieren, könnten im Code gelöscht werden. Es könnte also sein, dass einige Migrationen nicht funktionieren.

Die Lösung für dieses Problem ist einfach: Verwenden Sie keine Entitäten innerhalb von Migrationen, Sie dürfen sich nicht auf den Entitätscode (PHP) verlassen, sondern nur auf die Datenbank!

Verwenden Sie also weder den Entitymanager noch DQL, sondern greifen Sie einfach über Doctrine\DBAL\Connection auf die Datenbank zu, wie im nächsten Teil des Codes:

Der obige Code erstellt Firmen (fügt Firmennamen in die Firmentabelle ein) aus dem Feld customer.work string.

Die beste Lösung ist jedoch die Verwendung einfacher SQL-Abfragen, wenn dies möglich ist. Beispiel für die Erstellung von Unternehmen auf der Grundlage des Feldes customer.work und die Verbindung der Kunden mit ihrem Unternehmen:

5. Alles in allem, im Großen und Ganzen

Lassen Sie uns das Wichtigste zusammenfassen:

  • Verwenden Sie den Konsolenbefehl, um die Migration zu erstellen (php app/console doctrine:migrations:diff)
  • Ändern Sie die Migration, um die Daten zu erhalten/zu migrieren.
  • Verwenden Sie die postUp-Methode für die Migration der Daten.
  • Führen Sie alle relevanten Ausscheidungen am Ende der postUp-Methode durch.
  • Wenn es möglich ist, führen Sie einfache SQL-Abfragen aus, um die Daten zu migrieren.
    • Wenn einfaches SQL die Aufgabe nicht erfüllen kann, verwenden Sie eine gespeicherte SQL-Prozedur mit Schleifen.
    • Oder verwenden Sie PHP in der postUp-Methode, aber stellen Sie sicher, dass Sie sich nur auf die Datenbank verlassen
      • keine Entitäten verwenden, sondern Doctrine\DBAL\Connection ($this->connection) verwenden
  • Führen Sie die Migrationen aus (php app/console doctrine:migrations:migrate)

Viel Spaß bei den Wanderungen. Fragen, Diskussionen und Kommentare sind willkommen. :)

Kontaktieren Sie uns!

Wir sind eine Digitalagentur, die sich auf die Entwicklung digitaler Produkte spezialisiert hat. Unsere Kernthemen sind Webseiten und Portale mit TYPO3, eCommerce mit Shopware und Android und iOS-Apps. Daneben beschäftigen wir uns mit vielen weiteren Themen im Bereich Webentwicklung. Kontaktieren Sie uns gerne mit Ihren Anliegen!

Kommentare

  • Stefan Galinski

    Stefan Galinski

    am 23.11.2015

    Very helpful, Damjan! Very helpful, Damjan!

  • Martins

    Martins

    am 20.12.2015

    Thanks! Thanks!

  • Olivier

    Olivier

    am 28.04.2016

    Hi Damjan !

    Very nice and helpful introduction to Doctrine Migrations.

    I am currently working on a SaaS web app. It means all our customers share the same backend Symfony application but each [...] Hi Damjan !

    Very nice and helpful introduction to Doctrine Migrations.

    I am currently working on a SaaS web app. It means all our customers share the same backend Symfony application but each one as its own database. Is it helpful to use Migrations in this case and why ?

    Thank you

  • Saint-Cyr

    Saint-Cyr

    am 28.05.2016

    Nice post !
    Olivier at your place I would do separate the whole project like one Symfony application for only one Data Base because normally designing a Data base is a standalone project (or [...] Nice post !
    Olivier at your place I would do separate the whole project like one Symfony application for only one Data Base because normally designing a Data base is a standalone project (or sub-project) and designing an application to communicate to is another one and most of the time the application structure (data structure), logic (algorithms), ... all rely on the DB but the reverse is not. But according to your case if more than one customer have the same business logic ( they can share one DB) then you can share the DB among them in this case but steel there is a probability that one day one of them ask for a feature that require change of DB schema and this might not be the desire of the other....

  • tomasz

    tomasz

    am 27.03.2017

    Any clues on debugging except var_dumping? Any clues on debugging except var_dumping?

  • Christian Elowsky

    Christian Elowsky

    am 23.10.2017

    One quick note. For this section where you execute the query, the results should actually be retrieved via a call to fetchAll() like so:
    $queryBuilder = [...] One quick note. For this section where you execute the query, the results should actually be retrieved via a call to fetchAll() like so:
    $queryBuilder = $this->connection->createQueryBuilder();
    $queryBuilder
    ->select('customer.work')
    ->from('customer', 'customer')
    ->groupBy('customer.work')
    ->orderBy('customer.work');
    $stmt = $queryBuilder->execute();
    $customersGroupedByWork = $stmt->fetchAll();

    foreach ($customersGroupedByWork as $customerGroupedByWork) {
    // Insert statement by DoctrineDBALConnection
    $this->connection->insert('company', ['name' => $customerGroupedByWork['work']]);

  • Liam

    Liam

    am 26.11.2019

    Nice guide! Nice guide!