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
paper_trailの導入
gem 'paper_trail', '~> 3.0.3'
bundle
rails generate paper_trail:install
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モデルが作られてしまう。データの量や管理の楽さから考えてもバージョン管理のほうが望ましいと思う。