Active Record : les migrations

Ruby on Rails par la pratique

Vincent Dubois

Dans cet article, faites connaissance avec Active Record en commençant par manipuler des structures de bases de données grâce aux migrations. Commentez ♪

- Titre : Active Record : les migrations

- Auteur : Vincent Dubois

- Parution : 27 septembre 2009

- Mise à jour : 29 décembre 2012

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2012 Vincent Dubois. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Qu'est-ce qu'ActiveRecord ?

- II. Anatomie d'une migration

- II-A. Les migrations sont des classes

- III. Créer une migration

- III-A. Créer un modèle

- III-B. Créer une migration seule

- IV. Écriture d'une migration

- IV-A. Créer une table

- IV-B. Modifier une table

- IV-C. Helpers spéciaux

- V. Exécuter des migrations

- VI. Modéliser l'application « Mes Contacts »

- VII. Conclusion

- VIII. Remerciements

I - Qu'est-ce qu'ActiveRecord ?

ActiveRecord est le composant qui permet la manipulation des données dans les applications Ruby on Rails. ActiveRecord est aussi et avant tout un design pattern pour lier les données d'une base de données à des modèles de programmation. Voici la définition très juste de ce design pattern trouvée sur Wikipedia : « En génie logiciel, le patron de conception (design pattern) active record (enregistrement actif en anglais) est une approche pour lire les données d'une base de données. Les attributs d'une table ou d'une vue sont encapsulés dans une classe. Ainsi, l'objet instance de la classe est lié à un tuple de la base. Après l'instanciation d'un objet, un nouveau tuple est ajouté à la base au moment de l'enregistrement. Chaque objet récupère ses données depuis la base; quand un objet est mis à jour, le tuple auquel il est lié l'est aussi. La classe implémente des accesseurs pour chaque attribut. » Voici un exemple concret : si nous avons une table « contacts » dans la base de données de notre application Rails, alors nous avons forcément une classe Contact qui peut être manipulée via ActiveRecord. Voici un exemple de code :

c = Contact.new(:first_name => "Dubois", :last_name => "Vincent")
c.save   # création du tuple

II - Anatomie d'une migration

Avant de rentrer dans le détail des migrations Active Record, voyons un exemple de migration que nous avons créé dans le tutoriel précédent :

db/migrate/20090422185716_create_contacts.rb

class CreateContacts < ActiveRecord::Migration
def self.up
create_table :contacts do |t|
t.string :name
t.string :email

t.timestamps
end
end

def self.down
drop_table :contacts
end
end

Cette migration ajoute une table « contacts », possédant deux colonnes « name » et « email » de type texte. La méthode « timestamps » quant à elle, ajoute des colonnes d'informations sur les dates de mises à jour des enregistrements. Nous détaillerons tout cela un peu plus tard.

II-A - Les migrations sont des classes

Une migration est une sous-classe de la classe ActiveRecord::Migration. À ce titre, elle définit deux méthodes : une méthode « up », qui permet d'ajouter une modification à la base de données, et une méthode« down », qui permet de retirer cette modification de la base de données. Active Record fournit des méthodes qui permettent de définir des tâches de modification de la structure de la base de données : cette migration ajoute une table « contacts », possédant deux colonnes « name » et « email » de type texte. La méthode « timestamps » quant à elle, ajoute des colonnes d'informations sur les dates de mises à jour des enregistrements. Nous détaillerons tout cela un peu plus tard.

- create_table

- change_table

- drop_table

- add_column

- change_column

- rename_column

- remove_column

- add_index

- remove_index

Les migrations sont effectuées lors de l'exécution de la commande « rake db:migrate ». Si une opération ne se déroule pas correctement, la migration qui a échoué fait l'objet d'un rollback. Les noms des fichiers de migrations sont du type YYYYMMDDHHMMSS_action_entite.rb. Dans le cas de ce tutoriel, le nom du fichier est : 20090418171815_create_contacts.rb. Le nom du fichier est sûrement différent chez vous, il dépend de l'heure de la création de l'entité « Contact ». Tous les fichiers de migrations se situent dans le répertoire : db/migrate/. La date de chaque nouveau fichier de migration étant postérieure à celle du dernier, chaque migration sera toujours exécutée dans l'ordre que vous aurez préétabli et « rollbackée » dans l'ordre inverse. Si vous souhaitez effectuer un « rollback » sur la base de données, il existe une commande qui opère le contraire de «rake db:migrate ». C'est la commande « rake db:rollback ». N'hésitez pas à l'utiliser en cas de besoin.

