データを破壊した話

CBcloud 2022アドベントカレンダーの4日目の記事です。

経緯

ある時、以下のような位置情報を持つテーブルでprefecture_idが入力されていないためにエラーが発生していたので、 full_addressからprefecture_idを補完するという作業を実行しました。

CREATE TABLE `locations` (
  `id` bigint unsigned NOT NULL AUTOINCREMENT,
  `prefecture_id` bigint,
  `full_address` carchar(255),
  PRIMARY KEY (`id`)
)

Railsを利用していたので以下のようなスクリプトを作成し、ステージング環境で実行しました。

ActiveRecord::Base.logger = Logger.new($stdout)

Location.where(prefecture_id: nil).find_each do |location|
  prefecture = find_prefecture_from(address: location.full_address)
  Location.update(prefecture_id: prefecture.id)
end

実行中、ログファイルを tail -f して経過を見ていたんですが、「ガガガッ....ガガガッ....」 という比較的間の開いた間隔でログが更新されるのに違和感を感じつつも実行完了まで待ちました。

完了後、確認のためテーブルを見てみると、 全てのレコードのprefecture_idが同一の値になっておりそこでバグがあるまま実行していたことが発覚しました。

原因は先程のスクリプト最後から二行目、モデルのインスタンスに対してupdateを 呼び出したいところ、モデルクラスに対して呼んでおり所謂where句のないupdateクエリを発行していました。

対応

本来このスクリプトはprefecture_idを補完するものだったので、 スクリプトの修正、全レコードでprefecture_idの削除、 最後にもう一度スクリプトを実行することで修復が完了しました。

予防

今回はfull_addressから情報が復元できたので良かったですが、 今後同様の事故が起きたとき必ずしも復元できるとは限らないので予防としてリンターの設定を行いました。

module RuboCop
  module Cop
    module Rails
      class DirectUpdate < Base
        MSG = "don't call update to ApplicationRecord"

        ENTITIES =
          Dir.entries('app/models').map { _1.gsub(/\.rb$/, '').camelize.to_sym }.freeze

        def_node_matcher :update_to_model?, <<~PATTERN
          (send
            (const _ $_)
            :update ...)
        PATTERN

        def on_send(node)
          const_name = update_to_model?(node)
          return unless const_name

          add_offense(node) if ENTITIES.include?(const_name)
        end
      end
    end
  end
end