Blog tech

Le versionning d'objet avec Paper Trail

Rédigé par Alexandre Salaun | 6 septembre 2011

Qu’est ce que Paper Trail ?

Paper Trail est une gem permettant de tracker les modifications effectuées sur les données pour les différents models de votre application RubyOnRails. Elle s’avère très utile lorsque vous souhaitez faire du versionning, par exemple pour avoir la possibilité d’annuler des changements…

Cette gem est compatible avec Rails 2.3 et Rails 3. Vous pouvez la retrouver dans le dépôt Github sur deux branches, l’une pour la version Rails 3 et l’autre pour Rails 2.3.

Installation

Rails 2.3

Dans un premier temps vous devez installer la gem en spécifiant la branche rails2 afin d’obtenir a version compatible avec Rails 2.3. Vous avez pour cela deux possibilités :

  • soit en ajoutant la gem dans le fichier config/environment.rb de votre application
config.gem 'paper_trail', :version => '~> 1'
  • soit avec la commande gem install
gem install paper_trail --version "~> 1"

Dans un second temps, vous devez lancer le script d’installation de Paper Trail avec la commande suivante :

script/generate paper_trail

Ce script génère une migration qui va créer une table “versions” dans votre base de données, il vous faut donc lancer la commande rake db:migrate afin de finaliser l’installation.

Rails 3

Comme avec Rails 2.3 vous devez installer la gem. Il vous suffit d’ajouter la gem Paper Trail dans votre gemfile (en spécifiant bien la version pour avoir celle compatible avec rails 3) :

gem 'paper_trail', '~> 2'

Puis, vous devez lancer le script d’installation de Paper Trail :

rails generate paper_trail:install

Enfin, il est également nécessaire de lancer les migrations avec la commande rake db:migrate pour créer la table “versions” dans votre base de données.

L’installation est donc maintenant terminée, vous pouvez donc utiliser Paper Trail dans vos différents models. Pour cela, l’unique chose à faire est d’ajouter la ligne suivante dans le model concerné :

has_paper_trail

Utilisation de Paper Trail

Maintenant que vous savez comment installer cette gem vous allez pouvoir découvrir les différents usages. Les exemples donnés ci-dessous sont réalisés avec la version Rails 3 de Paper Trail mais il n’y a normalement aucune différence notable entre les deux versions (vous pouvez le constater sur le blog de l’auteur de la gem, dans le paragraphe “Rails 2.3 versus Rails 3”)

Tracking

La principale fonctionnalité de Paper Trail est de permettre un tracking des modifications effectuées sur vos objets. Vous pouvez donc visualiser l’historique des modifications d’un objet enregistré en base.

Afin d’illustrer cet usage, nous allons, dans une application Rails 3 et après avoir installé Paper Trail, créer un model User. Il faut ensuite indiquer que ce model utilise la gem :

class User < ActiveRecord::Base
  has_paper_trail
end

Maintenant, si vous créer un objet User depuis une console vous pourrez constater qu’une ligne a été insérée dans la table versions de votre base de données avec pour item_type le nom de votre model (ici User), commme évènement “create” et comme item_id l’id de votre objet User dans la tables users.

id | item_type | item_id |  event | whodunnit | object |      created_at     |
---|-----------|---------|--------|-----------|--------|---------------------|
1  | User      | 1       | create | NULL      | NULL   | 2011-09-03 10:37:17 |

Vous pouvez également observer que l’on peut stocker, pour chaque version, l’id de l’utilisateur (attention c’est une chaîne qui est stockée en base et pas un entier) qui a effectué la modification si celle-ci est faite depuis un controller et qu’il y a à ce moment là un current_user. Ceci peut être utile si l’on souhaite supprimer toutes les modifications effectuées par un utilisateur.

Ensuite, si vous modifiez votre objet une deuxième ligne concernant ce dernier sera insérée dans la table versions avec comme évènement “update”. Une nouvelle version de votre objet est donc stockée en base.

id | item_type | item_id |  event | whodunnit | object |      created_at     |
---|-----------|---------|--------|-----------|--------|---------------------|
1  | User      | 1       | create | NULL      | NULL   | 2011-09-03 10:37:17 |
---|-----------|---------|--------|-----------|--------|---------------------|
2  | User      | 1       | update | NULL      | ---    | 2011-09-03 10:39:23 |

Attention, une version n’est créé que si l’objet a été modifié, dans le cas où un “save” ou un “update” est effectué mais qu’aucune modification n’a lieu sur l’objet alors il n’y a pas de nouvelle version de l’objet.

Enfin, la destruction de l’objet insère également une ligne de la table versions :

user.destroy

# =>
#
# id | item_type | item_id |  event  | whodunnit | object |      created_at     |
# ---|-----------|---------|---------|-----------|--------|---------------------|
# 1  | User      | 1       | create  | NULL      | NULL   | 2011-09-03 10:37:17 |
# ---|-----------|---------|---------|-----------|--------|---------------------|
# 2  | User      | 1       | update  | NULL      | ---    | 2011-09-03 10:39:23 |
# ---|-----------|---------|---------|-----------|--------|---------------------|
# 3  | User      | 1       | destroy | NULL      | ---    | 2011-09-03 10:42:45 |

Fonctions de bases

Différentes méthodes s’avèrent très utiles lorsque vous souhaitez naviguer dans l’historique des modifications d’un objet. La première d’entre elle, versions permet de voir toutes les versions d’un objet dans un tableau :

