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:
public function registerBundles() {
$bundles = array(
...
new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(),
);
...
}
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.
<?php
namespace SGalinski\LicenceManagement\Migrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20151118233337 extends AbstractMigration
{
/**
* @param Schema $schema
*/
public function up(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE person ADD title VARCHAR(255) DEFAULT NULL');
}
/**
* @param Schema $schema
*/
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('ALTER TABLE person DROP title');
}
}
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
public function up(Schema $schema)
{
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('CREATE TABLE address (id INT AUTO_INCREMENT NOT NULL, street VARCHAR(255) DEFAULT NULL, street_number VARCHAR(255) DEFAULT NULL, postal_code VARCHAR(255) DEFAULT NULL, city VARCHAR(255) DEFAULT NULL, country VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB');
$this->addSql('ALTER TABLE person ADD address_id INT DEFAULT NULL, DROP address_street, DROP address_street_number, DROP address_postal_code, DROP address_city, DROP address_country');
$this->addSql('ALTER TABLE person ADD CONSTRAINT FK_34DCD176F5B7AF75 FOREIGN KEY (address_id) REFERENCES address (id)');
$this->addSql('CREATE INDEX IDX_34DCD176F5B7AF75 ON person (address_id)');
}
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:
- Erstellen Sie die Adresstabelle und legen Sie den Fremdschlüssel address_id in der Personentabelle an.
- Kopieren Sie alle Adressdaten aus der Personentabelle in die Adresstabelle.
- 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:
INSERT INTO address (street, street_number, postal_code, city, country)
SELECT address_street, address_street_number, address_postal_code, address_city, address_country
FROM person;
UPDATE person, address
SET person.address_id = address.id
WHERE address.street = person.address_street
AND address.street_number = person.address_street_number
AND address.postal_code = person.address_postal_code
AND address.city = person.address_city
AND address.country = person.address_country;
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
public function postUp(Schema $schema) {
// Copy addresses from person to address table
$this->connection->exec('
CREATE PROCEDURE copy_address()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE var_street, var_street_number, var_postal_code, var_city, var_country VARCHAR(255);
DECLARE var_person_id INT;
DECLARE var_address_id INT;
DECLARE address_cursor CURSOR FOR SELECT id, address_street, address_street_number, address_postal_code, address_city, address_country FROM person;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN address_cursor;
read_loop: LOOP
FETCH address_cursor INTO var_person_id, var_street, var_street_number, var_postal_code, var_city, var_country;
IF done THEN
LEAVE read_loop;
END IF;
INSERT INTO address (street, street_number, postal_code, city, country) VALUES (var_street, var_street_number, var_postal_code, var_city, var_country);
SET var_address_id = LAST_INSERT_ID();
UPDATE person SET address_id = var_address_id WHERE id = var_person_id;
END LOOP;
CLOSE address_cursor;
END;
CALL copy_address();
DROP PROCEDURE IF EXISTS copy_address;'
);
// Dropping - step 3
$this->connection->exec('ALTER TABLE person DROP address_street, DROP address_street_number, DROP address_postal_code, DROP address_city, DROP address_country, DROP active_from, DROP active_to');
}
Wir haben diese 3 Teilschritte durchgeführt:
- Erstellen Sie die gespeicherte Prozedur.
- Führen Sie es aus (call)
- 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:
class Version20151111205453Copy extends AbstractMigration implements ContainerAwareInterface
{
/**
* @var ContainerInterface
*/
private $container;
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
// ...
public function postUp(Schema $schema) {
/** @var EntityManager $em */
$em = $this->container->get('doctrine.orm.entity_manager');
// Copy work from customer to company table
// Use PHP code in migrations:
$queryBuilder = $this->connection->createQueryBuilder();
$queryBuilder
->select('customer.work')
->from('customer', 'customer')
->groupBy('customer.work')
->orderBy('customer.work');
$customersGroupedByWork = $queryBuilder->execute();
foreach ($customersGroupedByWork as $customerGroupedByWork) {
// Persisting by Doctrine\ORM\EntityManager
$company = new Company();
$company->setName($customerGroupedByWork['work']);
$em->persist($company);
/** @var CustomerRepository $customerRepository */
$customerRepository = $em->getRepository('CompanyManagementBundle:Customer');
$allCustomers = $customerRepository->findAll();
/** @var Customer $customer */
foreach ($allCustomers as $customer) {
if ($customer->getWork() === $company->getName()) {
$company->addEmployee($customer);
}
}
$em->flush();
}
}
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:
// Copy work from customer to company table
// Use PHP code in migrations:
$queryBuilder = $this->connection->createQueryBuilder();
$queryBuilder
->select('customer.work')
->from('customer', 'customer')
->groupBy('customer.work')
->orderBy('customer.work');
$customersGroupedByWork = $queryBuilder->execute();
foreach ($customersGroupedByWork as $customerGroupedByWork) {
// Insert statement by Doctrine\DBAL\Connection
$this->connection->insert('company', ['name' => $customerGroupedByWork['work']]);
// ...
// ...
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:
public function postUp(Schema $schema) {
// ...
// Copy work from customer to company table
$this->connection->exec(
'INSERT INTO company (name)
SELECT customer.work
FROM customer
GROUP BY customer.work
ORDER BY customer.work;'
);
$this->connection->exec(
'INSERT INTO companies_employees (company_id, customer_id)
SELECT company.id, customer.id
FROM company, customer
WHERE company.name = customer.work;'
);
// ...
}
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
am 23.11.2015Very helpful, Damjan! Very helpful, Damjan!
Martins
am 20.12.2015Thanks! Thanks!
Olivier
am 28.04.2016Hi 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
am 28.05.2016Nice 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
am 27.03.2017Any clues on debugging except var_dumping? Any clues on debugging except var_dumping?
Christian Elowsky
am 23.10.2017One 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
am 26.11.2019Nice guide! Nice guide!