III - Créer une migration

III-A - Créer un modèle

Lors de la création d'un modèle, la migration adéquate est automatiquement créée. Par exemple, si on exécute la commande (ne le faites pas, car l'entité « Contact » existe déjà) :

ruby script/generate model Contact name:string email:string

Alors, automatiquement, un fichier [yyyymmddhhmmss]_create_contacts.rb est créé (cf. §1). Vous pouvez ajouter à la main autant de colonnes que vous le souhaitez au sein du bloc create_table.

III-B - Créer une migration seule

Vous pouvez si vous le souhaitez créer seulement une migration, hors du cadre de la création d'une entité ou d'un « scaffold ». Procédons par exemple à l'ajout d'un attribut « first_name » correspondant au prénom du contact et changeons l'attribut « name » en « last_name » à cette occasion.

ruby script/generate migration AddFirstNameToContacts

Voici le contenu du fichier de migration par défaut :

class AddFirstNameToContacts < ActiveRecord::Migration
def self.up
end

def self.down
end
end

Évidemment, ça ne fait pas grand-chose pour l'instant. Précisons tout d'abord que les méthodes « up » et « down » opèrent chacune respectivement une montée et une descente de version de migration. Dans ce but, nous allons donc prévoir de quoi revenir sur nos pas, c'est-à-dire envisager la suppression de la colonne « first_name » et le renommage de la colonne « last_name » en « name ». Commençons par la méthode « up » :

def self.up
add_column :contacts, :first_name, :string
rename_column :contacts, :name, :last_name
end

Observons le code. Nous avons tout d'abord ajouté la colonne « first_name » grâce à la méthode « add_column » qui prend en paramètre le nom de la table, le nom de la colonne, le type de colonne et d'autres paramètres éventuels non précisés ici. Ensuite, la méthode « rename_column » nous permet de renommer la colonne « name ». Nous devons maintenant implémenter l'opération inverse dans la méthode « down » :

def self.down
remove_column :contacts, :first_name
rename_column :contacts, :last_name, :name
end

Facile, n'est-ce pas ? Si nous avions seulement voulu ajouter la colonne « first_name », nous aurions même pu le faire en tapant :

ruby script/generate migration AddFirstNameToContacts first_name:string

Et Rails aurait généré pour nous les lignes contenant « add_column » et « remove_column ». À l'inverse, une migration nommée « RemoveFirstNameToContacts » aurait généré une méthode « remove_column » dans la méthode « up » et une méthode « add_column » dans la méthode « down ». Le principe de « convention over configuration » est poussé ici à son paroxysme. Nous pouvons d'ailleurs aller un peu plus loin si nous le souhaitons en ajoutant plusieurs descriptions de colonnes à la commande : elles seront toutes ajoutées à la migration.

IV - Écriture d'une migration

IV-A - Créer une table

L'écriture d'une table passe par le bloc de code create_table. Voici un exemple de code typique :

create_table :contacts, :force => true do |t|
t.string :name
end