user.versions
# => [#<Version id: 1, item_type: "User", item_id: 1, event: "create", whodunnit: nil, object: nil, created_at: "2011-09-03 10:21:24">, #<Version id: 2, item_type: "User", item_id: 1, event: "update", whodunnit: nil, object: "---\ncreated_at: 2011-09-03 10:21:24.173698000Z\nemai...", created_at: "2011-09-03 10:37:17">]

Les méthodes previous_version et next_version appliquées à l’objet en question permettent de naviguer dans l’historique des versions. Elles retournent respectivement la versions précédente ou la version suivante si elle existe ou nil dans le cas contraire.

user.previous_version
 # => #<User id: 1, firstname: "Alexandre", lastname: "Salaün", email: "test@test.com", created_at: "2011-09-03 10:21:24">

Vous pouvez également naviguer dans l’historique des modifications grâce aux méthodes previous et next en les appliquant à des versions de votre objet :

user.versions.last.previous
# retourne la version précédente de la dernière version

user.versions.first.next
# retourne la seconde version de l'objet

Il est également possible de remettre un objet tel qu’il était dans une version précédente après l’avoir modifié autant de fois que vous le souhaitez grâce à la commande reify :

user = user.versions.find(2).reify
user.save
# l'objet user est revenu tel qu'il était lors de sa première modification

Grâce à la commande live? vous pouvez savoir si un objet est dans sa version courante ou si il provient d’une ancienne version :

user.live? # l'objet user a été modifié mais aucun retour à une version précédente n'a été fait
# => true

user = user.versions.find(2).reify
user.live?
# => false

Configurer le tracking

Il est possible d’activer ou de désactiver Paper Trail pour des cas donnés, par exemple uniquement pour un environnement donné en le spécifiant de le fichier de configuration de ce dernier :

# in config/environments/development.rb
config.after_initialize do
  PaperTrail.enabled = true
end

# in config/environments/test.rb
config.after_initialize do
  PaperTrail.enabled = false
end

# in config/environments/production.rb
config.after_initialize do
  PaperTrail.enabled = true
end

Vous pouvez aussi le spécifier par model :

# pour activer paper_trail
User.paper_trail_on

# pour le désactiver
User.paper_trail_off

Ou bien le spécifier pour un appel donné :

# on supprime notre user sans utiliser le versionning
user.without_versioning :destroy

D’autre part, le tracking peut être activé uniquement si certain champs sont modifiés :

class User < ActiveRecord::Base
  has_paper_trail :ignore => [:first_name, :age]
end

ou bien uniquement sur certaines actions :

class User < ActiveRecord::Base
  has_paper_trail :on => [:update, :destroy] # on ne gère pas la création
end

Undo

L’une des utilisations les plus courantes de Paper Trail est la création d’une méthode undo afin de permettre à un utilisateur d’annuler la modification qu’il vient de faire.

En effet, si sur une page listant différents utilisateurs vous avez un lien permettant de supprimer chacun d’entre eux comme ceci :

Lorsque l’utilisateur clique sur le lien, dans votre controller, vous retourner un message flash afin d’avertir l’utilisateur de la suppression de l’utilisateur souhaité. Il vous suffit de rajouter un lien dans ce message faisant appel à une fonction qui annule la dernière modification pour un objet donné.

Dans le controller de versions (qui correspond donc aux versions des objets stockées en base par Paper Trail), vous créez une méthode revert_changes (ou tout autre nom qui vous convient) qui rétablit l’avant dernière version de l’objet :

# encoding: UTF-8
class VersionsController < ApplicationController
  def revert_changes
    @version = Version.find(params[:id])
    @version.reify.save!

    redirect_to :back, :notice => "Modification annulée"
  end
end

Vous ajoutez une route vers cette action :

Project::Application.routes.draw do
  resources :users
  post "versions/:id/revert_changes" => "versions#revert_changes", :as => "revert_changes"
end

Enfin, dans votre controller de users, vous ajoutez un lien dans le message de retour vers cette action qui aura pour effet d’annuler la suppression :

# encoding: UTF-8
class UsersController < ApplicationController
  ...

  def destroy
    @user = User.find(params[:id])
    @user.destroy

    undo = view_context.link_to "Annuler la suppression", revert_changes_path(@user.versions.last), :method => :post
    redirect_to users_path, :notice => "L'utilisateur a été supprimé. " + undo
  end

  ...
end

Si vous supprimez un utilisateur, vous avez donc maintenant dans votre message un lien qui vous permet d’annuler la suppression :

Les actions ne sont donc plus irrémédiables et l’utilisateur peut revenir en arrière si il se rend compte d’une erreur.

Si vous souhaitez avoir plus de détail sur la façon de mettre en place cette méthode, un railscast de Ryan Bates aborde ce sujet plus en détail.

Conclusion

La gem Paper Trail a donc différents usages, que ce soit du simple tracking à l’utilisation de liens de type undo. Sa mise en place s’avère être des plus simples et la documentation disponible sur le github du projet est complète.

Cette gem est donc simple d’utilisation et parfaite pour les cas d’utilisations présentés ci-dessus. Il existe d’autres outils permettant de versionner les objets mais celle-ci reste une valeur sûre.

L’équipe Synbioz.

Libres d’être ensemble.