🧜‍♀️

Mermaid の ER 図から Rails のマイグレーションファイルを生成する

2023/07/19に公開

対象とする読者

  • Rails 開発の設計で Mermaid を使っている人
  • ドキュメントとモデルを同期したい人
  • Markdown パーサーを開発してみたい人

はじめに

Rails 開発で新機能を開発する際、新たに追加するモデルや、その関連付けについての議論が重要だと感じる方が多いのではないでしょうか。
筆者が所属するチームでも、モデリングの議論は新規開発の初期の工程の多くを占めます。

Mermaid で描いた ER 図の例

オンラインでのミーティングが多い昨今、モデリングの議論の場面では、ドキュメンテーションツールに書いた ER 図を叩き台に議論する機会が多いです。
Notion を使うことが多いため、最近対応した Mermaid 記法で ER 図を書くことが特に多くなっています。

この Mermaid 記法は GitHub の PullRequest でも図の出力に対応していることから、最近使用頻度が増えてきています。

https://dev.classmethod.jp/articles/mermaid-markdown-is-supported-in-notion/

https://dev.classmethod.jp/articles/github-mermaid-markdown-cntrol/

Mermaid の ER 図で議論するメリット・デメリット

Mermaid は Notion でも GitHub でも表示機能が強化されているため、ER 図を使ってモデルの関連付けを意識するのに適しています。
ただ、ER 図として Mermaid 記法で書いた内容を Rails のマイグレーションファイルに書き起こすのが手間に感じていました。

モデリングの議論で合意をとった内容は、なるべく早くマイグレーションしてしまいたいものです。また、モデルが複数個ある場合は、マイグレーションファイルに書き起こした際にリレーションや制約を間違える可能性もあります。

Mergration

そこで、 Mermaid の ER 図から Rails のマイグレーションファイルを生成する gem mergration を作りました。

https://github.com/38tter/mergration

mergration という名称は mermaidmigration をミックスしたもので、rails generate コマンドでマイグレーションファイルを生成するのと同じ感覚で、mermaid を書いたファイルからマイグレーションファイルを生成できます。

コマンド実行のデモ

仕組み

mergration は大きく二つの処理を実行しています。

  1. Mermaid 記法のパース
  2. マイグレーションファイルの生成

以下、順にご説明したいと思います。

1. Mermaid 記法のパース

まず必要なのが、mermaid 記法のパース処理です。
そのために markdown パーサーである kramdown を拡張し、mermaid をパースできる gem を作りました(現状は ER 図のみパースできます)。

この処理によって、パース結果が抽象構文木と呼ばれる構造のハッシュとして得られます。
こちらの実装の詳細については別途記事にしたいと思っています。

https://github.com/38tter/kramdown-mermaid

具体例として、以下のような 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 が参考になりました。

https://github.com/paper-trail-gem/paper_trail

実装の大きな流れとしては以下の通りです。

  1. 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
  1. ::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
  1. 上記のパース結果のハッシュをオプションとしてテンプレートメソッドに渡す

おわりに

現在は .md ファイルに記述した mermaid の ER 図のみが対象ですが、今後は標準入力からの入力などにも対応する想定です。
また、mermaid のシーケンス図のパースにも対応することで、モデル以外にもサービスクラスなどの生成などにも挑戦したいと思っています。

ドキュメントがそのまま開発時のボイラープレートになるような開発者体験を目指して、今後も開発を続けていきたいです。もしよろしければ使ってみてください。

追記

RubyWeekly #663 に mergration が掲載されました。

https://rubyweekly.com/issues/663

Discussion