読者です 読者をやめる 読者になる 読者になる

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

RailsでPaperTrailを使って取り消し機能を作成する

Rails中級 Rails Model


Photo by Flickr: Scott Smith (SRisonS)'s Photostream


PaperTrailとは、モデルの変更(作成/更新/削除)をトラックするgemです。
モデルが変更されたら逐一データを保存するので「監査やバージョン管理」に役立ちます。

今回は、次のように、モデルの作成/更新/削除の取り消しをできるようにします。


動作確認

  • Rails 4.2.0
  • Ruby 2.2.1
  • paper_trail 4.0.0.beta2

目次

  1. PaperTrailのインストール
  2. PaperTrailの基本的な使い方
  3. PapterTrailで取消機能を実装する
  4. PaperTrailの使用時の注意点


1. PaperTrailaのインストール

Gemfileにpaper_trailを追加します。

# Gemfile
gem 'paper_trail', '~> 4.0.0.bata'


Bundlerを実行します。

bundle install


PaperTrailがモデルの変更データを記録するテーブルのversionsテーブルを作成するマイグレーションファイルを作成します。

bundle exec rails g paper_trail:install


参考までに、次のようなマイグレーションファイルが作成されます。

# db/migrate/yyyymmddhhMMss_create_versions.rb
class CreateVersions < ActiveRecord::Migration
  def change
    create_table :versions do |t|
      t.string   :item_type, :null => false
      t.integer  :item_id,   :null => false
      t.string   :event,     :null => false
      t.string   :whodunnit
      t.text     :object
      t.datetime :created_at
    end
    add_index :versions, [:item_type, :item_id]
  end
end


では、マイグレーションを実行します。

bundle exec rake db:migrate


2. PaperTrailの基本的な使い方

データの変更をトラックしたいモデルに、次のようにhas_paper_trailを追加します。

# app/models/product.rb
class Product < ActiveRecord::Base
  has_paper_trail
end

すると、様々なメソッドが使えるようになります。

versions - モデルの変更履歴を取得する。

product = Product.find(1)
product.versions #=> [<PaperTrail::Vesion>, <PaperTrail::Vesion>, ...]


何が変更されたか確認する。

product = Product.find(1)

# 直近の更新を取得する
v = product.versions.last

# イベントを取得する
v.event      #=> 'update' (か 'create' か 'destroy')

# 更新したユーザーを取得する
v.whodunnit  #=> '153' (コントローラー内で更新され、コントローラーがcurrent_userメソッドをもっていて、current_userがidを返す場合)

# 更新した時刻を取得する
v.created_at #=> Sat, 14 Feb 2015 22:32:41 JST +09:00

# 更新/削除前のproductを取得する(作成時はnilを返す)
product = v.reify

# 次のversionを取得
next_version = v.next

# 前のversionを取得
previous_version = v.previous


3. PaperTrailで取消機能を実装する

作成、変更、削除などを誤って実行してしまったときに、取り消しをできるように、PaperTrailを使って「取消機能」を実装します。

scaffoldでProductの簡単なCRUDを作成します。

bin/rails g scaffold Product name price:integer public:boolean


取消機能を追加したいモデルにhas_paper_trailを追加し、データのトラッキングをできるようにします。

# app/models/product.rb
class Product < ActiveRecord::Base
  has_paper_trail
end


取消リンクをflashメッセージに表示させるようにするので、Productコントローラーのredirect_toのflashメッセージに取消リンク(revert_linkメソッド)を追加します。
作成を取消したときに詳細画面に戻るとモデルがないためエラーになるので、createとupdateのリダイレクト先はindexにしています。また、showアクションは削除しました。

# app/controllers/products_controller.rb
class ProductsController < ApplicationController
  before_action :set_product, only: [:edit, :update, :destroy]

  # GET /products
  # GET /products.json
  def index
    @products = Product.all
  end

  # GET /products/new
  def new
    @product = Product.new
  end

  # GET /products/1/edit
  def edit
  end

  # POST /products
  def create
    @product = Product.new(product_params)

    if @product.save
      redirect_to products_url, notice: "Productを作成しました: #{revert_link}"
    else
      render :new
    end
  end

  # PATCH/PUT /products/1
  def update
    if @product.update(product_params)
      redirect_to products_url, notice: "Productを更新しました: #{revert_link}"
    else
      render :edit
    end
  end

  # DELETE /products/1
  def destroy
    @product.destroy
    redirect_to products_url, notice: "Productを削除しました: #{revert_link}"
  end

  private
    def set_product
      @product = Product.find(params[:id])
    end

    def product_params
      params.require(:product).permit(:name, :price, :public)
    end
end


次に、取消リンクを作成するメソッドを追加します。
複数の画面で使えるようにするために、Applicationコントローラーに追加します。

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  ...

  protected
    def revert_link
      view_context.link_to('取消', revert_version_path(@product.versions.last), :method => :post)
    end
end


そして、レイアウトファイルにflashメッセージを表示するように追記します。
また、デフォルトでリンクがエスケープされてしまうのでrawメソッドでリンクを表示できるようにします。
※ユーザー入力値をflashメッセージに表示すると、XSS脆弱性になるので気をつけて下さい。心配な場合は、flashのnoticeとalert以外のキーを用意して、それのみrawで表示するなど通常のメッセージと分けた方が良いかもしれません。

<!-- app/views/layouts/application.html.erb -->
...
  <body>
  <% flash.each do |name, msg| %>
    <%= content_tag :div, raw(msg), :id => name, :class => "alert" %>
  <% end %>
...
</body>
</html>


次に、取消を実施するVersionsコントローラーを作成します。

bin/rails g controller versions


Versionsコントローラーにrevertメソッド(取消)を追加します。

# app/controllers/versios_controller.rb
class VersionsController < ApplicationController
  def revert
    @version = PaperTrail::Version.find(params[:id])
    if @version.reify
      @version.reify.save!
    else
      @version.item.destroy!
    end
    redirect_to :back, notice: "#{@version.event} を取り消しました"
  end
end

更新/削除時は@version.reifyは更新/削除前(1つ前)のモデルを返すので、1つ前のモデルに更新します。
作成時は@version.reifynilを返すので、削除を行っています。


最後に、取消(revert)アクションのルーティングを追加します。

# config/routes.rb
post "versions/:id/revert" => "versions#revert", :as => "revert_version"


では、bin/rake sでサーバーを起動して動作を確認してみます。
ある商品を削除してみます。

そして、上記の「取消」リンクから取り消しを行うと、削除した商品が削除されていないことになりました。


もちろん、商品の削除の取り消しだけでなく、商品の作成、更新の取り消しを行うことができます。


4. PaperTrailの使用時の注意点

古いデータを削除する

使い続けるほどversionsテーブルのレコード数が多くなってしまうので、レコードをクーロンなどで定期的に削除するようにする。

# 一週間以上経過したversionレコードを削除する
bundle exec rails runner "PaperTrail::Version.delete_all ['created_at < ?', 1.week.ago]"


他にも監査するイベントやカラムを指定するなどいろいろなことができます。
困った場合は、PaperTrail - GitHubを参照してください。

以上です。