0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rails】default_scope と upsert_all で酷い目に遭った話

Last updated at Posted at 2025-04-30

結論

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_deletedfalseにして更新をかけたとしても、デフォルトスコープが優先されて勝手にtrueに上書きされて更新される。

(反省)

大きな実装をいきなり全体から書き始めてしまったがために、細部の挙動に関心がいかなかった。

いきなりコンソールでたくさんのレコードを作ってみて、find_in_batches書いて、回してみて多分全部いけてる!みたいなことをせずに、まずは一件のレコードを作って更新してみて、ちゃんと全てのカラムが正常に更新されているかを確実にしてからやるべきだった。

そうすれば各カラムの確認もちゃんとしようと思えてたかも。そこで「削除済みのレコードってどうなるんだっけ?」みたいな疑問も持ててたかもなぁ、と。

やはり「小さく実装する」を常に意識すべきだなと思った。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?