結論
default_scope -> { where(is_deleted: false) }
以上のデフォルトスコープが付いたDiary
モデルに対して以下のrakeタスクを回したら、ユーザーが削除した日記を全て復活させてしまった。
task :migrate_diaries do
Diary.unscoped.find_in_batches(batch_size: 1000) do |diaries|
updates = diaries.map do |diary|
attributes = diary.attributes.dup
attributes[:title] = diary.raw_title
attributes[:content] = diary.raw_content
attributes[:updated_at] = Time.current
end
Diary.upsert_all(updates) if updates.present?
end
end
このスクリプトの作成意図
「今まで暗号化してDBに保存していた日記のタイトルと内容を、平文にして保存し直すためのrakeタスクを作成」したかった。
データ可視化ツールで今までは暗号化された状態でしか見ることができなかった日記の内容が確認できるようになる。
問題
削除済みも含めた全てのレコードがis_deleted: true
で保存されてしまっていた。
「diary.attributes.dup
の時点でis_deleted
の内容もコピーしたはずなのに」
幸いdeleted_at
カラムも生えていたので、このカラムを参照してis_deleted
を元に戻すスクリプトを回し直して修正した。
原因
同様の状況に陥った記事で以下の説明が。
原因としてはdefault_scopeが検索だけではなく、全てのクエリを対象としているため、レコードの作成や更新のタイミングにも影響を与えていることです。
デフォルトスコープは「全て」のクエリが対象なので、Diary.upsert_all
の時もunscopted
をつけなければならなかった。
これをつけ忘れたせいで、is_deleted
をfalse
にして更新をかけたとしても、デフォルトスコープが優先されて勝手にtrue
に上書きされて更新される。
(反省)
大きな実装をいきなり全体から書き始めてしまったがために、細部の挙動に関心がいかなかった。
いきなりコンソールでたくさんのレコードを作ってみて、find_in_batches
書いて、回してみて多分全部いけてる!みたいなことをせずに、まずは一件のレコードを作って更新してみて、ちゃんと全てのカラムが正常に更新されているかを確実にしてからやるべきだった。
そうすれば各カラムの確認もちゃんとしようと思えてたかも。そこで「削除済みのレコードってどうなるんだっけ?」みたいな疑問も持ててたかもなぁ、と。
やはり「小さく実装する」を常に意識すべきだなと思った。