Webアプリケーションは変化してゆくもの。時間と共に適切な設計は変わってゆきます。
RDB のスキーマも変化を免れることはできません。
当然、Webアプリケーションを稼働しながらのスキーマ変更になりますから、
- サービスを止めない
- データ欠損を起こさない
そんなスキーマ変更が求められます。
スキーマ変更時に発生し得る問題
例えばカラム名を変更したいとしましょう。
A というカラム名を B という名前に変更します。
alter table tablename rename column A to B;
ただリネームするだけであれば、こんな SQL で事足ります。けれど、その実行タイミングについて考えてみましょう。
カラム名を変更するためには、Webアプリケーションを古いカラム A を参照しているコードから、新しいカラム B を参照するコードに更新する必要があります。
通常、そのデプロイ(およびそれに伴うアプリケーションの再起動)タイミングと RDB のスキーマ変更のタイミングを完全に同期することは難しいでしょう。
また、大抵の場合アプリケーションは冗長化されて複数のサーバで動いています。各サーバへのデプロイタイミングを全く同時にはできません。
ですので、古いカラム A を参照するアプリケーションと、新しいカラム B を参照するアプリケーションが同時に存在することになります。
これがつまりどういうことかというと、以下のような状況を防ぐのは難しいということになります。
- DBスキーマが古い状態で新しいコードが動く
- DBスキーマが新しい状態で古いコードが動く
ということで、新旧のスキーマ・コードが並行しても問題なく動作するアプローチを考えることになります。
問題へのアプローチ
まず一つ、とても単純な解決方法があります。
メンテナンス時間を確保し、アプリケーションをすべて停止した状態でスキーマとコードを更新する、というやり方です。
ソーシャルゲームのアプリケーションではよく見かけるやり方ですし、大掛かりなスキーマ変更の場合はこの方法をとった方が良いこともあるでしょう。
けれど、アプリケーションを可能な限り停止したくない場合もあるかと思います。特に些細なスキーマ変更のたびにいちいちメンテナンス時間を取らなければならないというのは大変です。
そんな場合の手順を以下にまとめます。
上でも例にあげた、カラム A を B にリネームする方法を考えてみましょう。
- 新しいカラムとして B を追加する(RDB のスキーマ変更)
- 古いカラム A と新しいカラム B の両方に書き込みを行うようにコードを変更する(アプリケーションの更新)
- カラム B のうち null なレコードについて、 A から値をコピーする
- 書き込みはまだ A と B の両方に行うようにしたまま、 B を参照するようにコードを変更する
- A への参照がなくなったことを確認して、 A への書き込みをやめるようにコードを変更する
- A を削除する
テーブルを分割したい、など詳細が違っても考え方は同じです。
もう少し汎用化した言い方にすると、以下のような手順になるかと思います。
- 新しいスキーマと古いスキーマが並行して動く環境を整える
- 新しいスキーマと古いスキーマのどちらを参照しても同じ値が返る状況にする
- その状態で、新しいスキーマのみを参照するようにアプリケーションを書き換える
- 古いスキーマを削除する
RDB のスキーマ変更の際は、常にこれを意識した上で行っています。
EC Booster では先日、請求関連のテーブル構造の見直しを行い、スキーマ変更を伴うリファクタリングを行いました。
いずれ機会があれば、そのときの実例をお話しできればと思います。