github : https://github.com/airblade/paper_trail
Railsアプリケーションで監査、またはバージョン管理のためにモデルの変更を追跡するツールです。モデルの生成から破棄まで監視していて、任意のバージョンへ戻したり、削除したものを復元したりできるようになります。
基本的にはgithubのREADMEを見ればだいたいわかるようになっていますが、少しつまづいたところもあったのでまとめました。
PaperTrailのバージョン
2015/11/25 バージョン4が最新安定版、ruby1.8.7以上、activerecord3.0以上6未満
バージョン5はunreleasedとなっています。
作成されるテーブルとその中身
PaperTrailは以下のようなversionsテーブルを作りログを残します。
item_typeがモデル名、 whodunnitはeventを起こしたユーザ(文字列可)です。
id | item_type | item_id | event | whodunnit | object | created_at |
---|---|---|---|---|---|---|
1 | Account | 1 | create | 2015-11-25 08:33:50.844782 | ||
2 | Account | 2 | create | 1 | 2015-11-25 08:35:31.590367 | |
3 | Sample | 1 | create | 1 | 2015-11-25 08:35:31.370328 | |
4 | Sample2 | 1 | create | 1 | 2015-11-25 08:35:31.633935 | |
5 | Sample2 | 2 | create | 1 | 2015-11-25 08:35:31.643612 | |
6 | Account | 2 | update | 1 | --- : id: 2 : email: sample@sample.com : encrypted_password: "***************" : name: sample : admin: false : created_at: 2015-11-25 08:35:31.590367000 Z : updated_at: 2015-11-25 08:35:31.590367000 Z : |
2015-11-25 08:37:34.267056 |
7 | Sample | 1 | update | 1 | --- : name: sample01 : created_at: 2015-11-25 08:35:31.370328000 Z : updated_at: 2015-11-25 08:35:31.370328000 Z : |
2015-11-25 08:37:34.226331 |
インストール
1.Gemfile追加
gem 'paper_trail', '~> 4.0.0'
2. Bundlerを実行
bundle install
3.データベースにバージョンテーブルを追加する
bundle exec rails generate paper_trail:install
bundle exec rake db:migrate
※ --with-associationsをつけるとモデルが変更されたときにそのモデルと関連するモデルのバージョンも格納するようになります。このときユニークな値であるtransaction_idを使うようになり、同じトランザクションで生成されたバージョンレコードを管理します。これがつかないと使えないものがいくつかあるようです。
bundle exec rails generate paper_trail:install --with-associations
使い方
以下のようにモデルにhas_paper_trail
を追加することでcreate
, update
, destroy
を追跡するようになる。
class Widget < ActiveRecord::Base
has_paper_trail
end
基本的な使い方の例
v = widget.versions.last # 直前の変更を取得する
v.event # 'update'か'create'か'destroy'
v.created_at # eventがいつ起こったか
v.whodunnit # controller経由で変更された場合、controllerがcurrent_user methodを持っていればidをstringで返す
widget = v.reify # widgetを直前の状態に戻す。(eventはnil)
APIまとめ
モデルオブジェクトから使えるmethod
has_paper_trailを追加すると使えるようになるmethod一覧
以下のようにhas_paper_trailを追加した場合で見ていきます。
class Widget < ActiveRecord::Base
has_paper_trail
end
- versions
- 登録されているバージョンすべて取得する
- PaperTrailオブジェクトの配列を返す
widget.versions
- version
- 現在のバージョンを取得する。
- PaperTrailオブジェクトを返す
- バージョン管理されていない(最新のものなど)はnilを返す
widget.version
- live?
- 現在参照しているものが使用しているものならtrue、違うならfalseを返す
widget.live? # trueの場合
widget.previous_version.live? # ひとつ前の状態を見るとfalseが返る
widget.destroy
widget.live? # 削除してもfalseになる
- paper_trail_originator
- 前の状態から現在の状態への変更を行ったユーザを返す
widget.paper_trail_originator
- version_at
- 与えられたタイムスタンプの時の状態を返す
- 下の例の場合2015年11月17日23時59分59秒時点のWidgetオブジェクトが返る
widget.version_at(Time.local(2015, 11, 17, 23, 59, 59))
- previous_version
- ひとつ前の状態を取得する。
- 前がない場合nilを返す
- 下の例の場合Widgetオブジェクトが返る
widget.previous_version
- next_version
- 次の状態を取得する。
- 次がない場合nilを返す
- 下の例の場合Widgetオブジェクトが返る
widget.next_version
- touch_with_version
- 通常のtouchコマンドではversionの更新が行われないのでtouch_with_versionを使用する
- touchと同様に下の例ならwidget.created_atが更新され、さらに新しいPaperTrailオブジェクトが生成される
widget.touch_with_version
# ------touchとtouch_with_versionの違い------
widget.updated_at # => 2015-11-18 07:54:45 UTC
widget.version.created_at # => 2015-11-18 07:54:45 UTC
sleep(5)
#-------------touch------------------------
widget.touch
widget.versions.size # => 3
widget.updated_at # =>2015-11-18 07:54:51 UTC
widget.version.created_at # => 2015-11-18 07:54:45 UTC
#-------------touch_with_version-----------
widget.touch_with_version
widget.versions.size # => 4
widget.updated_at # =>2015-11-18 07:54:51 UTC
widget.versions.last.created_at # =>2015-11-18 07:54:51 UTC
- paper_trail_off!
- PaperTrailによるログの収集が行われなくなる
- 収集済みのログは見れる
Widget.paper_trail_off!
- paper_trail_on!
- PaperTrailによるログの収集を始める
Widget.paper_trail_on!
- paper_trail_enabled_for_model?
- classまたはinstanceに対しPaperTrailによるログの収集が行われていればtrueを返す
Widget.paper_trail_enabled_for_model?
widget.paper_trail_enabled_for_model?
PaperTrail::Versionインスタンスが持つmethod
- version.reify
- このバージョンで管理されているモデルオブジェクトを返す
- dupオプション
- 次のバージョンで管理されているモデルオブジェクトを返す
- has_oneオプション
- --with-associationsが必要
- has_manyオプション
- --with-associationsが必要
- mark_for_destructionオプション
- --with-associationsが必要
version.reify
- paper_trail_originator
- 前のバージョンから現在のバージョンへの変更を行ったユーザを返す
version.paper_trail_originator
- terminator、whodunnit、version_author
- 現在のバージョンから次のバージョンへ変更を行ったユーザを返す
- どれも挙動は同じ?
version.terminator
version.whodunnit
version.version_author
- next
- 次のバージョンを返す
version.next
- previous
- 前のバージョンを返す
version.previous
- index
- バージョンの全体の中でのindex番号を返す
version.index
- event
- バージョンが作られた時の操作(create、updated、destroy)を返す
version.event
- where_object
- 属性でバージョンの問い合わせを行う
- バージョンオブジェクトの配列が返る
# content属性が"Hello"でtitle属性が"Article"なバージョンを探す
Trail::Version.where_object(content: "Hello", title: "Article")
- where_object_changes
- 属性でバージョンの問い合わせを行う
- --with-associationsが必要
- 指定した属性が変更されたバージョンオブジェクトの配列が返る
# content属性が"Hello"に変更されたバージョンを探す
PaperTrail::Version.where_object_changes(content: "Hello")
versionsとversionの名前変更
versionsとversionのメソッドがすでに存在する場合、モデル側で以下のようにすることでversionsとversionの名前を変更できます。
class Widget < ActiveRecord::Base
has_paper_trail :versions => :paper_trail_versions,
:version => :paper_trail_version
end
ユーザをログに残す
ApplicationControllerに以下のように追記することでユーザをログに残せます。
class ApplicationController
before_filter :set_paper_trail_whodunnit
end
ただしPaperTrailではset_paper_trail_whodunniは
def set_paper_trail_whodunnit
@set_paper_trail_whodunnit_called=true
::PaperTrail.whodunnit = user_for_paper_trail if ::PaperTrail.enabled_for_controller?
end
でuser_for_paper_trailは
def user_for_paper_trail
return unless defined?(current_user)
ActiveSupport::VERSION::MAJOR >= 4 ? current_user.try!(:id) : current_user.try(:id)
rescue NoMethodError
current_user
end
のように定義されているのでcurrent_userでユーザ名をとれない場合は
ApplicationControllerで以下のようにuser_for_paper_trailをオーバーライドします。
class ApplicationController
def user_for_paper_trail
logged_in? ? current_member.id : 'Public user' # or whatever
end
end
ただ、私がPaperTrailを使用したのはGrapeで作成したAPIに
上のことを行おうとしたらうまくいきませんでした。
これはGrapeのAPIがApplicationControllerを継承していないためで、
以下のように各APIに追記することでユーザ名(今回はID)を取得するようにできました。
GrapeのAPIはbaseを継承しているのでそちらに書けば一括で適用されます。
module API
module V1
class ******API < API::V1::Base
before do
PaperTrail.whodunnit = current_account.id unless request.get?
end
~
~
~
grapeではAPIを実行する前にbeforeメソッドを実行します。ここでGETの場合、DBの書き換えは行っていないと思われるのでunless request.get?
をつけていますがなくても動くと思います。
データベースのテーブル名を変える
モデルごとに別テーブルを作成する場合
PaperTrailではデータベースにversionsという名前のテーブルを追加します。
しかし、モデルごとに登録されるログのテーブルを変更したい時などは別名のテーブルを追加することで実現できます。
app/models/内の各モデルファイルを以下のように変更する。
class PostVersion < PaperTrail::Version
self.table_name = :post_versions # 追加されるテーブル名
self.sequence_name = :post_versions_id_seq # postgressでなければいらない
end
class Post < ActiveRecord::Base
has_paper_trail :class_name => 'PostVersion' # PostVersionで定義したテーブルを使用するようになる
.
.
.
end
この後、DBにテーブルを追加したりしなければなりませんが
versionテーブルを消す作業をやらないだけで基本はこの後に書かれた作業と同じなのでそちらを参考にしてください。
行うこととしてはversionのmigrationファイルをコピーして作成したいテーブル名に合わせるだけです。
versionテーブルを使用しないで別の名前のテーブルを作成する場合
上記の方法は各モデルごとに別名のテーブルを追加することを考えて書きましたが、
今度はもともと存在するversionsテーブは作成しない(削除する)で別名のテーブルですべてのモデルのログを管理する方法を書きます。
まずはapp/models/paper_trail/version.rbを追加し、以下を加える。
module PaperTrail
class Version < ActiveRecord::Base
include PaperTrail::VersionConcern
self.abstract_class = true
end
end
今回は「audits」という名前でテーブルを作ることを考える
app/models/audit.rbを作成して以下のように記述
class Audit < PaperTrail::Version
self.table_name = :audits # 追加されるテーブル名
self.sequence_name = :audits_id_seq # postgressでなければいらない
end
各モデルで以下のようにhas_paper_trailの部分に追記します。
class Post < ActiveRecord::Base
has_paper_trail :class_name => 'Audit'
・
・
・
versionsのmigrationファイルをコピーしてversionsをauditsに書き換えます。
# db/migrate/20151125030055_create_audits.rb
class CreateAudits < ActiveRecord::Migration
TEXT_BYTES = 1_073_741_823
def change
create_table :audits 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, :limit => TEXT_BYTES
t.datetime :created_at
end
add_index :audits, [:item_type, :item_id]
end
end
元のversionsのmigrationファイルを削除します。
そしてDBの作り直します。
私の環境ではこんな感じで行いました
rm db/migrate/20151120030055_create_versions.rb
bundle exec rake db:drop
bundle exec rake db:create
bundle exec rake db:migrate
これでversionsテーブルは作成されず、auditsテーブルが作成され、
versionsテーブルで管理していたログと同じ内容がauditsテーブルに登録されるようになりました。