Nic Lin's Blog

喜歡在地上滾的工程師

Carrierwave 與 PaperTrail 的天生不合

前言

這兩支算是滿普遍熱門的 rails gem

  • 做 file upload 會用 Carrierwave
  • 紀錄 model operation log 會直接用 PaperTrail

但這兩支 Gem 有天生不合的地方,如果安裝時沒有稍微調整,等數據跑下去,發現時就會知道這是個巨坑

設定情況

  1. 一個常見情況 User table 有 image 欄位,資料格式是 string, 用來傳使用者頭像的數據
  2. 設定了 has_paper_trail, 只有在 update 時觸發的 callback 會記錄 object 前後變化
class User < ApplicationRecord
...
mount_uploader :image, HeadImageUploader
has_paper_trail on: [:update]
...
end

在另一張表 Order 裡面,我希望能夠拿到該訂單下訂時的 user 的 is_enterprise 狀態

class Order < ApplicationRecord
...
  def owner_is_enterprise_when_ordering?
    owner.versions.version_at(self.created_at).is_enterprise?
  end
...
end

因為 PaperTrail 再執行 version_at 時,會直接去下 WHERE 找出當時時間 range 的 Log 並把裡面的 object deserialized 出來成一個 instance,也就是執行了 reify

所以到這裡一切都很美好,只要 call owner_is_enterprise_when_ordering? 我可以知道當時下訂單時,該使用者的企業認證狀態囉, right?

踩雷的開始

但今天我就拿到一個 error

ActionView::Template::Error: undefined class/module HeadImageUploader::Uploader70109597847360

這時我就不明白怎麼會炸在 HeadImageUploader ?

直到我嘗試了更新 User 的 image 後,去執行 owner_is_enterprise_when_ordering? 發現確實炸了

/home/deploy/.rbenv/versions/2.4.4/lib/ruby/2.4.0/psych.rb:253 in load
/gems/paper_trail-8.0.1/lib/paper_trail/serializers/yaml.rb:17 in load
/gems/paper_trail-8.0.1/lib/paper_trail/version_concern.rb:184 in object_deserialized
/gems/paper_trail-8.0.1/lib/paper_trail/reifier.rb:17 in reify
/gems/paper_trail-8.0.1/lib/paper_trail/version_concern.rb:217 in reify
/gems/paper_trail-8.0.1/lib/paper_trail/record_trail.rb:412 in version_at
app/models/order.rb:391 in owner_is_enterprise_when_ordering?

嗯,Log 顯示一路炸上去

繼續追查下去,發現一個重點,通常 PaperTrail 預設的儲存格式是 YAML,但像是 Carrierwave 就是 JSON,也就是我們會遇到所謂「天生不合」的問題。

我們發現在每一次的 callback, paper trail versions 會將 image 欄位試圖存入整個 object, 然後在執行 reify 時在做 object deserialized 就炸了

接著開始找解決方案,大致有兩種作法

  1. skip 掉 image 的 paper trail
  2. 對 paper trail 做 monkey-patch

不過不管你挑哪一種作法,基本之前存的資料如果拉出來會爆就是廢了,畢竟他可是直接存 YAML 呢(竟然是預設值),然後你就會發現最好玩的是…

在 PaperTrail 的 issue 中,有人建議 A JSON serializer should be included by default

然後 Carrierwave 裡面的 wiki 也直接教你 How to: use with PaperTrail

結論

最後我是放棄 patching 資料了,就直接 rescue 找不到資料的提示了

提醒如果有這兩個 Gem 混用的時機,切記要注意

最好一開始就把 PaperTrail 預設改成 JSON 或是把 file uploader 的欄位 skip 掉

但在討論串中就有人認為既然都 Paper trail 了,理當是每個欄位都需要被紀錄,所以把欄位 skip 掉也不是很好的作法。

如果無藥可救,就放棄整個 PaperTrail 的 reify 功能吧,反正你也對爛掉的資料做不了 deserialize…

comments powered by Disqus