Doctrine : les migrations avec le bundle DoctrineMigrationsBundle (2/3)

Maintenant que nous avons installé notre bundle dédié aux migrations des données via Doctrine, nous pouvons créer le bundle qui va servir de support à notre exercice et que nous allons appeler Scolarite car il est censé matérialiser le fonctionnement de notre université, la fabrique de développeurs ! Voici le résumé de la création de notre bundle en mode interactif :

Symfony/app/console generate:bundle
Bundle namespace: FabriqueDeDevs/ScolariteBundle
Bundle name [FabriqueDeDevsScolariteBundle]: 
Target directory [/home/sebastien.ferrandez/migrations/Symfony/src]: 
Configuration format (yml, xml, php, or annotation): yml
Do you want to generate the whole directory structure [no]? yes
You are going to generate a "FabriqueDeDevs\ScolariteBundle\FabriqueDeDevsScolariteBundle" bundle
in "/home/sebastien.ferrandez/migrations/Symfony/src/" using the "yml" format.

Generating the bundle code: OK
Checking that the bundle is autoloaded: OK
Confirm automatic update of your Kernel [yes]? 
Enabling the bundle inside the Kernel: OK
Confirm automatic update of the Routing [yes]? 
Importing the bundle routing resource: OK

doctrine-logo

Les cours à la fabrique de développeurs

Nous souhaitons modéliser de manière très simpliste la gestion des cours; des élèves s’inscrivent chez nous qui choisissent des filières composées d’un certain nombre de cours. Ces cours sont assurés par des enseignants dans des salles. Nous établissons les règles suivantes, de manière tout à fait arbitraire :

  • un élève appartient à une seule filière
  • une filière est composée d’un ou plusieurs cours
  • un cours n’a qu’une seule filière
  • un enseignant a un ou plusieurs cours
  • un cours n’a qu’un seul et unique enseignant mais un enseignant a un ou plusieurs cours
  • un cours se tient dans une seule salle à un moment donné et une salle peut abriter plusieurs cours. On peut très bien créer un cours sans salle car il arrive qu’on ne puisse décider de suite dans quelle salle il se tiendra, celle-ci sera rajoutée plus tard.

J’ai mis en gras les objets du domaine d’application qui vont donc être éligibles au titre d’entité. Nous allons commencer par les plus simples et nous allons tout générer en ligne de commande sans passer par le mode interactif :

Symfony/app/console generate:doctrine:entity --no-interaction \

--entity=FabriqueDeDevsScolariteBundle:Eleve \

--fields="nom:string(40) prenom:string(40) age:integer"

Ce que nous allons faire systématiquement c’est changer l’annotation Doctrine laissée vierge et qui contient le nom que la table cible va avoir.
Rendons nous dans le fichier nouvellement généré :

vi Symfony/src/FabriqueDeDevs/ScolariteBundle/Entity/Eleve.php

Et rajoutons la ligne en gras dans son entête:

/**
 * Eleve
 *
 * @ORM\Table(name="eleve")
 * @ORM\Entity
 */

Maintenant que nous avons généré une première entité, notre schéma de bases de données va devoir refléter ce changement, c’est à dire passer de l’état initial 0 à l’état 1 « une table élève est à créer ».
Nous faisons usage de l’argument diff :

Symfony/app/console doctrine:migrations:diff

Un fichier est généré :

Generated new migration class to "/home/sebastien.ferrandez/migrations/Symfony/app/DoctrineMigrations/Version20130420192803.php" from schema differences.

Vous devinez aisément le format de ces fichiers : VersionYYYYMMDDHHMMSS. Voyons ce que celui-ci contient :

class Version20130420192803 extends AbstractMigration
{
    public function up(Schema $schema)
    {
        // this up() migration is autogenerated, please modify it to your needs
        $this->abortIf($this->connection->getDatabasePlatform()->getName() != "mysql");

        $this->addSql("CREATE TABLE eleve (id INT AUTO_INCREMENT NOT NULL, nom VARCHAR(40) NOT NULL, prenom VARCHAR(40) NOT NULL, age INT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB");
    }

