この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。
マイグレーション作成時にやりがちな失敗について
最近、自室用にHHKB pro2をまた買ったよ。オフィスで使ってるTypeSとは違いノーマルの方だけど。TypeSに比べると少し音が煩い代わりに打鍵感はほぼ同じでとても良い。新卒のみんなも初任給で10個くらい買おう。
はい、というわけで今日は マイグレーション作成時にやりがちな失敗 についての話。
RDBMSの予約語をカラム名にしてしまう
気づきにくい、発覚しにくい、忘れやすい。三拍子揃ったうっかりポイント。
DBにはシステム上で識別子として使われるいくつかの単語がある。これらは予約語とされており原則としてカラム名やテーブル名として使うのを避ける必要があるが、案外うっかりカラム名として使ってしまうことがある。
そんな単語は使わないって? ところが予約語はDBの種類によって差がある上にその数も多い。例えばMySQLなら予約語は 231個 ある。23個じゃないよ。(mysql 5.6)
しかも結構使いたくなってしまうような語が多い。 interval、option、key、usage、release、condition ……全部ダメだ。(意外なことにcountは予約語ではない)
それならマイグレーション生成する時に警告でも出してくれればいいが残念ながらそんなものは出ない。ご不満なら自分でissueでもプルリクでも立てよう、今日から君もOSSコミッターだ。
で、結局これをやってしまうと何が起こるんだ? 答えは何も起こらない。 少なくとも普通にRailsアプリケーションで使っていく上ではまず問題にならない。が、例えばMySQLコンソールでこんなSQLを書けばエラーが返る。
mysql> SELECT key FROM nekos;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'key from nekos' at line 1
なんで? Railsでは動くよ?
その理由はActiveRecordが発行してくれるSQLは全部テーブル名まで指定した上で引用府で囲まれるため。こうしてやることで予約語であってもSQLは無事に通る。
SELECT `nekos`.`key` FROM `nekos`;
Railsで無事に使えるならいいじゃん? 良くないよ! ActiveRecordだけがそのデータベースを使うとは限らないし単純にDB設計として推奨されない。一度作成して使い始めてしまったカラム名を後から変更するのは非常に困難だ。作成時に避けよう。
デフォルト値や制約を設定しない
さて、nekosテーブルのbrushing_flagというブーリアン型(true/false)のカラムについて考えてみよう。
SELECT COUNT(*) FROM nekos;
=> 3
SELECT COUNT(*) FROM nekos WHERE brushing_flag = true;
=> 1
SELECT COUNT(*) FROM nekos WHERE brushing_flag != true;
=> ?
?の部分はどうなるかな。正解は、 わからない。
いや、ふざけている訳ではなくて「わからない」というのが正解だ。
何故ならSQLのWHEREではbrushing_flagにnullが入っている場合、trueともfalseともしない。それどころか brushing_flag != trueという条件にも引っかからないからだ。論理的に考えるとなるほど正しい。
だからbrushing_flagがnilであるレコードの数に応じて結果は0から2まで不定だ。nullの値を条件の対象に出来るのは IS NULLの構文だけであり、ガス室に入った猫ちゃんよろしく 「trueであるともfalseであるとも言えない」 のだ。
このため、既に稼働しているアプリでブーリアン型のカラムを追加してrailsのコードを書いていると面食らうことがある。rubyの感覚ではnilであればfalseだからだ。しかし、Neko.not.where(brushing_flag: true)と書いても期待している結果にはならない。直感的じゃないなぁ…。
期待する挙動を得るためにはNeko.where(brushing_flag: [false, nil])と書く必要がある。しかし、これはあんまりイケてない。 そもそもnilをfalse扱いにしたいならカラムの初期値をfalseにすべきだからだ。
初期値が設定できない理由やnullを許可する明確な理由がなければ可能な限り入れて行きたい。特にブーリアン型は必須と言えるかも知れない。新しいカラムを追加したりテーブル構成を弄る時には既存データとの整合性を慎重に考慮する必要があるが、それも初期値設定を活用したい。
これをモデルのvalidateではなくマイグレーションで行う意味は、前述したようにDBをRailsアプリだけが使うとは限らないからだ。仮に別のRailsアプリが使ったとしてもモデルが一致するとは限らない。DBレベルで避けるのが安全だ。
さもなければ思いがけないところで条件漏れが起きたり突然 undifined method error for nil:NilClass と怒られてしまう。
既存マイグレーションを書き換える
やるべきではない。既に保守フェイズに入っているアプリでこれをやるのは考えただけでもクラっとする。
ただ例外として初期の開発フェイズにおいては話が変わる。データベースの仕様変更が頻繁でマイグレーションで愚直に管理して行くことが却って混乱を招くため、単一の初期マイグレーションファイルとしてまとめて、変更が生じる度に書き換えてDBをリセットしていくやり方もある。
整合性を保持しなければいけないデータもまだないのでこれで良い。万事、状況に応じてベストプラクティスは変わる。
その他
他にもいくつか思うことがあるが、それらについては賛否両論あるため省略する。例えばserialize/storeの是非やインデックスの付与タイミングなど。
railsのマイグレーションに限らずSQLのDB設計アンチパターン(不適切なやり方)については 『SQLアンチパターン』 などの書籍で詳しく扱っているので未読なら読むと良いです。この本に書かれている内容に比べるとこの記事の話は局所的で瑣末なこと。
最後に強調しておきたいのが 「アンチパターンの回避や正規化は未然・早めに行うこと」 。既に数十万件以上のものレコードが積み上がって複雑に関係しあった状況からシステムを止めずに正規化したり改修するコストは半端じゃない。わずかな手間を嫌がると驚きの複利で返済不可能な負債が積み上がる。