db:migrateをちゃんと理解する

このエントリーは [BitStar Advent Calendar 2023](https://qiita.com/advent-calendar/2023/bitstar) の7日目になります。
ここまでの投稿を見ていると自分の働いてる会社はエンターテックカンパニーではなく実はChatGPTカンパニーなのではないかと思い始めていたんですが、そんなことはないと我に返ったのでここでしっかり流れを断ち切りたいと思います。

ということで私からはRailsエンジニア向けに「`db:migrate`って結局何してるの？」という話をしたいと思います。（これ実は意外と知らない人いるのかなって個人的に思っただけなので、当然のごとくそれくらい知ってるよっていう方はスルーしてください）

## 巨大テーブルの操作は何かと気を使う
私自身普段は今年リリースした"BitStar Match"の開発が主な業務になるんですが、少し前からエンジニア採用にも携わってまして、その際に候補者の方から「御社の開発における魅力は何ですか？」といった質問をたまに受けることがあります。
そこで私はいつも「インフルエンサー業界の膨大なデータを扱えることです」と答えているんですが、これは弊社のビジネス的な強みでもあり、かつエンジニアとしても自ずとパフォーマンスに対する意識が求められたりするので、ことサーバーサイドに関しては色々と気を使う必要があってめんどくさry、じゃなくてとても"**魅力的な**"要素なのかなと思っております。
ということで試しに弊社の問題児筆頭候補のテーブルをこの際ちゃんとCOUNTしてみました。
<img width=180px src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628237/71d69ada-ad28-a315-a5fe-9e9b2ddbb148.png>
（2億件、まあ思ったよりは良い子ちゃんでした。）

そしてこういう膨大なデータを扱う上でよくある失敗の一つがRails使ってたら当たり前のように打つコマンド、`db:migrate`です。
皆さんご存知、このDB構造を変更するコマンドですが、巨大なテーブルに対して実行すると普通に数時間かかったりしてdeploy時だとタイムアウトで落ちてしまいます。なのでそれを防ぐためにはRailsが内部で何をしてるのか、そこをきちんと理解しておく必要があります。

## 結局 db:migrate は何してるの？
> マイグレーションは、データベーススキーマの継続的な変更を、統一的かつ簡単に行なうための便利な手法です。マイグレーションではRubyのDSLが使われているので、生のSQLを作成する必要がなく、スキーマおよびスキーマ変更がデータベースに依存しなくなります。
> 
> 個別のマイグレーションは、データベースの新しい「バージョン」とみなせます。スキーマは空の状態から始まり、マイグレーションによる変更が加わるたびにテーブル、カラム、エントリが追加または削除されます。Active Recordはマイグレーションの時系列に沿ってスキーマを更新する方法を知っているので、履歴のどの時点からでも最新バージョンのスキーマに更新できます。Active Recordはdb/schema.rbファイルを更新し、データベースの最新の構造と一致するようにします。

こちらRailsガイドからの引用です。
もうこれが結論ではあるんですが、これだとActive Recordの立場からすると「俺の仕事そんな簡単にまとめんなよ」って言われそうなので、もう少しちゃんとその仕事内容を具体的に紹介したいと思います。

この`db:migrate`の役割は大きく分けて以下2つに分類されるかなと思っています。
1. DB構造の変更
2. schema.rbの作成・更新


### 1. DB構造の変更
そもそもマイグレーションファイルはご存知の通り積み上げ形式でversion管理されています。
実際にその状態を可視化するコマンドが``db:migrate:status``です。
```
$rails db:migrate:status

 Status   Migration ID    Migration Name
--------------------------------------------------
...省略
   up     20231121024720  Create platform manual metrics
   up     20231121054236  Rename comment auther column to brief comments
   up     20231126060902  Add sales profit to influencer informations
```
ですが一体Railsはこれをどこから参照しているのでしょうか？
答えはschema_migrationsテーブルになります。
そんなテーブル作った覚えないって思うかもしれませんが、これは`db:migrate`を初めて実行したときにRailsが勝手に作成するテーブルです。
中身を見てみるとversionしか入っていないのがわかります。

<img width=200px src=https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628237/d4931e00-84c2-aab1-0dfa-21ba7c863bf5.png>

Railsはここに実行済みマイグレーションのversionを保存して管理しているということです。

そして`db:migrate`を実行したときはこのschema_migrationsに保存されているversionと全てのマイグレーションファイルのversionを突合し、schema_migrationsに存在しないversionのマイグレーションファイルのみを実行します。


#### よくある勘違い①
~ 直近でmigrateしたversion以降が実行対象となる ~

Railsは全てのマイグレーションの実行履歴を管理しています。
そして基本的には過去のマイグレーションファイルをdownにすることもほとんどないはずなので、一見すると常に最新の差分のみが実行されているようにも見えますが、実際はそうではなく、上述したように全てのマイグレーションファイルから未実行のマイグレーションを実行します。

### 2. schema.rbの作成・更新
マイグレーションでDB構造の変更が完了した後に、`db:migrate`はschema.rbファイルの作成・更新も行います。
というと誤解を招きそうなので、正確に言うと、`db:migrate`によって`db:schema:dump`が内部的に実行されているということです。
そして`db:schema:dump`は何をしているかと言うと、その名の通りDBから最新のテーブル構造を引っ張ってきてschema.rbを更新します。

#### よくある勘違い②
~ schema.rbは実行したマイグレーションの差分のみが反映されている ~

これは「実行したマイグレーションがschema.rbにも変更を加えている」という誤解です。正しくは「マイグレーション後の最新のDB構造がschema.rbに反映されている」になります。

## これでやっと ALTER TABLE できる
`db:migrate`について、ここまで理解しておけばあとはどうにでもなります。（おそらく）
最初に問題提起していた巨大テーブルをALTERしたいという例では、以下の流れで実行すると解決できます。
1. migrationファイルを作成
2. そのmigrationによって発行されるSQLをRailsを経由せず直接実行する
    - 発行されるSQLがわからない場合は開発環境の場合`log/development.log`の中に残るので、一度実行してみて確認するのが良いと思います。
    - テーブルロックについては別途考慮が必要かもしれませんがここでは触れません。
3. そのmigrationのversionをschema_migrationsにINSERTする
4. `db:schema:dump`でschema.rbを最新の状態に更新する

## おわりに
ということで、今回は`db:migrate`の動きを少しだけ掘り下げて紹介させて頂きました。
Active Recordは便利な一方でその内部で何をやっているかを理解しておかないと、今回のようにデータ量が膨大になったときには対応し切れなくなります。
これを機会にRailsについてより深く理解していく第一歩になれば幸いです。

BitStarでは機械学習等も用いながらインフルエンサー業界のビッグデータを活用したサービス開発に携わって頂けるRailsエンジニアを募集しております！

https://www.wantedly.com/projects/1533737