社内DBマイグレーションルール

dotfiles 弄りが趣味の @ToruIwashita です。

この記事は、社内向けに作った DB マイグレーションを行う際のルールを紹介するものです。
Web サービスにおいて DB マイグレーションを本番環境で実施しようとした時に、見落としがちなポイントの確認など、何かの役に立てば幸いです。

※ この記事には Django、GCP を想定したサンプルコードが出てきます。適宜読み替えて下さい。

# ルール作りの目的と前提知識

## ルール作りの目的

DB マイグレーションはアプリの根幹であるデータベースの構造を変更するため、失敗時にはアプリに多大な影響を与えてしまう。
しかし現状、社内には DB マイグレーションに関する How to やルールがなく、どのように実装・マイグレーション実行を行うかは担当するエンジニア個人の判断になっているため、作業の質に斑が出る。

これを改善するため、「失敗しても大丈夫なマイグレーションにする」を大原則に、ルールを作る。

## マイグレーションの定義

[マイグレーションとは、ソフトウェア・アプリケーションやハードウェア、システム、データ、開発言語などを別の環境に移行したり、新しい環境に切り替えたりする事を意味する](https://boxil.jp/mag/a3005/#3005-1)。
今回ルール作りの対象となるのは、DB に関連するマイグレーションであるが、そのマイグレーションも主に2つに分類できる。

概念                   | 説明
:---                   | :---
データマイグレーション | [異なる種類の装置やソフトウェア、システム、データ形式などの間でデータを移す事](http://e-words.jp/w/%E3%83%87%E3%83%BC%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3.html)。
DB マイグレーション    | [DB に保存されているデータを保持したまま、テーブルの作成やカラムの変更などを行う事](https://densan-labs.net/tech/codefirst/migration.html)。

上記2分類の内、今回は DB マイグレーションのルール作る。
※ 現実ではデータマイグレーションと DB マイグレーションが混在した作業を行う事があるので、その点には注意が必要。

## DB マイグレーションの種類

DB マイグレーションの対象となるのは DDL の実行。
以下は DDL で行われるテーブル操作の代表的なもの。

- テーブル作成(CREATE TABLE)
- テーブル削除(DROP TABLE)
- テーブル変更(ALTER TABLE)
  - カラム追加
  - カラム削除
  - 制約の追加
  - 制約の削除
  - デフォルト値の変更
  - カラムのデータ型の変更
  - カラム名の変更
  - テーブル名の変更

# DB マイグレーションのルール

## ルールの方針

DB マイグレーションのルールは、DB マイグレーションの種類毎に実装時、または DB マイグレーション実行時の注意点やチェック項目を含んだドキュメントを用意し、それらを必ず確認する事とする。
アプリ開発時にテーブル定義変更を行う場合は、以下に書かれたチェック項目や注意点を確認し、対応を行う事。

※ 以降特別な明記がない場合「マイグレーション」という言葉は「DB マイグレーション」を指す。

### 全作業共通のルール

マイグレーション実行時には必ず DB のバックアップを取る。
GCP を利用している場合、DB マイグレーション実行前に以下のようなコマンドでバックアップを取る。

e.g.)

```
> gcloud sql instances list --format 'table(name)' | tail -n +2 | xargs -I@ gcloud sql backups create --instance @
```

※ CLI でなく GCP の WEB コンソールからの作成でも問題ない。

#### チェック項目

項目                                             | 概要
:---                                             | :---
本番環境のデータでマイグレーションが実行できるか | 開発環境で成功したマイグレーションであっても開発環境では作られづらいデータがある場合に、外部キー制約等の影響により本番環境の DB では失敗する場合がある。<br/>また、DDL の実行時にトランザクション処理でテーブルにロックが掛る可能性があるため、ユーザーがアプリを利用している状態においてマイグレーションが失敗する場合がある。そういった観点からもマイグレーションが実行可能(もしくは、失敗しても問題ない)か確認する。
マイグレーションのロールバックは実行可能か       | マイグレーションの実装方法によってはロールバックが（データが消失する場合や、コードのバグでコマンドが通らない場合など）実行できない場合があるため、実行可能であるか確認する。
実行されるマイグレーションを把握しているか       | 何らかの不具合でロールバックを行う事になった時、事前に`./manage.py showmigrations`等で実行されるマイグレーションを把握していれば作業が容易になるので確認する。

チェック項目の「マイグレーションのロールバックは実行可能か」については、Django アプリであれば以下のようなコマンドを開発環境で一度実行する事を推奨する。

```
# 実行したいマイグレーションは 0032
> ./manage.py showmigrations polls
polls
 ...
 [X] 0030_auto_20181202_0342
 [X] 0031_auto_20181219_1320
 [ ] 0032_remove_user_username

# マイグレーションの実行
> ./manage.py migrate polls
Operations to perform:
  Apply all migrations: polls
Running migrations:
  Applying polls.0032_remove_user_username... OK

# マイグレーションのロールバック実行
> ./manage.py migrate polls 0031
Operations to perform:
  Target specific migration: 0031_auto_20181219_1320, from polls
Running migrations:
  Rendering model states... DONE
  Unapplying polls.0032_remove_user_username... OK
```

#### 注意点

マイグレーション実装者がマイグレーションの実行を担当した方が良いが、そうでない場合は実行されるマイグレーションを確認して、事前に手順を明確にする事を推奨する。(ロールバック手順含む)

### テーブル作成

#### チェック項目

項目                                             | 概要
:---                                             | :---
アプリリリース前にマイグレーションの実行が必要か | 新たに作成するテーブルを参照するアプリが先にリリースされて問題が起こりそうな場合、事前マイグレーションが必要か確認する。
初期データの投入が必要か                         | 初期データを投入する必要がある場合、投入方法や投入タイミングを確認する。
適切なインデックスが作成されるか                 | 特に短期間で大量のデータインサートが見込まれるテーブルや、アプリの実装上大量のデータを持つテーブルとの JOIN を含んだクエリが発行される場合は DB のパフォーマンスに影響が出るため要確認。

#### 注意点

新たに作成するテーブルを参照するアプリが先にリリースされたとしても影響度合いが小さい場合、アプリリリース直後にマイグレーションを実行する、という判断をしても問題ない。
インデックスについてはカーディナリティを考慮して作成すべきで、むやみに増やす必要はない。

### テーブル削除

#### チェック項目

項目                                                   | 概要
:---                                                   | :---
削除対象テーブルはアプリから参照されていないか         | 削除対象テーブルにマッピングされたクラスは固より、アプリの中で生の SQL が使われている可能性もあるので要確認。
アプリ以外のサービスで利用されていないか               | アプリとは別で集計や、何らかの処理に使われていないかを確認する。
マイグレーションが途中で失敗した場合ロールバック可能か | 1回のリリースでトランザクション処理を跨いだ複数の DDL が実行される場合に、1つ目成功、2つ目失敗という事態が起きた時、データが失われてロールバックが難しくなる事があるので、ロールバックを考慮したマイグレーションを作る、または DB のリストアを含めたロールバック手順を確認する。
アプリに不具合が見つかった場合に、ロールバック可能か   | マイグレーションに成功した後にアプリのリリースを行い、その後にアプリに不具合が見つかった場合、アプリのロールバックには「削除したテーブルとデータ」を戻す必要が出てくるため、手順を確認する。

#### 注意点

アプリの仕様上不要と考えられても、削除してしまうとユーザーからの問い合わせ等があった時に調査不能になってしまう可能性がある。そういった観点からもテーブル削除が適切か確認する。
削除する場合は、中身のデータのバックアップを何処かに取るべきか、という事も検討する。

### カラム追加(テーブル変更)

#### チェック項目

項目                                             | 概要
:---                                             | :---
アプリリリース前にマイグレーションの実行が必要か | 新たに作成するカラムを参照するアプリが先にリリースされて問題が起こりそうな場合、事前マイグレーションが必要か確認する。
カラム追加の実行時間に問題がないか               | データ量が多いテーブルに対してのカラム追加の場合、時間が掛る場合があるので本番環境と同等のデータ量を持つ DB で実行時間を確認し、必要であればリリースの時間をずらす、またはメンテナンスリリースを行う。
適切な制約が作成されるか                         | NOT NULL 制約や外部キー制約、ユニーク制約がアプリの仕様に対して適切か確認する。
適切なインデックスが作成されるか                 | 特に短期間で大量のデータインサートが見込まれるテーブルや、アプリの実装上大量のデータを持つテーブルとの JOIN を含んだクエリが発行される場合は要確認。

#### 注意点

新たに追加するカラムを参照するアプリが先にリリースされたとしても影響度合いが小さい場合、アプリリリース直後にマイグレーション実行の判断をしても問題ない。
インデックスについてはカーディナリティを考慮して作成すべきで、むやみに増やす必要はない。

### カラム削除(テーブル変更)

#### チェック項目

項目                                                   | 概要
:---                                                   | :---
カラム削除の実行時間に問題がないか                     | データ量が多いテーブルに対してのカラム削除の場合、時間が掛る場合があるので本番環境と同等のデータ量を持つ DB で実行時間を確認し、必要であればリリースの時間をずらす、まははメンテナンスリリースを行う。
削除対象カラムはアプリから参照されていないか           | 削除対象カラムにマッピングされたクラスは固より、アプリの中で生の SQL が使われている可能性もあるので要確認。
アプリ以外のサービスで利用されていないか               | アプリとは別で、削除対象テーブルが集計や何らかの処理に使われていないかを確認する。
マイグレーションが途中で失敗した場合ロールバック可能か | 1回のリリースでトランザクション処理を跨いだ複数の DDL が発行される場合に、1つ目成功、2つ目失敗という事態が起きた時、データが失われてロールバックが難しくなる事があるので、ロールバックを考慮したマイグレーションを作る、または DB のリストアを含めたロールバック手順を確認する。
アプリに不具合が見つかった場合に、ロールバック可能か   | マイグレーションに成功した後にアプリのリリースを行い、その後にアプリに不具合が見つかった場合、アプリのロールバックには「削除したテーブルとデータ」を戻す必要が出てくるため、手順を確認する。

#### 注意点

アプリの仕様上不要と考えられても、削除してしまうとユーザーからの問い合わせ等があった時に調査不能になってしまう可能性がある。そういった観点からもカラム削除が適切か確認する。
削除する場合は、中身のデータのバックアップを何処かに取る等の対応が必要かも検討する。

### 制約の追加・削除 / デフォルト値の変更 / データ型の変更(テーブル変更)

#### チェック項目

項目                                             | 概要
:---                                             | :---
マイグレーションの実行時間に問題がないか         | データ量が多いテーブルに対しての制約追加の場合、時間が掛る場合があるので本番環境と同等のデータ量を持つ DB で実行時間を確認し、必要であればリリースの時間をずらす、またはメンテナンスリリースを行う。
本番環境のデータでマイグレーションが実行できるか | 制約の追加・削除が成功するか否かはデータに依存するため、本番のデータを確認する。

#### 注意点

チェック項目の「本番環境のデータでマイグレーションが実行できるか」については例えば以下のような事が考えられるので、本番データチェック時には要確認。
※ アプリのバリデーションでデータをチェックしていたとしても、既存テーブルの制約が緩ければ予期せぬデータが入る事があるので、必ず確認する。

- NOT NULL制約: NULL を含むカラムの場合、マイグレーションに失敗する。
- ユニーク制約: 重複した値が存在するカラムの場合、マイグレーションに失敗する。
- 外部キー制約: キーを参照されるテーブルに存在しないキーを含んだカラムの場合、マイグレーションに失敗する。

### カラム名の変更

#### チェック項目

項目                             | 概要
:---                             | :---
リリースタイミングや方法は適切か | アプリのリリースとマイグレーションの実行には必ずタイムラグが発生するので、影響範囲を加味してリリースの時間をずらす、またはメンテナンスリリースを検討する。

### テーブル名の変更

#### チェック項目

項目                             | 概要
:---                             | :---
リリースタイミングや方法は適切か | アプリのリリースとマイグレーションの実行には必ずタイムラグが発生するので、影響範囲を加味してリリースの時間をずらす、またはメンテナンスリリースを検討する。

# マイグレーション実行に関する Tips

Tips はマイグレーション実行に関して参考となる情報や、ルール化はしないが気をつけるべきポイントをまとめたもの。

### モデル名の変更

Django ではモデル名を変更した時に`./manage.py makemigrations`によってマイグレーションファイルを作成しようとすると、以下のようなプロンプトが表示される。
このプロンプトに`N`を渡した場合、作成されるマイグレーションファイルはテーブルの削除と作成を含むものになるため注意が必要。

```
> ./manage.py makemigrations polls
...
Did you rename the polls.User model to NewUser? [y/N]
...
```

もしくは、変更内容によっては Django がモデル名の変更を検知できず、プロンプト表示無しでテーブルの削除と作成のマイグレーションが作成される可能性はあるので、`./manage.py makemigrations`で作成されるマイグレーションの中身の確認する事を推奨。

### テーブルの削除

例えばモデル削除の作業時に、テーブル削除のマイグレーションを単純に作ってしまうと、ロールバックが必要になった場合に DB をリストアする以外の選択肢がなくなってしまう場合がある。

GCP であればリストアが簡単ではあるが、一旦テーブル削除でなくリネームのマイグレーションを作成し、戻しのマイグレーションではテーブル名を元に戻すような実装にすると、Django の世界でロールバックを完結できる。
※ リネームしたテーブルを削除したい場合は、後日改めてテーブル削除のマイグレーションを実施すれば良い。

### マイグレーション実行の方針

冒頭でも述べた通り、マイグレーションはアプリの根幹であるデータベースの構造を変更するため、失敗時にはアプリに多大な影響を与えてしまう。
また、Django はマイグレーションファイル毎にトランザクション処理を行う仕様となっている。

こういった事から、モデルの変更をいくつも重ねた後にその変更をまとめてリリースする場合は、複数のマイグレーションファイルで構成されるマイグレーションを全て実行しなければアプリとの整合性が取れなくなる。
※ 複数のマイグレーションファイルで構成されるマイグレーションはトランザクション処理となっていないので、不整合な状態で止まる可能性が高くなってしまう。

こういった問題を回避するためには、マイグレーション実行に関して「複数のマイグレーションファイルを含むリリースは極力行わない」という方針を取る事が良いと考えられる。
※ 複数のマイグレーションを含むリリースであっても内容やマイグレーションの作り方によっては問題無い場合がある。