Voici la suite du précédent tutoriel sur Ruby on Rails publié sur ce même blog ou nous avons mis en place une application de gestion de contacts rudimentaire.

Migration de schéma de base de données

Imaginons que nous voulions gérer une liste de sociétés, et lier chacun de nos contacts à sa société... Il va nous falloir modifier notre base de données en ajoutant une table companies et une clé company_id dans la table contacts. Cela peut s'avérer compliqué puisqu'elle contient déjà des données... même si en l'occurence il ne s'agit pour l'heure que de données de test.

Rails et plus particulièrement ActiveRecord proposent un outil de gestion des évolutions du modèle de données intelligent, les migrations. Au même titre que nous avions créé le fichier db/migrate/001_contacts_development.rb pour définir le schéma initial lors du tutoriel précédent, nous allons maintenant créer son évolution incrémentale dans le fichier db/migrate/002_contacts_development.rb :

class ContactsDevelopment < ActiveRecord::Migration
  
  def self.up
    # On renomme le champs name en first_name (ne devra contenir que le prénom)
    rename_column :contacts, "name",       "first_name"
    
    # Ajout d'un champs pour stocker le nom de famille
    add_column    :contacts, "last_name",  :string
    
    # Ajout d'un champs pour stocker la société du contact
    add_column    :contacts, "company_id", :integer
    
    # Création de la table des sociétés
    create_table "companies", :force => true do |t| 
      t.column "name",        :string
      t.column "description", :text
      t.column "url",         :string
    end 
  end 
 
  def self.down
    # Rétablissement de la colonne name
    rename_column :contacts, "first_name", "name"
 
    # Destruction de la colonne last_name
    remove_column :contacts, :last_name
    
    # Destruction de la colonne contenant la référence à une société
    remove_column :contacts, :company
    
    # Destruction de la table contenant les sociétés
    drop_table :companies
  end 
 
end

