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 :
$ cat db/configuration | xargs sequel -d
Avec Bash, j’aurais tendance à utiliser la commande suivante, qui fait la même chose :
$ sequel -d $(cat db/configuration)
Avec Fish, le shell que j’utilise au quotidien, la commande est encore plus simple :
$ 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 :
$ 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 :
$ 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 :
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 :
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 :
$ 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.
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 :
<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 :
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 :
<!-- 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 :
# 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 :
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 :
<!-- 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.
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 :
$ bundle exec rake db:migrate
Migrating…
Done.
$ heroku run rake db:migrate
Running rake db:migrate on ⬢ quiet-plains-59626... up, run.2190 (Free)
Migrating…
Done.
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 :
- Démarrage de l’application avec une base de données sans tables.
- Chargement du modèle Post, à partir de la base de données sans tables.
- Migrations, la base de données contient les tables.
- 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 :
$ heroku restart
Restarting dynos on ⬢ quiet-plains-59626... done
Maintenant vous pouvez poster des articles avec des gifs !