ridgepoleのメリットと使い方

Ruby on Railsの標準的なデータベース定義ツールはActiveRecordの”マイグレーション”です。これは、データベース定義の変更点をSQLではなくRubyスクリプトで記述し、バージョン管理を可能にするものです。
ただ、マイグレーションはDB定義の変更が増えてくると無数のファイルが生成されて見通しが悪くなります。

ridgepole を使うことで、マイグレーションと同様の記法でDB定義を1つの設定ファイルに集約でき、冪等な設定で運用できるようになります。また、RDBMSのドライバーを切り替えることで、PostgreSQL / MySQL / SQLite などデータベース間のスキーマ移行も簡単になります。

旧聞ながら、 クックパッドにおける最近のActiveRecord運用事情 の説明から、クックパッド本サービスの大規模環境で利用されていることが分かります。

同じ目的でRails非依存のツールに pistachio や sqldef があります。 PostgreSQLについてはpistachioの完成度が高いと言えます。いずれが適しているかはプロジェクトの重点により決めます。

Rails以外のフレームワーク向けの活用

ridgepoleはRailsをターゲットとして作られたツールですが、Railsを導入していない環境でも活用できます。
たとえばPHPのWebフレームワークでもマイグレーションによるスキーマ管理を行うものがあり、メリット・デメリットの事情は同じです。

結局のところDB管理は、本線アプリのコーディングと別の管理が求められるため、ridgepoleを利用する余地はあります。
ただし、利用しているWebフレームワークとDB上のスキーマの規約を理解したうえで使う必要はあります。

RailsマイグレーションDSLの影響

ridgepoleのスキーマ定義は、 Railsマイグレーションのcreate_table と同じ文法のDSLで書きます。
過不足なくridgepole向けといえるリファレンスがなく、Railsマイグレーションの解説には周辺情報が付いてしまっている点は分かりづらいかもしれません。

説明はないのですが、ActiveRecordがサポートしている拡張型も利用できます。 RailsガイドのPostgreSQL解説 に説明があります。
特殊な型もありますが、 UUID やBytea、JSONを確認したところマイグレートできました。

このようにRailsのActiveRecordが備えている機能は、実際には使えるものが多くあり、おそらくユーザーが想像しているよりridgepoleは高機能です。
またDSLではありますが、Rubyで書いた文も一部有効であるため、同じ構成の複数のテーブルを簡潔に定義できたりもします。

一方で、スキーマがRailsのデフォルトの変更に影響される面もあります。
過去の例では、idのデフォルト型がinteger->bigintに拡張されたり、timestampの詳細精度が変更されたりしました。

Railsはベストプラクティスを強いるフレームワークであり、ユーザーよりも速く進化していきます。
作成タイミングによってDBスキーマが異なるということも起きるのがRails wayなのです。

インストールと基本的な使い方

セットアップは、bundlerを利用する場合、Gemfileに gem “ridgepole” を追加してbundle installです。

ridgepoleの基本的な使い方は、Schemafileというファイルにデータベース定義を記述しておき、以下のようにridgepoleコマンドでDBMSに設定を反映するだけです。

$ bundle exec ridgepole -c config/database.yml -f config/Schemafile -a

このコマンドにより、データベースのスキーマがSchemafileの定義と同じ状態になるようにDDLが発行されます。(--dry-runオプションを付けるとDDLの内容を確認できます)

引数の-aがapply、-cはRails標準のデータベース接続設定ファイルdatabase.ymlを指定しています。database.ymlは一般的なRailsの接続設定と全く同じです。

Schemafileにはカラムの定義にくわえて、インデックス、複合インデックスの定義も可能です。テーブル定義がまとまっているので、MySQLでdescribeしなくてもSchemafileを読めばクラス構造をほぼ把握できます。

カラム定義に指定できるオプションは、 ActiveRecordのadd_column と同様です。

database.ymlの環境変数をコマンドラインから指定

docker / kubernetes の環境で、databaseの接続ホストが動的に決まるためdatabase.ymlにerbを用いて変数で指定したいケースがあります。Railsは

host: <%= EN['PG1_SERVICE_HOST'] %>
password: <%= ENV['POSTGRES_PASSWORD'] %>

