データベースリファクタリングに挑戦してみた。(トリガーを使わないで列名を変更する)

Web+DBのシステムを運用していると、カラム名が実態と合わないというケースが必ず出てきます。
そこでリファクタリング(カラム名を変更)したくなるのですが、そう簡単にはいきません。
なぜなら、いきなりALTER文で列名変更しようものなら、まず間違いなくアプリケーションがエラーで動かなくなるからです。
列名変更を成功させるためには、アプリケーションのコードを書き換え、新カラムの追加、データ移管、旧カラムの削除と、移行期間を設けて新旧両方のカラムを維持しながら改修作業を進めていく必要があります。

こういうデータベースリファクタリングの手法というのはWeb上にはほとんど事例がありません。
「データベースリファクタリング」という、ずばりの書籍が一冊だけ存在するので読んでみたのですが、トリガーを駆使する手法なので若干敷居が高いように感じました。
(トリガーをすらすら書ける人にとっては良いと思います)



今回、トリガーを使わないでアプリ側でがんばってやってみたらうまくできたので、事例として紹介します。
下記で紹介しているのはあくまで一例です。(特に、SELECT * に関しては賛否両論あると思います。)
他にもっとよいやり方があったら教えてください!

トリガーを使わない列名変更の流れ

例として、「user.meiというカラムをuser.firstnameというカラムに変更する」ケースをとりあげます。

  • アプリ側での事前改修
  • 新しいDB列の追加(移行期間の始まり)
  • アプリ側での切り替え
  • 古いDB列の削除(移行期間の終わり)
  • アプリ側での後片づけ

アプリ側での事前改修

事前にアプリを改修して、新旧両方対応の仕組みをいれておきましょう。
このフェーズの目標は、DB列が追加されたときにアプリが自動で追従できるようになることです。
SELECT * FROM を使う
アプリケーション内のSQLで、SELECT a,b,c FROM という風に列名をハードコーディングしていると、カラム名変更したいときに全てのSQLを直すハメになります。
SELECT * FROM と書けば、列が追加されたときに自動で対応できますので、なるべくSELECT * を使います。
INSERT, UPDATE直前で新旧列に同じ値が入るように仕込む
MVCのフレームワークのモデル層で、DB保存する直前に仕込みを入れます。
例えばPHPであれば、
$row['firstname'] =& $row['mei'];
$db->save($row);
ORマッパーなどを使ってる場合も考え方は同じです。

この時点では新列user.firstnameはまだ存在しないので、$row['firstname']はどこにも保存されません。
しかしこうしておけば、DBに列が追加された直後から、firstnameにもmeiと同じ値が入るようになります。
SELECT直後で仕込みを入れて、新旧両方の名前で同じ値を見れるようにする
例えばPHPであれば、
$row = $db->fetch($sql);
if (!isset($row['firstname'])) {
  $row['firstname'] =& $row['mei'];
}
$rows = $db->fetchAll($sql);
foreach ($rows as $row) {
  if (!isset($row['firstname'])) {
    $row['firstname'] =& $row['mei'];
  }
}
こうしておけば、まだDB上にfirstname列は存在しないにも関わらず、ビューやコントローラからは$row['firstname']という名前でその値を参照できるようになります。
ビュー、コントローラ、HTMLのコードを直す
ビュー、コントローラ、HTML、Javascriptなどで'mei'となっている箇所を全て'firstname'に書き換えます。
こうすれば、モデル層以外からは、あたかも'firstname'というアイテムが存在するかのようになります。
実際のDB上のカラム名はまだ'mei'のままなのですが、それはモデル内部に隠ぺいされているので意識しなくてよくなります。

新しいDB列の追加(移行期間の始まり)

ALTER文で列を追加します。
ALTER TABLE user ADD COLUMN firstname varchar(255);
UPDATE文で旧列を丸ごと新列にコピーします。
UPDATE user SET firstname = mei;
ここでアプリをテストします。
特に、画面からユーザの新規登録や変更保存を行った際に、新旧列のデータが同値になることを確認します。

アプリ側での切り替え

アプリケーション側のSQLを改修して、'mei'への参照を全て'firstname'への参照に切り替えます。

具体的に言うと、SELECT mei と書かれている箇所を全てSELECT firstnameに書き換えます。
WHERE句, ORDER BY句なども同様です。

このフェーズの目標は、「旧カラム'mei'があってもなくてもシステムの挙動には関係ないと」状態にするこです。

改修したらアプリをテストします。

古いDB列の削除(移行期間の終わり)

旧列をDBから削除します。
ALTER TABLE user DROP COLUMN mei;
アプリをテストします。

アプリ側での後片づけ

アプリの後方互換用のコードをすべて削除します。(特に、INSERT/UPDATE直前の同期処理)

まとめ

ざっとこんな感じです。
私が今回やったのは小規模なWebアプリ(それでもWebサーバ3台構成)だったので、実際には移行期間の始まりから終わりまで1時間ほどでした。
クラス名やメソッド名の変更に比べれば、DB定義の変更ははるかに大変だということがよくわかりました。
ただ、それでも変なカラム名を使い続けるよりは、ちゃんとリファクタリングをして気持ちよく開発運用できる方が長期的には健全であると思います。

大規模なサイトやお金が絡むショッピングサイトなどではテストを綿密にやる必要があるので、移行期間に数日~数週間かかるかもしれませんね。(案件によってはやらない方がよいかも)

以上、データベースリファクタリングの紹介でした。
カテゴリ:

人気記事