RDBMS のスキーマ管理ツール Ridgepole を使ってみたのでメモ。
公式
Ridgepole
http://ridgepole.codenize.tools/
winebarrel/ridgepole
https://github.com/winebarrel/ridgepole
winebarrel/ridgepole-example
https://github.com/winebarrel/ridgepole-example
インストール
% gem install ridgepole mysql2 --no-rdoc --no-ri
% ridgepole -v
ridgepole 0.6.4
使ってみる
テーブル定義などは ridgepole-example のものを使わせて頂いた。
作業用に適当なディレクトリを作成。
% mkdir ridgepole-example && cd ridgepole-example
データベースを2つ作成。
% mysql -u root -e "CREATE DATABASE IF NOT EXISTS blog DEFAULT CHARACTER SET utf8;"
% mysql -u root -e "CREATE DATABASE IF NOT EXISTS blog_prod DEFAULT CHARACTER SET utf8;"
Schemafile を作成。
% cat << _EOF_ > Schemafile
create_table "articles", force: true do |t|
t.string "title"
t.text "text"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "comments", force: true do |t|
t.string "commenter"
t.text "body"
t.integer "article_id"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "comments", ["article_id"], name: "index_comments_on_article_id", using: :btree
_EOF_
ドライラン実行。
DB 接続設定は引数でも指定できるようなのでここでは引数を使ってみた。
% ridgepole -c '{adapter: mysql2, socket: /tmp/mysql.sock, database: blog}' --apply --dry-run -f Schemafile
Apply `Schemafile` (dry-run)
create_table("articles", {}) do |t|
t.string("title", {:limit=>255})
t.text("text", {:limit=>65535})
t.datetime("created_at", {})
t.datetime("updated_at", {})
end
create_table("comments", {}) do |t|
t.string("commenter", {:limit=>255})
t.text("body", {:limit=>65535})
t.integer("article_id", {:limit=>4})
t.datetime("created_at", {})
t.datetime("updated_at", {})
end
add_index("comments", ["article_id"], {:name=>"index_comments_on_article_id", :using=>:btree})
# CREATE TABLE `articles` (
# `id` int(11) auto_increment PRIMARY KEY,
# `title` varchar(255),
# `text` text,
# `created_at` datetime,
# `updated_at` datetime)
# ENGINE=InnoDB
# CREATE TABLE `comments` (
# `id` int(11) auto_increment PRIMARY KEY,
# `commenter` varchar(255),
# `body` text,
# `article_id` int(11),
# `created_at` datetime,
# `updated_at` datetime)
# ENGINE=InnoDB
# CREATE INDEX `index_comments_on_article_id` USING btree ON `comments` (
# `article_id`)
当然DB接続設定はファイルに書く事も出来るので、ここでファイルを作成しとく。
% cat << _EOF_ > database.yml
development:
adapter: mysql2
encoding: utf8
socket: /tmp/mysql.sock
database: blog
username: root
production:
adapter: mysql2
encoding: utf8
socket: /tmp/mysql.sock
database: blog_prod
username: root
_EOF_
Rails の database.yml と同じ形式なのでそのまま使う事も可能。
実際に DB に適用してみる。
ここでは development
環境の DB にのみ適用したいので -E development
を指定。
% ridgepole -c database.yml -E development --apply -f Schemafile
Apply `Schemafile`
-- create_table("articles", {})
-> 0.1952s
-- create_table("comments", {})
-> 0.0540s
-- add_index("comments", ["article_id"], {:name=>"index_comments_on_article_id", :using=>:btree})
-> 0.0734s
テーブルが作成された事を確認。
% mysql -uroot blog -e "SHOW TABLES; SHOW CREATE TABLE articles\G SHOW CREATE TABLE comments\G"
+----------------+
| Tables_in_blog |
+----------------+
| articles |
| comments |
+----------------+
*************************** 1. row ***************************
Table: articles
Create Table: CREATE TABLE `articles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) DEFAULT NULL,
`text` text,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
*************************** 1. row ***************************
Table: comments
Create Table: CREATE TABLE `comments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commenter` varchar(255) DEFAULT NULL,
`body` text,
`article_id` int(11) DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_comments_on_article_id` (`article_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8
同様にして production
にも適用しておく。
% ridgepole -c database.yml -E production --apply -f Schemafile
テーブル定義のエクスポート
既に存在する DB 上のテーブル定義を Schemafile にエクスポートする事も出来る。
まずは先ほど作成した development
用 DB に適当にカラムを追加してみる。
% mysql -uroot blog -e 'ALTER TABLE `articles` ADD `category_id` INT DEFAULT NULL AFTER `text`; SHOW CREATE TABLE articles\G'
*************************** 1. row ***************************
Table: articles
Create Table: CREATE TABLE `articles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) DEFAULT NULL,
`text` text,
`category_id` int(11) DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
テーブル定義をエクスポート。
% mkdir schemas
% ridgepole -c database.yml -E development --export --output schemas/Schemafile
Export Schema to `schemas/Schemafile`
出力された Schemafile を確認。
追加したカラムも反映されている。
% cat schemas/Schemafile
create_table "articles", force: :cascade do |t|
t.string "title", limit: 255
t.text "text", limit: 65535
t.integer "category_id", limit: 4
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "comments", force: :cascade do |t|
t.string "commenter", limit: 255
t.text "body", limit: 65535
t.integer "article_id", limit: 4
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "comments", ["article_id"], name: "index_comments_on_article_id", using: :btree
テーブル毎にスキーマファイルを分割したい場合は --split
オプションを指定する。
% ridgepole -c database.yml -E development --export --split --output schemas/Schemafile
Export Schema
write `schemas/articles.schema`
write `schemas/comments.schema`
write `schemas/Schemafile`
この機能があるので、ローカルでは直接 DB を変更して開発し、定義が固まったらエクスポートしてコミット、という方法も取れる。
DSL に慣れない場合は当面この方法でも良さそう。
テーブル定義変更
今度は Schemafile 側を変更してみる。
% sed -i '/"text".*$/a \ t.text "author", limit: 65535' schemas/articles.schema
% cat schemas/articles.schema
create_table "articles", force: :cascade do |t|
t.string "title", limit: 255
t.text "text", limit: 65535
t.text "author", limit: 65535
t.integer "category_id", limit: 4
t.datetime "created_at"
t.datetime "updated_at"
end
--apply --dry-run
してみて発行される ALTER 文を確認。
% ridgepole -c database.yml -E development --apply --dry-run -f schemas/Schemafile
Apply `schemas/Schemafile` (dry-run)
add_column("articles", "author", :text, {:limit=>65535, :after=>"text"})
# ALTER TABLE `articles` ADD `author` text AFTER `text`
DB との差分を確認
現在の DB 上の定義との差分も確認出来る。
ただ、 Schemafile を分割している場合は全てカレントディレクトリに存在していないといけないっぽい。
コメント欄で教えて頂いた通り、バージョン 0.5.1 で分割された Schemafile でも --diff
が使えるようになった。
% ridgepole -E development --diff database.yml schemas/Schemafile
add_column("articles", "author", :text, {:limit=>65535, :after=>"text"})
# ALTER TABLE `articles` ADD `author` text AFTER `text`
development
環境を Schemafile にダンプしてあるので、-E production
を指定すれば production
環境との差分も確認可能。
% ridgepole -E production --diff database.yml schemas/Schemafile
add_column("articles", "author", :text, {:limit=>65535, :after=>"text"})
add_column("articles", "category_id", :integer, {:limit=>4, :after=>"author"})
# ALTER TABLE `articles` ADD `author` text AFTER `text`
# ALTER TABLE `articles` ADD `category_id` int(11) AFTER `author`
また、 DB 接続設定ファイルを別ファイルに分ければエクスポートを経由しなくても DB 同士の差分を比較できた。
adapter: mysql2
encoding: utf8
socket: /tmp/mysql.sock
database: blog
username: root
adapter: mysql2
encoding: utf8
socket: /tmp/mysql.sock
database: blog_prod
username: root
% ridgepole --diff production.yml development.yml
add_column("articles", "category_id", :integer, {:limit=>4, :after=>"text"})
# ALTER TABLE `articles` ADD `category_id` int(11) AFTER `text`
--diff
オプションの引数は current
expected
の順だが、 --reverse
を指定すると逆向きの DDL を出力してくれる。
% ridgepole -E production --diff database.yml schemas/Schemafile --reverse
remove_column("articles", "author")
remove_column("articles", "category_id")
# ALTER TABLE `articles` DROP `author`
# ALTER TABLE `articles` DROP `category_id`
パーティション
SHOW CREATE TABLE
した時にコメントで出力されるパーティションをエクスポート出来るようにするには別途 activerecord-mysql-awesome
gem が必要な模様。
インストール。
% gem install activerecord-mysql-awesome
Fetching: activerecord-mysql-awesome-0.0.8.gem (100%)
Successfully installed activerecord-mysql-awesome-0.0.8
1 gem installed
適当にパーティションを追加。
% mysql -uroot blog -e 'ALTER TABLE `articles` PARTITION BY HASH(id) PARTITIONS 8; SHOW CREATE TABLE articles\G'
*************************** 1. row ***************************
Table: articles
Create Table: CREATE TABLE `articles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) DEFAULT NULL,
`text` text,
`category_id` int(11) DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
/*!50100 PARTITION BY HASH (id)
PARTITIONS 8 */
再度スキーマをエクスポート。
activerecord-mysql-awesome
を有効にするための --enable-mysql-awesome
オプションを指定。
% ridgepole -c database.yml -E development --enable-mysql-awesome --export --split --output schemas/Schemafile
Export Schema
write `schemas/articles.schema`
write `schemas/comments.schema`
write `schemas/Schemafile`
以下のようにパーティションが出力されている事を確認。
% cat schemas/articles.schema
create_table "articles", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8\n/*!50100 PARTITION BY HASH (id)\nPARTITIONS 8 */" do |t|
t.string "title", limit: 255
t.text "text", limit: 65535
t.integer "category_id", limit: 4
t.datetime "created_at"
t.datetime "updated_at"
end
試しに --mysql-use-alter
オプションも併せて指定してみたが、PARTITION
作成文は ALTER
文としては出力されない模様。
このスキーマを production
用 DB に適用してみる。
create_tagble
で出力されているので、一旦 articles
を DROP TABLE
してから実行。
% mysql -uroot blog_prod -e 'DROP TABLE `articles`;'
% ridgepole -c database.yml -E production --merge -f schemas/Schemafile
Merge `schemas/Schemafile`
[WARNING] No difference of schema configuration for table `comments`. (if you changed some options, please reconfirm your Schemafile)
-- create_table("articles", {:options=>"ENGINE=InnoDB DEFAULT CHARSET=utf8\n/*!50100 PARTITION BY HASH (id)\nPARTITIONS 8 */"})
-> 0.1177s
確認。
% mysql -uroot blog_prod -e 'SHOW CREATE TABLE `articles`\G'
*************************** 1. row ***************************
Table: articles
Create Table: CREATE TABLE `articles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) DEFAULT NULL,
`text` text,
`category_id` int(11) DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
/*!50100 PARTITION BY HASH (id)
PARTITIONS 8 */
ちゃんとパーティションも作成されたっぽい。
unsigned 型
category_id
の型を unsigned int
に変更。
% mysql -uroot blog -e 'ALTER TABLE `articles` MODIFY `category_id` INT unsigned DEFAULT NULL; SHOW CREATE TABLE `articles`\G'
*************************** 1. row ***************************
Table: articles
Create Table: CREATE TABLE `articles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) DEFAULT NULL,
`text` text,
`category_id` int(10) unsigned DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
/*!50100 PARTITION BY HASH (id)
PARTITIONS 8 */
articles
テーブルのスキーマをエクスポート。
--tables
オプションに続いてテーブル名を指定すると該当のテーブルのみエクスポートされる。
が、その場合親の Schemafile
も指定したテーブルのスキーマファイルのみが require
されるように書き換わってしまうので注意。
また、 activerecord-mysql-awesome
を有効にするための --enable-mysql-awesome
オプションが無いと unsigned
が出力されない模様。
# --enable-mysql-awesome 無し
% ridgepole -c database.yml -E development --tables articles --export --split --output schemas/Schemafile
# unsigned の情報が消えてしまっている
% cat schemas/articles.schema
create_table "articles", force: :cascade do |t|
t.string "title", limit: 255
t.text "text", limit: 65535
t.integer "category_id", limit: 4
t.datetime "created_at"
t.datetime "updated_at"
end
# --enable-mysql-awesome 有り
% ridgepole -c database.yml -E development --enable-mysql-awesome --tables articles --export --split --output schemas/Schemafile
# unsigned: true が出力された
% cat schemas/articles.schema
create_table "articles", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8\n/*!50100 PARTITION BY HASH (id)\nPARTITIONS 8 */" do |t|
t.string "title", limit: 255
t.text "text", limit: 65535
t.integer "category_id", limit: 4, unsigned: true
t.datetime "created_at"
t.datetime "updated_at"
end
他の DB に反映させるには --apply
時にも --enable-mysql-awesome
オプションの指定が必要なので注意する。
外部キー制約
外部キー制約を追加してみる。
手元で使っていた 5.6 ではパーティションが存在すると外部キー制約を作成できないようなので一旦 articles
を DROP
して作り直し。
% mysql -uroot blog -e 'DROP TABLE `articles`'
% ridgepole -c database.yml -E development --tables articles --apply -f schemas/Schemafile
% mysql -uroot blog -e 'SHOW CREATE TABLE `articles`\G'
*************************** 1. row ***************************
Table: articles
Create Table: CREATE TABLE `articles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) DEFAULT NULL,
`text` text,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
外部キー制約を作成。
% mysql -uroot blog -e 'ALTER TABLE `comments` MODIFY `article_id` INT NOT NULL;'
% mysql -uroot blog -e 'ALTER TABLE `comments` ADD CONSTRAINT `fk_article_id_on_comments` FOREIGN KEY (`article_id`) REFERENCES `articles` (`ID`); SHOW CREATE TABLE `comments`\G'
*************************** 1. row ***************************
Table: comments
Create Table: CREATE TABLE `comments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commenter` varchar(255) DEFAULT NULL,
`body` text,
`article_id` int(11) NOT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_comments_on_article_id` (`article_id`) USING BTREE,
CONSTRAINT `fk_article_id_on_comments` FOREIGN KEY (`article_id`) REFERENCES `articles` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
作成したテーブルからスキーマファイルをエクスポート。
% ridgepole -c database.yml -E development --export --split --output schemas/Schemafile
% cat schemas/comments.schema
create_table "comments", force: :cascade do |t|
t.string "commenter", limit: 255
t.text "body", limit: 65535
t.integer "article_id", limit: 4, null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "comments", ["article_id"], name: "index_comments_on_article_id", using: :btree
add_foreign_key "comments", "articles", name: "fk_article_id_on_comments"
ちゃんと外部キー制約もエクスポートされた。
一旦 production を DROP
して適用してみる。
% mysql -uroot blog_prod -e 'DROP TABLE `comments`, `articles`'
% ridgepole -c database.yml -E production --apply -f schemas/Schemafile
Apply `schemas/Schemafile`
-- create_table("articles", {})
-> 0.0093s
-- create_table("comments", {})
-> 0.0073s
-- add_index("comments", ["article_id"], {:name=>"index_comments_on_article_id", :using=>:btree})
-> 0.0142s
-- add_foreign_key("comments", "articles", {:name=>"fk_article_id_on_comments"})
-> 0.0157s
適用されたっぽい。
確認。
% mysql -uroot blog_prod -e 'SHOW CREATE TABLE `comments`\G'
*************************** 1. row ***************************
Table: comments
Create Table: CREATE TABLE `comments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commenter` varchar(255) DEFAULT NULL,
`body` text,
`article_id` int(11) NOT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_comments_on_article_id` (`article_id`) USING BTREE,
CONSTRAINT `fk_article_id_on_comments` FOREIGN KEY (`article_id`) REFERENCES `articles` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
外部キー制約が作成された。
感想
Rails の Migration に慣れているなら standalone-migrations もあるけど、Ridgepole なら DB を直接弄った場合でもちゃんと管理出来る。
開発中にちょっとした変更でいちいち Migration 弄るのは面倒くさくて好きじゃないのでこれは結構嬉しいかも。
DSL も違和感ないので今 Migration 使ってる場合でも移行するのは簡単そう。
参考
クックパッドにおける最近のActiveRecord運用事情 - クックパッド開発者ブログ
http://techlife.cookpad.com/entry/2014/08/28/194147
Ridgepole を PostgreSQL と組み合わせて使う - Qiita
http://qiita.com/Tomohiro/items/89846cc2cfdf53ac993e