17 - Migrations

Introduction

Jusqu’ici nous avons agi comme si l’application bâtie sur le framework et la base de données étaient deux entités indépendantes, qui évoluaient chacune de son coté, sans vraiment tenir compte de l’autre. C’est une possibilité et vous pouvez bâtir votre framework sur cette idée.

Toutefois, la plupart des applications web aujourd’hui utilisent une base de données créée pour l’occasion. Il est donc intéressant de faire évoluer la base de données en même temps que l’application. On peut même oublier SQL et utiliser le langage du framework pour gérer les modifications de la base de données. C’est le concept de migration.

Une première migration

Comme nous avons déjà une base de données, Sequel va nous permettre de créer une migration qu’on pourra utiliser pour répliquer notre base existante, par exemple sur Heroku. Pour cela nous utiliserons le programme sequel avec l’option -d :

sequel -d chaine_de_connexion

Vous pouvez copier/coller la chaîne de connexion. Mais puisque cette chaîne réside dans le fichier db/configuration, autant se servir du shell. La commande suivante devrait fonctionner avec la plupart des shells :

Afficher le contenu de la base comme une migration
$ cat db/configuration | xargs sequel -d

Avec Bash, j’aurais tendance à utiliser la commande suivante, qui fait la même chose :

Une autre façon de faire, avec Bash
$ sequel -d $(cat db/configuration)

Avec Fish, le shell que j’utilise au quotidien, la commande est encore plus simple :

Pareil, mais avec Fish
$ sequel -d (cat db/configuration)

Quelque soit la méthode que vous allez choisir, elle va afficher dans le terminal le contenu de notre première migration avec Sequel :

Contenu de la base, d’après Sequel
$ sequel -d postgres://framework:password@localhost:5432/framework_blog
Sequel.migration do
  change do
    create_table(:posts) do
      primary_key :id
      String :title, :text=>true
      String :content, :text=>true
      DateTime :date
    end
  end
end

Pour conserver les migrations, rangeons les dans un nouveau dossier :

Création du dossier db/migrations
$ mkdir db/migrations

Chaque migration devra être placée dans un fichier Ruby nommé d’après le pattern version_description.rb. La description pourra être le texte qui vous plaira mais la version devra répondre à un schéma précis. Soit elle sera un numéro d’ordre, comme 001, 002, 003, etc. Soit elle sera une date, comme 20170401, 20170412, 20170423, etc. Soit elle sera un ensemble date + time, comme 20170401120035, 20170401173302, etc. Il faudra veiller à ne pas mélanger les schémas. Le plus simple étant le numéro d’ordre, c’est celui que nous allons utiliser. Écrivez votre première migration dans un fichier 01_create_posts.rb :

Fichier Ruby contenant la migration
 1 # Fichier db/migrations/01_create_posts.rb
 2 Sequel.migration do
 3   change do
 4     create_table(:posts) do
 5       primary_key :id
 6       String :title, :text=>true
 7       String :content, :text=>true
 8       DateTime :date
 9     end
10   end
11 end

Maintenant que nous avons une belle migration toute neuve, nous allons apprendre à l’utiliser en local. Supprimez la table posts de la base de données. N’hésitez pas à ouvrir deux consoles en même temps, une pour psql et une autre pour sequel. En SQL on supprime une table et son contenu avec drop table :

Supprimer une table avec SQL
drop table posts;

Avec l’utilitaire sequel, on lance les migrations avec l’option -m :

sequel -m dossier/des/migrations chaîne_de_connexion

Ce qui, avec le shell Bash, nous donnera la commande suivante :

Lancer la migration avec sequel
$ sequel -m db/migrations/ $(cat db/configuration)

Lorsque vous lancerez cette commande, ne soyez pas surpris si elle n’affiche rien. Ce sera parfaitement normal et signifiera que tout s’est bien déroulé. Vous pourrez alors regarder le contenu de votre base avec la commande \d dans psql. Vous devriez y voir deux tables (plus une sequence qui est en gros la description de l’incrémentation de la clé primaire de la table posts) :

\d
              List of relations
 Schema |     Name     |   Type   |   Owner   
--------+--------------+----------+-----------
 public | posts        | table    | framework
 public | posts_id_seq | sequence | framework
 public | schema_info  | table    | framework
(3 rows)

La table schema_info est ajoutée par Sequel pour gérer les migrations et contient la «version actuelle», c’est à dire le numéro d’ordre de la dernière migration effectuée.

Voir le contenu de la table schema_info
framework_blog=# select * from schema_info;
 version 
---------
       1
(1 row)

Ajouter une seconde migration

Je voudrais ajouter un gif optionnel aux articles. Pour cela je vais créer une migration avec la version 2 et le nom add_gif_to_posts. Cette migration utilisera la méthode add_column pour ajouter la colonne gif à la table posts. Le gif sera représenté par du code HTML, comme celui que nous fourni Giphy. Voici un exemple de code HTML que fourni ce service :

Code pour afficher un GIF venant de Giphy
<iframe src="//giphy.com/embed/tyxovVLbfZdok" width="480" height="301.2" frameBo
rder="0" class="giphy-embed" allowFullScreen></iframe> <p> <a href="https://giph
y.com/gifs/movie-happy-excited-tyxovVLbfZdok">via GIPHY</a> </p>