のような記述で環境変数から接続情報を取得できるのですが、ridgepoleコマンドがテンプレート変数を処理できないケースに遭遇しました。

この場合、

$ ridgepole -c config/database.yml -f config/Schemafile -E PG1_SERVICE_HOST=192.168.0.1 -E POSTGRES_PASSWORD=connectionpass -a

と-Eオプションで設定することで期待どおりに動作しました。

Rails向けの使い方

CREATE DATABASE/DROP DATABASEはRails標準のrails dbを利用して、以下のようなコマンドセットでライフサイクル管理が可能になります。(PostgreSQL/MySQLに直接接続してCREATE DATABASEする手順でもとくに問題なく動作します)

初歩的な注意点

ridgepoleを使う場合、マイグレーションを併用しないことが重要です。一見当然ですが、たとえばrailsコマンドでモデルを生成するとデフォルトでmigrationファイルも生成されるため、

$ rails g model SomeNewModel --skip-migration

のようにマイグレーションを作らないオプションを付けます。
不要なマイグレーションファイルが存在していると実行時にエラーになります。誤ってマイグレーションファイルが生成されてしまった場合には、単純にdb/migrate/にあるマイグレーションファイルをrmで削除します。

また、べき等な動作の結果、テーブル名・カラム名変更のつもりで安直に記述するとデータが消えうるため注意が必要です。名称変更の手順は、[ridgepoleでテーブル名・カラム名の変更手順][3]で解説しています。

スキーマ変更後にインスタンス再起動

ridgepoleコマンドでDBスキーマを変更したあと、Railsから認識するためにはAPサーバーインスタンスを再起動すべきなようです。