L'objet table « t » manipulé par le bloc nous permet de créer des colonnes dans la table. Notons au passage l'usage d'un deuxième paramètre nommé « :force », qui permet de recréer la table dans le cas où elle existe déjà. Il y a deux façons de créer des colonnes. Il y a l'ancienne façon (à l'origine dans les versions antérieures de Rails) :

t.column :name, :string, :null => false

Et il y a la nouvelle manière introduite depuis peu dans Rails, du moins de façon officielle. Les migrations qui l'utilisent sont surnommées « sexy migrations ». C'est celle que nous appliquerons par la suite :

t.string :name, :null => false

Il est intéressant de préciser que nous n'avons pas de colonne d'identifiant à créer. En effet, Rails crée pour nous automatiquement une colonne de nom « id » qui joue le rôle de clé primaire dans la table. Cela dit, il existe des moyens d'outrepasser ceci, mais là encore, il vaut mieux suivre le principe « convention over configuration ». Les types supportés par Active Record lors de la création d'une colonne sont :primary_key, :string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary et :boolean. Il est possible d'utiliser d'autres types de données non supportés de la façon suivante :

t.column :name, 'autre_type', :null => false

Mais le type ne sera supporté probablement que par la base de données que vous utiliserez. C'est donc à utiliser avec précaution.

IV-B - Modifier une table

La méthode change_table, proche cousin de la méthode create_table, est utilisée pour les tables déjà existantes. L'objet manipulé par le bloc de code dispose de plus de méthodes. En voici un exemple :

change_table :contacts do |t|
t.remove :email, :country
t.string :address
t.index :first_name
t.rename :first_name, :firstname
end

Toutes ces méthodes sont des raccourcis. Si nous avions dû créer ceci de façon standard, nous aurions fait :

remove_column :contacts, :email
remove_column :contacts, :country
add_column :contacts, :address, :string
add_index :contacts, :first_name
rename_column :contacts, :first_name, :firstname

Tout cela nous permet de ne pas réécrire plusieurs fois le même code et donc de respecter le principe « DRY » de Rails.

IV-C - Helpers spéciaux

Rails fournit un certain nombre de méthodes «helpers » spéciales qui permettent de manipuler la structure des tables ou encore les relations entre les tables. La première, que nous avons déjà vue plus haut dans ce tutoriel, est la méthode « timestamps ». Cette méthode déclare automatiquement deux colonnes, nommées « created_at » et « updated_at», qui permettent respectivement de tracer les dates de création et de mise à jour des enregistrements dans la table. La deuxième méthode que nous allons voir et qui est très importante est la méthode « references ». C'est cette méthode qui permet à Rails de structurer les tables afin d'établir le lien entre deux d'entre elles. Voici un exemple d'utilisation :

t.references :category

L'exemple ci-dessus permet de générer automatiquement le lien entre la table actuelle et la table « category » via un champ « category_id » qui va être automatiquement créé.

V - Exécuter des migrations

Rails nous fournit un certain nombre de tâches Rake qui permettent de manipuler les migrations. Celle que nous utiliserons le plus souvent est la commande « rake db:migrate ». Elle peut être agrémentée du numéro de version de migration à atteindre. Dans ce cas, les migrations suivantes ne sont pas effectuées. Par exemple :

rake db:migrate VERSION=20090420203025

Dans l'exemple ci-dessus, la version correspond au préfixe du fichier de migration. Nous pouvons également exécuter l'opération inverse, c'est-à-dire décrémenter le niveau des migrations. Pour cela, Rails met à notre disposition la méthode « rake db:rollback ». Seule, la commande permet de descendre d'un niveau de migration à chaque exécution. On peut, comme pour la commande « migrate », préciser « VERSION=… ». Dans ce cas, on descend le niveau de migration jusqu'à la version donnée en paramètre. Le mieux pour l'opération rollback, est quand même de préciser le paramètre « STEP » pour descendre plusieurs niveaux de migrations à la fois. Par exemple :

rake db:rollback STEP=3

Lorsque vous invoquez les commandes « rake db:migrate » ou « rake db:rollback », la tâche « b » est exécutée automatiquement et met à jour le fichier schema.rb qui se situe dans le répertoire db. Ce fichier reflète alors la structure de la base de données. Il est possible de recharger la structure de la base de données à partir du fichier schema.rb à l'aide de la commande « rake db:schema:load »

VI - Modéliser l'application « Mes Contacts »

Voici le diagramme de classes de notre application de gestion des contacts : nous souhaitons qu'un utilisateur puisse gérer ses propres contacts. Chaque contact pourra avoir plusieurs numéros de téléphone, messageries instantanées, sites web, adresses postales, et adresses email.

Avant de créer les migrations qui vont nous permettre de modéliser la base de données, nous devons supprimer les migrations que nous avons ajoutées précédemment :

rake db:rollback STEP=2
rm db/migrate/*

Maintenant que la base de données est vierge, nous pouvons créer nos entités. Il est néanmoins temps de faire un point sur le modèle. Certaines choses peuvent être mutualisées. C'est le cas par exemple de la gestion des utilisateurs. Plutôt que de créer de zéro une gestion des utilisateurs, nous allons appliquer le principe « DRY » et utiliser le plugin « restful_authentication ». Ce plugin va nous permettre de gérer tous les enregistrements et connexions des utilisateurs à l'application. Commençons par l'installer :

ruby script/plugin install git://github.com/technoweenie/restful-authentication

Le plugin est maintenant installé dans notre répertoire vendor/plugins. Pour créer l'entité « User » et la structure des tables utilisateurs en base de données, lançons la commande :

ruby script/generate authenticated user sessions --include-activation

Cette commande crée beaucoup de choses, notamment l'entité « User » (fichier app/models/user.rb) et le fichier de migration correspondant db/migrate/[VERSION]_create_users.rb. Sachez, pour finir, que le plugin génère des contrôleurs et vues qui permettent de gérer les utilisateurs et les sessions d'authentification. Il rajoute au fichier routes.rb des routes qui facilitent l'utilisation de ces contrôleurs (exemples : /signup, /register, /login, /logout). Nous verrons en détail ce plugin dans un autre chapitre quand nous en saurons plus sur les contrôleurs. Le but ici était d'initialiser l'entité « User ». Concentrons-nous maintenant sur la création des autres entités. À cette fin, lançons les commandes suivantes :

ruby script/generate model Phone phone_type:integer phone_number:string is_main:boolean

ruby script/generate model InstantMessaging protocol:integer address:string is_main:boolean

ruby script/generate model Website url:string is_main:boolean

ruby script/generate model Address address_type:integer address_street:string address_postcode:string
address_city:string address_region:string address_country:string is_main:boolean

ruby script/generate model Email email_type:integer email_address:string is_main:boolean

ruby script/generate model Contact first_name:string last_name:string nickname:string birthdate:date
photo:string company_name:string company_department:string company_position:string notes:text

Nous devons ensuite rajouter à la main les liens entre les différentes entités. Pour cela, nous allons utiliser la méthode «references » que nous avons étudiée dans le paragraphe 3.3. Ajoutons la ligne :

t.references :contact

dans les méthodes create_table des fichiers de migrations correspondant aux entités « Phone », « InstantMessaging », « Website », « Address » et « Email » pour faire le lien entre ces entités et l'entité « Contact ». Le lien entre les deux entités « Phone » et « Contact » se lit : un contact possède de zéro à plusieurs numéros de téléphone. Pour finir, il reste à faire le lien entre les utilisateurs et les contacts. Dans le fichier de migration de l'entité « Contact », ajoutons :

t.references :user

Une dernière chose maintenant. Si vous êtes observateur, vous vous êtes probablement aperçu qu'il manque un attribut à l'ensemble du modèle. Il y a un attribut « tag » appartenant à l'entité « Contact ». Cet attribut veut seulement dire que l'on doit pouvoir « tagger » un contact avec un à plusieurs mots. On pourra générer par exemple avec ce genre de données un « nuage de tags » des contacts. Cette fonctionnalité existe déjà dans Rails par l'intermédiaire de plugins. ActsAsTaggableOnSteroids en est un. Nous l'étudierons plus en détail dans un autre chapitre. Nous allons l'installer et l'utiliser pour le raccorder à notre modèle :

ruby script/plugin install http://svn.viney.net.nz/things/rails/plugins/acts_as_taggable_on_steroids

Ensuite nous devons créer la migration correspondante à l'aide de la commande :

ruby script/generate acts_as_taggable_migration

La dernière chose à faire est de rajouter la fonctionnalité de « tagging » aux contacts. Nous devons ouvrir le fichier app/models/contact.rb et le compléter comme suit :

class Contact < ActiveRecord::Base
acts_as_taggable
end

Voilà, tous nos modèles sont décrits, toutes les migrations correspondantes sont écrites et l'apport de certains plugins a permis de nous prémâcher beaucoup de travail. N'oublions pas de mettre à jour la base de données avant de passer à la suite :

rake db:migrate

VII - Conclusion

ActiveRecord est LE composant de Rails qu'on se doit de connaître sur le bout des doigts. Une bonne conception de la base de données passera forcément par une écriture correcte des migrations. Les migrations sont un outil puissant, car elles permettent de gagner un temps fou lors de l'initialisation d'un projet Rails. On ne doit pas les négliger. Dans la prochaine partie, nous aborderons le développement par les tests dans Ruby on Rails. Nous verrons quel est leur rôle et comment en tirer un maximum de bénéfice. Vous pouvez retrouver le source de ce tutoriel ici.

VIII - Remerciements

J'adresse tous mes remerciements à l'équipe de développement web pour leurs relectures et leurs conseils.