ぴよログ

↓に移転したのでこっちは更新されません、多分。

Railsである時点でのモデルのスナップショットのためにpaper_trailでのバージョニングを行う

移転しました →

RailsでA、Bというモデルがあるとする。モデルAはモデルBを参照しているが、参照しているモデルBはある瞬間のスナップショットにしたいとする。つまり、モデルAが欲しいのは関連を作ったときのモデルBの情報で、それ以降に変更が加わって新しくなったものではないということだ。

例として不適切かもしれないが、モデルAをお気に入りモデル、モデルBをツイートとしてみる。あるユーザーがあるツイートをお気に入りに登録すると、お気に入りモデルが新しく作られる。

擬似コードはこんな感じ。

p @tweet.body # => "今日は雨でした"
@fav = @user.favorites.create(tweet_id: @tweed.id)

@favが作られた時点での@tweet.body今日は雨でしたというものだった。このあと@tweetの投稿者が何らかの理由で元の文章を今日は晴れでしたに変更したとする。@favから参照できる@fav.tweet.bodyは普通にやっていれば最新の情報である今日は晴れでしたとなっているはずだけど、そうではなく@favを作った時点での情報、今日は雨でしたが欲しい。

これが今回の前提。長い。

このような機能を実現するためにモデルのバージョン管理を行う。ActiveRecordをバージョン管理するためのgemがいくつかあるが、Ruby Toolboxを見る限りではpaper_trailがよさそうだ。

The Ruby Toolbox - Active Record Versioning

airblade/paper_trail

paper_trailの導入

  1. gem 'paper_trail', '~> 3.0.3'
  2. bundle
  3. rails generate paper_trail:install
  4. rake db:migrate

3と4でバージョン管理用のテーブルができる。

そしてバージョン管理したいモデルでhas_paper_trailを呼んであげればOK。

class Favorite < ActiveRecord::Base
  has_paper_trail
  # ...
end

paper_trailについて簡単に説明すると、モデルの作成時や変更時に古い情報をyaml化してpaper_trail用のテーブルに入れるという仕組みになっている。

使ってみる

詳しくはGitHubのREADMEを読んでもらうとして、最初に書いたケースを満たすような使い方を擬似的なコードで紹介する。

# user2がtweetする
@tweet = user2.tweets.create(body:"今日は雨でした")

# user1がさっきのtweetをお気に入りにする
@fav = user1.favorites.create(tweet_id:@tweet.id)

# user2がさっきのtweetの文章を変更する
@tweet.body = "今日は晴れでした"
@tweet.save

class Favorite < ActiveRecord::Base
  has_paper_trail
  belongs_to :user
  belongs_to :tweet
  def saved_tweet
    tweet.version_at(created_at)
  end
end

p @fav.saved_tweet.body  # => "今日は雨でした"

ここでキーとなるのはversion_atというpaper_trailメソッドで、Time系のオブジェクトを渡すとその時点のモデルを返してくれる。変更がなければ最新の物が返ってくる。これで当初の目的は果たせるようになった。

別の方法としてTweetモデルをそのままコピーしたimmutableなモデルを用意するという解決策があるかもしれないが、その方法だとFavoriteの数だけimmutableモデルが作られてしまう。データの量や管理の楽さから考えてもバージョン管理のほうが望ましいと思う。