    public function down(Schema $schema)
    {
        // this down() migration is autogenerated, please modify it to your needs
        $this->abortIf($this->connection->getDatabasePlatform()->getName() != "mysql");

        $this->addSql("DROP TABLE eleve");
    }
}

Vous voyez d’emblée deux méthodes, up et down; la première est exécutée dans le sens ascendant, c’est à dire en cas de migration et l’autre dans le sens descendant, dans le cas d’une rétro-migration (d’un rollback). On retrouve dans l’un les actions antagonistes de l’autre, si dans up vous avez un create table, vous aurez un drop table dans le down.
C’est dans ce fichier que vous pouvez changer les types de données dans vos requêtes SQL avant génération des éléments de la base de données ! Quoiqu’il en soit, les changements du diff ne sont pas appliqués à la base de données tant que vous n’avez pas utilisé la commande migrate :

Symfony/app/console doctrine:migrations:migrate

Voici ce que me donne à l’écran l’exécution de cette commande :

Migrating up to 20130420192803 from 0

  ++ migrating 20130420192803

     -> CREATE TABLE eleve (id INT AUTO_INCREMENT NOT NULL, nom VARCHAR(40) NOT NULL, prenom VARCHAR(40) NOT NULL, age INT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB

  ++ migrated (0.08s)

  ------------------------

  ++ finished in 0.08
  ++ 1 migrations executed
  ++ 1 sql queries

Je migre bien de l’état 0 vers l’état 20130420192803 en faisant un CREATE TABLE eleve. Tout se passe conformément à ce que nous imaginions…Continuons avec notre deuxième entité, celle qui concerne nos enseignants :

Symfony/app/console generate:doctrine:entity --no-interaction \

--entity=FabriqueDeDevsScolariteBundle:Enseignant \

--fields="nom:string(40) prenom:string(40) age:integer"

Nous allons faire la même chose que précédemment :

  • donner, dans notre entité, un nom à la table qui va être créée
  • faire un diff
  • faire un migrate

Le diff produit un fichier de migration qui va cette fois concerner la table enseignant, dont nous venons de mettre le nom dans l’entité qui va la gérer. Le migrate, lui, va donner :

WARNING! You are about to execute a database migration that could result in schema changes and data lost. Are you sure you wish to continue? (y/n)y
Migrating up to 20130420214445 from 20130420192803

  ++ migrating 20130420214445

     -> CREATE TABLE enseignant (id INT AUTO_INCREMENT NOT NULL, nom VARCHAR(40) NOT NULL, prenom VARCHAR(40) NOT NULL, age INT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB

  ++ migrated (0.05s)

  ------------------------

  ++ finished in 0.05
  ++ 1 migrations executed
  ++ 1 sql queries

Nous en profitons pour voir comment évolue les tuples de table migration_versions, gérée par Doctrine :

sebastien.ferrandez@sebastien:~/migrations$ mysql -u root -p -e 'use exercice; select * from migration_versions'
Enter password: 
+----------------+
| version        |
+----------------+
| 20130420192803 |
| 20130420214445 |
+----------------+

Vous voyez que toute commande migrate entraîne la création d’une ligne dans cette table ! Nous continuons avec la création des autres entités :

Symfony/app/console generate:doctrine:entity --no-interaction \
--entity=FabriqueDeDevsScolariteBundle:Salle \
--fields="nom:string(40)"

Symfony/app/console generate:doctrine:entity --no-interaction \
--entity=FabriqueDeDevsScolariteBundle:Cours \
--fields="nom:string(40) date:datetime duree:integer"

Symfony/app/console generate:doctrine:entity --no-interaction \
--entity=FabriqueDeDevsScolariteBundle:Filiere\
--fields="nom:string(40)"

L’idée est d’isoler chaque changement du schéma de base de données (pour faire plus simple, chaque création de table) dans une migration. Si nous passons une gigantesque migration qui contient TOUT, en cas de nécessité de rétro-migrer, nous nous retrouverions à faire un grand pas en arrière au lieu de reculer à petits pas.
Nous avons fini avec les entités, nous allons à présent dans la dernière partie, « raccorder » celles qui doivent l’être ! A suivre…

symfony_black_03

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.