Railsプロジェクトの初期開発フェーズでのDBスキーマ管理を見直す


2014年 10月 23日

DBのスキーマ、皆様どのように管理されているでしょうか。

Railsを利用されている方の多くは、ActiveRecordのマイグレーションを利用して管理をされているかと思います。

私もいままでいくつかのRailsプロジェクトに関わってきましたが、
ほぼ全てのプロジェクトでActiveRecordのDBマイグレーションを利用してきました。
(一部のプロジェクトはActiveRecordを使っていないため、マイグレーションも独自のものを利用しています)

ActiveRecordのマイグレーションでは、DBスキーマ変更の差分情報をマイグレーションスクリプトとして保存しておきます。例えば、新しいテーブル「users」を作成する場合は、下記のようなマイグレーションスクリプトを作成します。

class AddUsers < ActiveRecord::Migration
  def up
    # ここにマイグレーション適用時の操作を書く
    create_table :users do |t|
      t.string :name
      t.datetime :created_at
      t.datetime :updated_at
    end
  end

  def down
    # ここにマイグレーション破棄した時の操作を書く
    drop_table :users
  end
end

テーブル作成/削除やカラム変更を行う際には、マイグレーションスクリプトを1つ作成します。
このように変更を管理することによって、DBスキーマを最新版へ移行させたり、任意の時点のスキーマに戻したりすることが可能です。また、スキーマの一貫性を保つことができます。

便利な機能なのですが、プロジェクトが始まったばかりの初期開発フェーズで、このマイグレーション運用をするのはかなり辛いです。

プロジェクト初期の段階では、DBの設計もちゃんと固まっていないため、スキーマ変更が頻繁に発生します。
スキーマ変更のたびにマイグレーションスクリプトを書かないといけないのは手間がかかるもので、DB修正を気軽に行うことができなくなります。

何か楽できる手立てはないものでしょうか。

初期開発フェーズでのDB Schema管理

運用保守フェーズに入ったシステムで、ActiveRecord標準のマイグレーションを利用することには、今のところ大きな不満はありません。
しかし、システム一次リリース前の開発フェーズで、DBマイグレーションを利用することは苦痛でしかありません。

私の所属している会社はSIerということもあり、新規にシステムを作成する、ということが間々あります。

開発初期の段階ではDBのスキーマ定義が頻繁に変わります。
テーブル定義をはじめからきっちり設計している、と思っていても、変わります。
このカラムにはINDEXを張っておかないと、とか、負荷対策の為にここにも同じデータ入れておこう、とか
テーブル分割しよう、とか。

下図は、とあるプロジェクトのマイグレーションファイルの一部です。
開発初期の段階では、あっという間にスクリプトファイルが増加していきます。

eb5e6916-bd24-d1d7-e4a9-94a2acf05a53.png

日付のついた、よく分からないマイグレーションファイルが大量にリポジトリにコミットされてるのは、なんだか違和感があります。
また、DBスキーマの今の状態がどうなっているのか、マイグレーションファイルから推測するのは難しいです。

なにより辛いのは、あるテーブルに1カラム足すだけでも、マイグレーションファイルを作成しないといけないことです。
このため、気軽にテーブル定義が変更できません。

マイグレーションをちゃんと使うのをあきらめた

DB設計が固まっていない段階でのマイグレーション運用は辛いです。
なので、ちゃんと使うことを諦めました。

前回とは別のプロジェクトのマイグレーションファイル一覧です。
ファイル数がかなり少ないです。

687aadd5-3923-436b-ee1d-f9eb7c0abe1d.png

initial_schema.rb というマイグレーションファイルを作り、そこをひたすら編集していきます。

運用段階に入ったシステムでリリース済みのマイグレーションファイルを編集する、というのは絶対にやってはいけないことです。
ですが、システムリリース前だと、このような適当な運用でもなんとかなります。
これでdb/migrate以下にマイグレーションファイルが大量にできてしまう問題を回避できますね。

DBスキーマの定義に変化があったとき、開発環境のDBをどうやって最新の状態にするか。
開発者は皆、以下のコマンドを叩いてDBを作り直します!

bundle exec db:drop db:create db:migrate db:seed

システム1次リリースまではずっとこの調子です。
上記コマンドを毎回入力するのはちょっと辛いので、以下のようなrake taskも用意しています。

namespace :db do
  task :overhaul do
    Rake::Task["db:migrate:reset"].invoke
    Rake::Task["db:seed"].invoke
  end
end

これで

bundle exec rake db:overhaul

と入力したらDB更新完了です。便利!
、、、じゃないです。辛いです。データなくなっちゃいます。

開発をするにあたり、開発者は各々自分用の秘伝のテストデータを作っていたりします。
また、本番に近いテスト用環境を構築し、そこで画面を叩いてテストデータを作るケースもあります。
それなのに、DBスキーマの更新が発生するたびにテスト用データが消えてしまうんですよ。これは辛いです。

「すみません、今まで作ったデータですが、DB更新するので消えて無くなります」ってなんかおかしくないですか?
こんな感じの開発スタイルを最近まで続けておりました。

マイグレーション運用を見直す時が来た

2014年8月の終わり頃、いつものようにはてなブックマーク巡回をしていると、以下の記事が流れてきました。

上記ブログの中で、Ridgepoleと呼ばれるスキーマ管理ツールが紹介されています。
このツールでは、DBスキーマはSchemafileと呼ばれるファイルに以下のようなDSLで記述しておきます。(DSLはActiveRecord SchemaのDSLと同じです)

create_table "publishers", force: true do |t|
  t.string   "name",       limit: 100, null: false
  t.text     "url"
end

カラムを修正する場合、Schemafileの定義を修正するだけです。

