Railsで削除してしまったデータを復元する


# はじめに
最近３つあった案件が２つ急になくなってしまってピンチのフリーランスエンジニアの[藤井](https://twitter.com/YoshitsuguFujii)です。
 えっ。不況？
最近は釣りばっかりしています。
  
ということで時間があったのでQiita初投稿です。 

# 道具箱を持とう
さていきなりですがみなさん道具箱お持ちでしょうか？ 
道具箱というのは自分が開発、運用、調査などをする際に便利に使える自分専用のツール類(主にcliツール)のことです。 

問題解決のためには、もちろんネットで探して既成のソフトを使ってもいいんですが、せっかく自分でコードを書けるのだから自分で作って自分の道具箱に追加しましょう。
特に新人の方はスキルも上達するのでおすすめです。
どこかから持ってきたコードを改造して使ったりしてみてもいいです。
日々使い続ける上で自分好みにカスタマイズしながら洗練させていきましょう。

# 心構え
 自分しか見ないし使わないのでコードは汚くていいんです。可読性なんて考えなくていいです。まさかりも飛んできません。 なのでPRで辛辣なことかかれませんし、どっちでもよくない？という微妙なラインの指摘もされません。
いらくなったらゴミ箱にドラッグすればいいんです。
なので本能のまま書きなぐりましょう。

# 自分の道具箱

 パッと見て公開できるものを含めて自分のお道具箱の中身を見てみました。 
* S3からインタラクティブにbucketを選んで正規表現で複数ファイルをダウンロードできるcliツール(圧縮ファイルの解凍もできる)
* URI 文字列をデコードした文字列を返すcliツール
* ファイルからascii文字を削除するcliツール
* DB定義書（エクセルファイルか)らridgepole用のスキーマファイル作成できるgem

もちろんボツネタもいっぱいありますが。

# 表題の件
なにかユーザーが作業して誤って消してしまった。元に戻したいという要望はあると思います。
もしくは間違えて本番で作業してしまった系のミスですね。
これに対応すべくコードを書きました。

削除復元ツールです。

※ 該当操作の直近のデータから抽出するので完全ではないです。

### 前提条件

1. Railsのプロジェクトであること
2. 戻せる消す前のデータがあること(AWSでは自動スナップショットのデータで復元することが多いです)
3. RailsのログにSQL(DELETE文)がすべて集約されていること(例えば外部キー制約で関連データを消しているとログに吐かれないので使えないです)

### 準備

#### 1. ログからDELETE文を抽出
まず該当のログを取得します。ログインするシステムであれば最初にユーザーの存在確認のselectが走ると思うのでそれを探したりすると見つけやすいです。
該当のアクションが見つかったらログのリクエストIDを使ってgrepすると該当操作のログが全部取得できるので簡単です。 
リクエストIDはこんな感じのログの。

```
[2022-07-29T07:20:39.414381 #21189]  INFO -- : [42fd108b-0efa-4936-b014-25c35109575d] Started GET "/" for 127.0.0.1 at 2022-07-29 07:20:39 +0000
```

**42fd108b-0efa-4936-b014-25c35109575d**の部分です。

そこからさらにDELETE SQLだけを抜き出します。
それをdelete.sqlというファイルに保存します。

#### 2. 該当の操作以前の日付のデータベースを用意する

例えばAWSのスナップショットからデータベースを復元して戻しましょう。

#### 3. 接続先を復元先のデータベースに設定する

railsの接続先を2の復元したデータベースにします

#### 4.  復元する

rails consoleを立ち上げて以下をコピペするなりファイルに保存してloadするなりして実行します。

```ruby
table_id_hash = {}

row_index = 0
sqls = []
File.open("delete.sql", mode = "rt"){|f|
  f.each_line{|line|
    p "行: #{row_index += 1}"
    p line
    p "--------------------"
    line = line.gsub(/\e\[\d{1,3}[mK]/, '')
    line = line.chomp
    table_name = line[/FROM `([A-Za-z_]*)`/, 1]
    table_id_hash[table_name] ||= []

    result = ActiveRecord::Base.connection.select_all(line.gsub('DELETE', 'SELECT *') )
    rows = result.to_a

    rows.each do |row|
      if row.nil?
        sqls << "\n"
        next
      end

      if table_id_hash[table_name].include?(row['id'])
        next
      end

      values = row.values.map do |val|
        if val.to_s.include?('UTC')
          "'#{val.to_s(:db)}'"
        elsif val.nil?
          "null"
        else
          "'#{val}'"
        end
      end

      table_id_hash[table_name] << row['id']
      sqls << "INSERT INTO `#{table_name}` (#{row.keys.join(',')}) VALUES (#{values.join(',')});\n"
    end
  }

  file.puts('begin;')
  sqls.reverse.each do |sql|
    file.puts(sql)
  end
  file.puts('commit;')
}

file.close
```
実行するとinsert文が羅列されたinsert.sqlというファイルが作成されるかと思います。
これを使って削除してしまったデータベースに適用してデータを復元しましょう。

コードはやっていることは簡単で、delete文からselect文を生成して実行し、その結果からinsert文を作成しています。
deleteの発行順序で流すと外部キー制約にひっかかることがあるのでreverseで消したのと逆の順番でinsert文を生成するようにしています。


# 最後に
1000行のdelete文から一瞬で1000行のinsert文が作成できます。
仕組みは簡単なんですが、意外と面白いんじゃないでしょうか？

絶賛お仕事募集中なので受託、業務委託の案件があったらお仕事ください