カラム追加後、再起動しない状態でアクセスしたところ、ActiveModel::MissingAttributeError (can’t write unknown attribute…のようなエラーに直面して、再起動したところ直りました。

運用中の既存DBに途中から導入

既存のデータベースのスキーマ管理のため、途中からridgepoleを導入したい場合、-eオプションのスキーマダンプを活用できます。

データベース接続設定(ホスト、ID・パスワードなど)をconfig.ymlに記載し、

$ ridgepole -c config.yml -e -o Schemafile

を実行すると、データベース内のテーブル一式を定義したスキーマ設定ファイルを得られます。

安易に定義ファイルを分割すると危険だった

version 1.0で、未定義のテーブルが実DBに存在する場合にもテーブルを削除しない挙動となり、安全性が増しています。
version 0.9以前では、ファイルをテーブル別に分割していて一部のファイルだけ参照してapplyしてしまうと、記述のないテープルについてはDROP TABLEの動作になり削除されます。

一般的には、version1.0以降を使用していることをよく確認すると安全です。

制約(CONSTRAINT)の追加

たとえば、PostgreSQLでUPSERTを利用するためにはユニーク制約が必要になりますが、制約(CONSTRAINT)を羃等に実装することは単純ではありません。
SQLで実装しようとするとPostgres: Add constraint if it doesn’t already existの議論のような工夫が必要になります。

ridgepoleでは、executeのブロックに実行条件を定義できるため、ADD CONSTAINTをIF NOT EXISTと同じように条件付きで実行できます。
公式ページにforeign key制約の例が掲載されており、PostgreSQL用に書き直すと、以下のようなコードになります。

execute("ALTER TABLE books ADD CONSTRAINT fk_author FOREIGN KEY (author_id) REFERENCES authors (id)") do |c|
  c.raw_connection.query(<<-SQL).each.size.zero?
    SELECT 1 FROM information_schema.table_constraints
    WHERE table_name = 'books' AND constraint_name = 'fk_author' LIMIT 1
  SQL
end

また、PostgreSQLの場合、制約とインデックスは関連する別物であり、制約がインデックスを必要とするため、同名のインデックスをadd_indexで貼っておかないと、2回目の実行時にエラーになります。

ユニークインデックスの実例は以下の通りです。インデックスはadd_indexで定義し、制約からはUSING INDEXで参照する、というテクニックが必要になります。

add_index("books", ["author_id"], name: "unique_author", unique: true)
execute("ALTER TABLE books ADD CONSTRAINT unique_author UNIQUE USING INDEX unique_author") do |c|
  c.raw_connection.query(<<-SQL).each.size.zero?
    SELECT 1 FROM information_schema.table_constraints
    WHERE table_name = 'books' AND constraint_name = 'unique_author' LIMIT 1
  SQL
end

これ以外の指定方法では、初回実行または2回目以降の実行で相互にコンフリクトしてエラーになります。

この制約を利用したUPSERTの書き方については、 PostgreSQLで重複レコードをINSERT で解説しています。
UPSERTもexecute()で羃等に実行可能であるため、システム必須条件のマスターレコードもridgepoleでセットアップしてしまうのも一手です。

ridgepoleの基本機能はDDLの管理ですが、ライフサイクルがDDLと同様のレコードも集約するとコードの凝集性は向上します。

REPLACE句のないリソース

TYPEのようにOR REPLACEオプションのないリソースは、以下のように重複時の例外処理を追加するとridgepoleでべき等に実行できます。

execute("DO $$ BEGIN
    CREATE TYPE comment AS (
      id bigint,
      description text
    );
  EXCEPTION
    WHEN duplicate_object THEN null;
  END $$;")

ただし、変更を加えたい場合には、REPLACEはしないため一度削除して再CREATEする必要がある点に注意します。

テーブル名・カラム名の変更手順

運用中のテーブル名やカラム名をリネームしたい場合には注意が必要です。

カラム名変更のケースでは設定ファイル中の列名を単に書き換えると、旧名のカラム削除&新名称のカラム作成という挙動になり、旧名で保持していた既存のカラムデータが消えます。

期待している列名変更のための方法は、renamed_fromオプションを指定することです。

create_table "articles", force: :cascade do |t|
  t.text     "desc", renamed_from: "text"
end

蛇足ですがrenamed_fromオプションで追記している部分が旧名で、text→descの変更例です。

なお、この構成ファイルをDBに適用してDB上の列名が新しい名前に変わったらrenamed_fromオプションをトルツメしても問題ありません。
たとえば再々変更を行いたい場合には、renamed_fromにDB上の現行名称(上のケースではdesc)を指定すれば名称変更動作になります。

カラム名移行のオペレーションは、(1)ターゲットのDBすべてに変更を適用&確認し、(2)renamed_fromを消したうえで(3)再度ridgepole -aで挙動確認する、段階まで集中的に実行しきって過渡的な状態を残さない方が安全かもしれません。

また、ridgepole外の手順になりますが、SQLで直接ALTER TABLE発行し、変更後のカラム名をridgepoleの構成ファイルに記載する(renamed_fromオプションを使わない)という手順でもカラムデータを失わずに変更できます。

構成ファイル上の名称とDB上の名称が一致している状態であればridgepoleからDDL発行が起きない、名称が違っている状態の基本挙動は削除&作成という挙動を把握しておくと良いでしょう。
なお、テーブル変更の際も同様にrenamed_fromオプションが利用できます。

同一テーブルを定義

Railsなどのアプリケーションで、古いデータを保管するためのアーカイブテーブルを作りたい場合があります。

ridgepoleのスキーマ定義はrubyのDSLになっているため、以下のようにテーブル名の配列からデッドコピーのスキーマを定義できました。

["users", "a_users"].each do |tbl_name|
  create_table tbl_name do |t|
    t.string        "name"
    t.string        "email", :null =&gt; false
    t.timestamps
  end
  add_index tbl_name, ["name"]
end

これにより、オンラインテーブルのスキーマを変更する際にアーカイブテーブルが不整合になるトラブルを避けられます。

保証されていない使い方のような気もしますが、現状、PostgreSQLとの組み合わせでは、とくに問題なく動作しています。

アーカイブ処理の実装例

このスキーマ定義の例ではUserモデル(usersテーブル)に以下のようなメソッドを定義することで、AUser(a_usersテーブル)にデータを移動できます。

def archive!
  AUser.create! self.attributes
  self.delete
end

トランザクション は途中でエラーが起きるとロールバックするまでロックされるため避けています。

スキーマ指定

PostgreSQLには商用DBと同様のスキーマというネームスペースがあります。MySQLはCREATE SCHEMAがCREATE DATABASEの別名であり、スキーマのレイヤがありません。

以下のように、接続設定のdatabase.yamlにschema_search_pathオプションを追加することで、ridgepoleもスキーマ内のテーブルメンテナンスが可能です。

development:
  adapter: postgresql
  encoding: unicode
  database: some_db
  host: localhost
  username: scott
  password: tiger
  schema_search_path: "some_schema"

なお、schema_search_pathオプション(pg gem由来)自体はカンマ区切りで複数のスキーマを指定できますが、ridgepoleとの組み合わせでは適切に動作しませんでした。

また、当然ですが、ridgepoleからDDLを実行する前に、該当のデータベース(上の例ではsome_db)に接続のうえ、CREATE SCHEMAでスキーマを作成しておく必要があります。psql接続時にDB指定を省略するとpostgresデータベースに接続するため、目的のDB内にスキーマを作成できていない、という失敗が起こり得ます。

idにUUIDを指定する

おなじみのとおり、Ruby on Railsの各モデルのidは自動生成される連番数値(INTEGER)がデフォルトです。ただ、Rails 4以降では、設定しだいでUUIDも使えます。

ある程度以上の規模のサービスを考えるとAUTO INCREMENTでは何かの拍子にモデル間の不整合が起きる可能性があるため、「事実上衝突しない」というメリットがあるUUIDの利用を検証してみました。

結論からいうと、ridgepoleとPostgreSQLを用いた構成では、以下の手順でPRIMARY KEYにuuidを利用できます。

  1. migrationでPostgreSQLのuuid機能をオンに設定する
  2. ridgepoleのidオプションに:uuidを指定して、apply

migrationでPostgreSQLのuuid機能をオンにする

まずmigrationについては、 Rails 5.1 + Using a UUID as a primary key in ActiveRecord with PostgreSQL に記載されているmigrationを作成・実行します。

$ rails g migration enable_pgcrypto_extension

db/migrateに生成されたファイルに1行追加。

class EnablePgcryptoExtension &lt; ActiveRecord::Migration[5.1]
  def change
    enable_extension 'pgcrypto'
  end
end

$ rails db:migrateを実行

== 20150705164619 EnablePgcryptoExtension: migrating ==========================
-- enable_extension("pgcrypto")
-> 0.0629s== 20150705164619 EnableUuidOsspExtension: migrated (0.0095s) =================

ridgepoleのスキーマ定義でidをuuidに

ridgepoleのSchemafileで、create_tableのオプションにid: :uuidを指定することで、idの設定が変わります。

create_table "users", id: :uuid do |t|
end
$ ridgepole -c config/database.yml -f config/Schemafile -a

を実行。

Apply `config/Schemafile`
-- create_table("users", {:id=>:uuid})
-> 0.0161s

pgcryptoを有効にしていないときのエラー

ridgepoleを使っているのにmigrationも実行するというのは中途半端な印象もありますが、ridgepoleではpgcryptoをenableできず、以下のようなエラーになります。

$ ridgepole -c config/database.yml -f config/Schemafile -a

Apply `config/Schemafile`
-- create_table("users", {:id=&gt;:uuid})
[ERROR] PG::UndefinedFunction: ERROR:  function gen_random_uuid() does not exist
HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
: CREATE TABLE "users" ("id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY, "name" character varying, "created_at" timestamp NOT NULL, "updated_at" timestamp NOT NULL)
*  1: create_table("users", {:id=&gt;:uuid}) do |t|
   2:  t.string("name", {})

ほかの手段としては、以下のようにpsqlコマンドからPostgreSQLを直接操作して設定をオンにすることもできます。

    postgres=# c SOME_DB_NAME;
    SOME_DB_NAME=# create extension "pgcrypto";

ただし、環境設定をConfigファイル化しておかないと、リカバリの際などに作業漏れの危険もあるため、とくにridgepoleの動作に支障がなければmigrationの利用は現実的な妥協点として成立するように思います。

⁋ 2015/06/27↻ 2026/10/08