Mermaid の ER 図から Rails のマイグレーションファイルを生成する
対象とする読者
- Rails 開発の設計で Mermaid を使っている人
- ドキュメントとモデルを同期したい人
- Markdown パーサーを開発してみたい人
はじめに
Rails 開発で新機能を開発する際、新たに追加するモデルや、その関連付けについての議論が重要だと感じる方が多いのではないでしょうか。
筆者が所属するチームでも、モデリングの議論は新規開発の初期の工程の多くを占めます。
Mermaid で描いた ER 図の例
オンラインでのミーティングが多い昨今、モデリングの議論の場面では、ドキュメンテーションツールに書いた ER 図を叩き台に議論する機会が多いです。
Notion を使うことが多いため、最近対応した Mermaid 記法で ER 図を書くことが特に多くなっています。
この Mermaid 記法は GitHub の PullRequest でも図の出力に対応していることから、最近使用頻度が増えてきています。
Mermaid の ER 図で議論するメリット・デメリット
Mermaid は Notion でも GitHub でも表示機能が強化されているため、ER 図を使ってモデルの関連付けを意識するのに適しています。
ただ、ER 図として Mermaid 記法で書いた内容を Rails のマイグレーションファイルに書き起こすのが手間に感じていました。
モデリングの議論で合意をとった内容は、なるべく早くマイグレーションしてしまいたいものです。また、モデルが複数個ある場合は、マイグレーションファイルに書き起こした際にリレーションや制約を間違える可能性もあります。
Mergration
そこで、 Mermaid の ER 図から Rails のマイグレーションファイルを生成する gem mergration
を作りました。
mergration
という名称は mermaid
と migration
をミックスしたもので、rails generate
コマンドでマイグレーションファイルを生成するのと同じ感覚で、mermaid を書いたファイルからマイグレーションファイルを生成できます。
コマンド実行のデモ
仕組み
mergration は大きく二つの処理を実行しています。
- Mermaid 記法のパース
- マイグレーションファイルの生成
以下、順にご説明したいと思います。
1. Mermaid 記法のパース
まず必要なのが、mermaid 記法のパース処理です。
そのために markdown パーサーである kramdown を拡張し、mermaid をパースできる gem を作りました(現状は ER 図のみパースできます)。
この処理によって、パース結果が抽象構文木と呼ばれる構造のハッシュとして得られます。
こちらの実装の詳細については別途記事にしたいと思っています。
具体例として、以下のような mermaid の ER 図があったとします。
erDiagram
hoges {
bigint id PK
int price
date start_on
datetime created_at
datetime updated_at
}
fugas {
bigint id PK
int price
datetime created_at
datetime updated_at
}
そのパース結果は以下の通りです。
# =>
{:type=>:root,
:options=>{:encoding=>#<Encoding:UTF-8>, :location=>1, :options=>{}, :abbrev_defs=>{}, :abbrev_attr=>{}, :footnote_count=>0},
:children=>
[{:type=>:entity,
:value=>" \n fugas {\n bigint id PK\n int price\n datetime created_at\n datetime updated_at\n }",
:options=>
{:entity=>"fugas",
:attributes=>
[{:type=>"bigint", :name=>"id", :constraint=>"PK"},
{:type=>"int", :name=>"price", :constraint=>nil},
{:type=>"datetime", :name=>"created_at", :constraint=>nil},
{:type=>"datetime", :name=>"updated_at", :constraint=>nil}],
:location=>24}},
2. マイグレーションファイルの生成
具体的なマイグレーションファイルの生成と、テストの書き方は、 paper-trail gem が参考になりました。
実装の大きな流れとしては以下の通りです。
- erb ファイルでマイグレーションファイルのテンプレートを用意する
class Create<%= table_name %> < ActiveRecord::Migration<%= migration_version %>
def change
create_table :<%= entity %> do |t|
<%- attributes.each do |attribute| -%>
<%= ["t.#{attribute[:type]} :#{attribute[:name]}", attribute[:constraint]].compact.reject(&:empty?).join(', ') %>
<%- end -%>
t.timestamps
end
end
end
-
::Rails::Generators::Base
を継承した独自ジェネレータクラスで、テンプレート呼び出しメソッドを上書きする(マイグレーションファイル内で参照する変数はそのメソッドにオプションとして渡す)
module Mergration
class TypeError < StandardError; end
class NotFoundError < StandardError; end
class InstallGenerator < MigrationGenerator
source_root File.expand_path('templates', __dir__)
desc 'Generates a migration from entities described on mermaid.'
def create_migration_file
files = parse_file
files.each do |file|
file.each do |f|
next unless f[:type] == :entity
@entity = f[:options][:entity]
@attributes = f[:options][:attributes]
@attributes.each do |attribute|
type = attribute[:type]
unless ActiveRecord::ConnectionAdapters::Table.instance_methods.include?(type.to_sym)
raise Mergration::TypeError, "Invalid type '#{type}' is given for #{attribute[:name]}"
end
end
add_mergration_migration(
'create_entity',
table_name: table_name,
entity: entity,
attributes: attributes
)
end
end
end
- 上記のパース結果のハッシュをオプションとしてテンプレートメソッドに渡す
おわりに
現在は .md ファイルに記述した mermaid の ER 図のみが対象ですが、今後は標準入力からの入力などにも対応する想定です。
また、mermaid のシーケンス図のパースにも対応することで、モデル以外にもサービスクラスなどの生成などにも挑戦したいと思っています。
ドキュメントがそのまま開発時のボイラープレートになるような開発者体験を目指して、今後も開発を続けていきたいです。もしよろしければ使ってみてください。
追記
RubyWeekly #663 に mergration が掲載されました。
Discussion