Nous aurons donc besoin d’une colonne de type text, et nous pouvons consulter la liste des types pour les migrations dans la documentation de Sequel. Ceci nous conduit à écrire la migration suivante :

Une seconde migration
1 # Fichier db/migrations/02_add_gif_to_posts.rb
2 Sequel.migration do
3   change do
4     add_column :posts, :gif, String, :text=>true
5   end
6 end

Il faut afficher ce nouveau champ dans nos formulaires :

Un champ pour le GIF
<!--  Fichier views/shared/form_fields.html.erb -->
<label>Titre
  <input type="text" name="title" id="title" value="<%= @post.title %>">
</label>

<label>Contenu
  <input type="text" name="content" id="content" value="<%= @post.content %>">
</label>

<label>Gif
  <input type="text" name="gif" id="gif" value="<%= h(@post.gif) %>">
</label>

La méthode h permet d’afficher le code HTML comme du texte (faites l’essai avec un formulaire de mise à jour sans cette méthode pour vous rendre compte de son utilité). La méthode h est fournie par le module ERB::Util et nous devons donc l’inclure dans notre framework :

On utilise le module ERB::Util
# Fichier lib/base_controller.rb
class BaseController
  # ...
  include ERB::Util
  # ...
end

Il faut maintenant que le contrôleur sache quoi faire de ce nouveau champ. Il est donc nécessaire de modifier les méthodes create et update de la classe PostsController :

On sauvegarde le GIF dans le contrôleur
class PostsController < BaseController
  def create
    Post.create(title: params["title"],
                content: params["content"],
                gif: params["gif"],
                date: Time.now)
    notice("Post créé avec succès")
    redirect_to "/posts"
  end

  def update
    post = Post[params["id"]]
    post.update(title: params["title"],
                content: params["content"],
                gif: params["gif"])
    redirect_to "/posts"
  end
end

Puis nous devons l’afficher dans les posts avec <%= @post.gif %>. Si un post n’a pas de gif, la méthode .gif retournera nil et rien ne sera affiché. Dans le cas contraire cela affichera le code HTML du gif :

Affichage du GIF
<!-- views/posts/show.html.erb -->
<h2><%= @post.title %></h2>
<div><i><%= Time.at @post.date %></i></div>
<p><%= @post.content %></p>

<%= @post.gif %>

<p>
  <a href="/posts/delete?id=<%= @post.id %>">
    Supprimer (mais ne venez pas pleurer après !)
  </a>
</p>

Généralement nous ne souhaitons pas manipuler quotidiennement des programmes en ligne de commande tel que sequel. En Ruby nous préférons rassembler toutes les commandes possibles dans des tâches Rake. Sans entrer dans les détails, Rake est l’équivalent Ruby de Make. Quand il y a peu de tâches, on utilise généralement un seul fichier nommé Rakefile. Le fichier suivant définit une tâche nommée db:migrate.

Une tâche Rake pour migrer
 1 # Fichier Rakefile
 2 namespace :db do
 3   desc "Run migrations"
 4   task :migrate do |t|
 5     puts "Migrating…"
 6     require "sequel"
 7     Sequel.extension :migration
 8     connexion = ENV["DATABASE_URL"] || File.read("db/configuration").chomp
 9     db = Sequel.connect(connexion)
10     Sequel::Migrator.run(db, "db/migrations")
11     puts "Done."
12   end
13 end

Ligne 6, nous chargeons la gem Sequel et ligne 7, nous chargeons la partie de Sequel qui concerne les migrations. Enfin ligne 10, nous lançons les migrations, c’est l’équivalent en Ruby de la ligne de commande sequel -m db/migrations $(cat/configuration).

Pour lancer les migrations en local vous utiliserez la commande bundle exec rake db:migrate. Pour les lancer sur heroku la commande sera heroku run rake db:migrate :

Lancer la migration en local
$ bundle exec rake db:migrate
Migrating…
Done.
Lancer la migration sur Heroku
$ heroku run rake db:migrate
Running rake db:migrate on ⬢ quiet-plains-59626... up, run.2190 (Free)
Migrating…
Done.
Le nouveau formulaire
Le nouveau formulaire

Et hop, en local tout fonctionne bien. Mais sur Heroku, vous aurez droit à une erreur, ça marche pas… Pourquoi ? Parce que en local, votre serveur web se lance par défaut en mode development, alors que sur Heroku il est lancé en mode production (nous verrons plus en détails la notion d’environnement dans un prochain chapitre). Et en mode production, le modèle Post n’est pas «réactualisé» après une migration. Voici l’enchaînement des faits en mode production, sur Heroku :

  1. Démarrage de l’application avec une base de données sans tables.
  2. Chargement du modèle Post, à partir de la base de données sans tables.
  3. Migrations, la base de données contient les tables.
  4. Affichage d’un formulaire à partir du modèle Post «vide» de l’étape 2, d’où erreur.

La solution la plus simple consiste à redémarrer l’application Heroku après une migration à l’aide la commande heroku restart pour que les modèles soient rafraichis :

Redémarrer une application sur Heroku
$ heroku restart
Restarting dynos on ⬢ quiet-plains-59626... done

Maintenant vous pouvez poster des articles avec des gifs !

Le résultat en image !
Le résultat en image !