Dans chaque classe de migration de schéma, une méthode up appliquera des modifications au modèles tandis qu'une méthode down permettra un rollback des modifications vers une version antérieure. Pour le détail des opérations effectuées par le script de migration ci-dessus, je crois que les commentaires parlent d'eux-mêmes ;) (j'en ai profité pour répartir le patronyme sur deux champs, nom et prénom)

La mise à jour effective du modèle dans la base de données s'effectue grâce à la commande :

$ rake db:migrate 
(in /home/niko/ww2/rails/contacts)
== ContactsDevelopment: migrating =============================================
-- rename_column(:contacts, "name", "first_name")
   -> 0.0750s
-- add_column(:contacts, "last_name", :string)
   -> 0.0153s
-- add_column(:contacts, "company_id", :integer)
   -> 0.0343s
-- create_table("companies", {:force=>true})
   -> 0.0304s
== ContactsDevelopment: migrated (0.1694s) ====================================

Nous n'avons pas d'outils d'administration pour notre nouvelle table companies, créons-les :

$ ruby script/generate scaffold Company

Il faut maintenant mettre à jour notre jeu de données ; on édite d'abord le nouveau fichier ./test/fixtures/companies.yml :

Nanonical:
  id:          1
  name:        Nanonical
  description: On vous préfère libres
  url:         http://www.nanonical.com
  
Crimosoft:
  id:          2
  name:        Crimosoft
  description: Notre monopole, c'est trop lol
  url:         http://www.crimosoft.com

Toute ressemblance avec des sociétés existantes ou ayant existé serait un sacré coup de pot

Puis, dans le fichier ./test/fixtures/contacts.yml :

John:
  id:         1
  first_name: John
  last_name:  Doe
  email:      john@doe.com
  address:    12, rue des champs
  zip:        75009
  city:       Paris
  country:    France
  company_id: 1 # John -> Nanonical
 
Bob:
  id:         2
  first_name: Bob 
  last_name:  Doe 
  email:      bob@doe.com
  address:    1, place du pré 
  zip:        35000
  city:       Rennes
  country:    France
  company_id: 2 # Bob -> Crimosoft

On réinsère nos nouvelles fixtures :

$ rake db:fixtures:load

On peut se rendre successivement aux adresses http://0.0.0.0:3000/contacts et http://0.0.0.0:3000/companies afin de s'assurer que tout va bien.

companies_scaffolding.png

Mise à jour des classes modèles

Pour que Rails prenne en compte les modifications structurelles de notre schéma, nous allons éditer les fichiers des modèles Contact et Company, notamment pour spécifier leur type de relation grace aux méthodes de classe ActiveRecord.

Le fichier ./apps/models/contact.rb :

class Contact < ActiveRecord::Base
  # Chaque employé appartient à une compagnie
  belongs_to :company
end

Et le fichier ./apps/models/company.rb :

class Company < ActiveRecord::Base
  # Une société possède plusieurs employés
  has_many :contact
end

Mise à jour du contrôleur

On récupère la liste des sociétés depuis le contrôleur gérant les contacts (situé dans le fichier ./app/controllers/contacts_controller.rb), d'abord pour la création :

class ContactsController < ApplicationController
  [...]
  def new
    @contact = Contact.new
    @companies = Company.find_all
  end

Puis pour l'édition:

class ContactsController < ApplicationController
  [...]
  def edit
    @contact = Contact.find(params[:id])
    @companies = Company.find_all
  end

Mise à jour de la vue

Enfin, modifions notre formulaire d'édition/création de contacts, situé dans app/views/contacts/_form.rhtml, comme suit :

<%= error_messages_for 'contact' %>
 
<!--[form:contact]-->
<p><label for="contact_company_id">Company</label>
<%= select 'contact', 'company_id', @companies.collect {|c| [c.name, c.id]} %></p>
 
<p><label for="contact_name">First name</label><br/>
<%= text_field 'contact', 'first_name'  %></p>
 
<p><label for="contact_name">Last name</label><br/>
<%= text_field 'contact', 'last_name'  %></p>
 
<p><label for="contact_email">Email</label><br/>
<%= text_field 'contact', 'email'  %></p>
 
<p><label for="contact_address">Address</label><br/>
<%= text_field 'contact', 'address'  %></p>
 
<p><label for="contact_city">City</label><br/>
<%= text_field 'contact', 'city'  %></p>
 
<p><label for="contact_zip">Zip</label><br/>
<%= text_field 'contact', 'zip'  %></p>
 
<p><label for="contact_country">Country</label><br/>
<%= text_field 'contact', 'country'  %></p>
<!--[eoform:contact]-->

Ce qui donne à peu-près ceci :

Nouveau formulaire de création/édition de contacts

Et le template d'affichage de notre liste de contacts, dans le fichier ./app/views/contacts/list.rhtml pour y ajouter la colonne affichant le nom de la société pour chaque contact :

<h1>Listing contacts</h1>
 
<table>
  <tr>
  <% for column in Contact.content_columns %>
    <th><%= column.human_name %></th>
  <% end %>
    <th>Société</th>
  </tr>
  
<% for contact in @contacts %>
  <tr>
  <% for column in Contact.content_columns %>
    <td><%=h contact.send(column.name) %></td>
  <% end %>
    <td><%= h contact.company.name %></td>
    <td><%= link_to 'Show', :action => 'show', :id => contact %></td>
    <td><%= link_to 'Edit', :action => 'edit', :id => contact %></td>
    <td><%= link_to 'Destroy', { :action => 'destroy', :id => contact }, :confirm => 'Are you sure?', :method => :post %></td>
  </tr>
<% end %>
</table>
 
<%= link_to 'Previous page', { :page => @contact_pages.current.previous } if @contact_pages.current.previous %>
<%= link_to 'Next page', { :page => @contact_pages.current.next } if @contact_pages.current.next %> 
 
<br />
 
<%= link_to 'New contact', :action => 'new' %>

Ce qui donne au final quelque chose comme ceci :

Nouvelle liste de contacts

To be continued

Dans un prochain tutoriel, nous aborderons la validation de formulaires.