Dragonfly attachmentを持つレコードをコピーする場合には注意が必要
Dragonfly については以前紹介した通りで、普段触っているアプリではこれを使っている。表示のタイミングで変換が走るのは高負荷のサイトには向かないかもしれないけど、使い勝手には満足している。
で、今回はこの Dragonfly を使っている場合に気をつける必要のあるユースケースを見つけたのでまとめておこうと思う。
ActiveRecord + Dragonflyでの画像の削除のタイミング
Paperclip もそうかもしれないけど、Dragonfly はよくできていて
- レコードの削除
- レコード内の Attachment の変更
のタイミングで Attach していたファイルをちゃんと掃除してくれる。これによって無駄なファイルが残ってしまうといったことがなくなる。
そしてこの処理は ActiveRecord で言うと
before_save
のタイミングで実行される。
削除時にはファイルの参照はチェックしないので自前で
今回ハマったのは
- コピーしたレコードで
- 画像を変更したら
- 他のレコードも参照していたファイルが消えてしまった!
というものです。
上の説明を読んでいればとても当たり前の話なんだけど、まぁそこはそれ。回避するには以下のいずれかの方法がありそう。
- 参照カウンタよろしく、同一の uid を持つレコードが before_save の段階で 2以上あったら削除を無効化
- コピーの段階で実体のファイルもコピー
今回使ったのは 1 の方法。
削除を無効化する具体的な方法
※ 以下のコードは dragonfly 0.9.12 のものであり、また回避用のコードは実際に動いているものとよく似ているけれど動作検証はしていません。
dragonfly の画像の削除は知らない間に行われるので、callback で処理しているに違いない。ということで、before_* で検索してみる。
すると
lib/dragonfly/active_model_extensions/class_methods.rb
に
before_save :save_dragonfly_attachments
before_destroy :destroy_dragonfly_attachments
こんな記述が見つかる。探すと今度は
lib/dragonfly/active_model_extensions/instance_methods.rb
に
def save_dragonfly_attachments
dragonfly_attachments.each do |attribute, attachment|
attachment.save!
end
end
def destroy_dragonfly_attachments
dragonfly_attachments.each do |attribute, attachment|
attachment.destroy!
end
end
という記述が見つかる。これはそれぞれ
lib/dragonfly/active_model_extensions/attachment.rb
の中にある
def destroy!
destroy_previous!
destroy_content(uid) if uid
end
def save!
sync_with_model
store_job! if job && !uid
destroy_previous!
self.changed = false
self.retained = false
end
のようだ。destroy! は期待通りに動いているので、save! でも同様に呼び出されている
def destroy_previous!
if previous_uid
destroy_content(previous_uid)
self.previous_uid = nil
end
end
があやしい。そこでこんなことをしてみた。
class Photo < ActiveRecord::Base
image_accessor :image
after_validation do
if self.image_uid_was and
self.class.where(:image_uid => self.image_uid_was).count > 1
self.image.instance_eval {
def destroy_previous!
;
end
}
end
end
end
- 例として Photo という Model を用意し
- その中の image という attribute に画像を保存
- ファイルを変更しようとしており(photo_uid_was が nil でなかったら)、同じ uid を参照しているものが他に存在していたら
- destroy_previous! を無効化
している。
今まさに保存しようとしているインスタンスだけ destroy_previous! の中身を空にしてしまう黒魔術。
すでに入っている before_save の callback の前にこれを追加することができるならそれでいいと思う。やり方が分からなかったので、before_save よりも前に呼ばれる after_validation に突っ込んだ。
after_validation から呼ぶなら before_save そのものを skip するという手も使えそうだけど、他に callback をセットしてるとそれもややこしいので、いちばん影響なさそうで手っ取り早い黒魔術で片付けることにした。
これでコピーしたレコードの画像を差し替えても他のレコードでロストしない。