例えば、created_atカラムを足す場合を考えてみます。
Ridgepoleでは、まず以下のようにSchemafileを書き換えます。

# created_at カラムを追加
create_table "publishers", force: true do |t|
  t.string   "name",       limit: 100, null: false
  t.text     "url"
  t.datetime "created_at"
end

その後、以下のコマンドを叩くだけでDB定義の更新が完了します。

ridgepole -c config.yml --apply

# alter table publishers add column datetime DEFAULT NULL; というクエリが発行される

スキーマ更新の際にわざわざマイグレーションファイルを作る必要がありません。
テーブル定義更新もdrop, createじゃなくて、alter文を発行してくれます。データが消える心配もありません。
RidgepoleがDBの現在の定義と、Schemafileとの定義を見比べて、差分更新を行ってくれるためです。

これは便利!是非導入しよう。
と思って、Ridgepoleをあれこれ試していましたが、色々足りない機能がでてきました。

  • DBのカラムコメント、テーブルコメントが記述できない
  • コメント機能はよく使います。後でDBスキーマからER図を自動生成する際に利用するため
  • 外部キー制約が記述できない (※ 2014/10/21 現在は外部のgemを導入することで表現可能なようです)
  • 余談ですが、最近はみんな外部キー制約使わないの?
  • テーブルのOptionを変更できない

などなど。

正直この程度なら、Ridgepoleに手を加えれば直せます。実際、Ridgepoleを直そうと、色々試行錯誤をしておりました。

例えばカラムコメント追加は、以下のようにちょっと手直しすれば解決できました。

https://gist.github.com/tim-nishio/5912b60d2ee8e5059532

ですが、私の悪い癖がでてしまって、、、
あれこれいじっているうちに、既存のものを改良するより、自分で一から作ってみたくなってしまったんですよね。

スキーマ管理ツールを自作してみた

突如自分でもスキーマ管理ツールを作ってみたくなったので、自分の手になじむツールを自作してみました。
Convergenceという名前のスキーマ管理ツールです。

フルスクラッチで作っているため、ActiveRecord等他のgemには依存しない作りになっております。
なので、Railsじゃないプロジェクトでも利用できるかと思います。

使い方は、上記のgithubを見ていただければわかるかと思います。
コマンド体系はRidgepoleにインスパイア!されたので、ほぼ同じです。
DSLはActiveRecordのDSLとは似てるようで違うので、注意が必要です。

今あるDBスキーマをConvergenceのDSLにダンプしたい場合は、以下のコマンドでDSL出力が可能です。

$ convergence -c database.yml --export > example.schema

database.yml には以下のようなDB接続情報を記述しておきます。

adapter: mysql
database: example_database
host: 127.0.0.1
username: root
password:

これで、例えば以下のようなDSLがexample.schemaファイルに出力されます。

create_table "authors", collate: "utf8_general_ci", comment: "著者" do |t|
  t.int "id", primary_key: true, extra: "auto_increment"
  t.varchar "name", limit: 110, comment: "著者名"
  t.datetime "created_at", null: true
  t.datetime "updated_at", null: true

  t.index "created_at", name: "index_authors_on_created_at"
end

create_table "papers", collate: "utf8_general_ci", comment: "論文" do |t|
  t.int "id", primary_key: true, extra: "auto_increment"
  t.varchar "title1", limit: 300, comment: "タイトル1"
  t.varchar "title2", limit: 300, comment: "タイトル2"
  t.text "description", null: true, comment: "概要"
end

create_table "paper_authors", collate: "utf8_general_ci", comment: "論文/著者関連" do |t|
  t.int "id", primary_key: true, extra: "auto_increment"
  t.int "paper_id", comment: "論文ID"
  t.int "author_id", comment: "著者ID"

  t.foreign_key "author_id", reference: "authors", reference_column: "id"
  t.foreign_key "paper_id", reference: "papers", reference_column: "id"
end

DBの定義を変更したくなったら、example.schemaファイルを編集します。
例えば、papersテーブルにcreated_atカラムとupdated_at カラムを足してみます。

--- a/example.schema
+++ b/example.schema
@@ -12,6 +12,8 @@ create_table "papers", collate: "utf8_general_ci", comment: "論文" do |t|
   t.varchar "title1", limit: 300, comment: "タイトル1"
   t.varchar "title2", limit: 300, comment: "タイトル2"
   t.text "description", null: true, comment: "概要"
+  t.datetime "created_at", null: true
+  t.datetime "updated_at", null: true
 end

上記カラムを足した後に、以下コマンドでDryrunが可能です。

$ convergence -c database.yml -i example.schema --dryrun 

# ALTER TABLE `papers` ADD COLUMN `created_at` datetime DEFAULT NULL AFTER `description`;
# ALTER TABLE `papers` ADD COLUMN `updated_at` datetime DEFAULT NULL AFTER `created_at`;

applyコマンドで、上記ALTER文をDBに対して発行することができます。

$ convergence -c database.yml -i example.schema --apply

SET FOREIGN_KEY_CHECKS=0;
  --> 0.00072s
ALTER TABLE `papers` ADD COLUMN `created_at` datetime DEFAULT NULL AFTER `description`;
  --> 0.040411s
ALTER TABLE `papers` ADD COLUMN `updated_at` datetime DEFAULT NULL AFTER `created_at`;
  --> 0.02394s
SET FOREIGN_KEY_CHECKS=1;
  --> 0.000158s

これでスキーマ変更も気軽に行えるようになりました。

まとめ

ActiveRecordの標準DBマイグレーションを、開発初期フェーズで利用するのは辛いものです。
RidgepoleConvergence 等のツールを利用することで、スキーマ変更にかかる手間を最小限に抑えることができます。

DBマイグレーションの運用に辛くなったら、これらのツールの導入を検討してみてはいかがでしょうか。