見出し画像

Railsのリレーション情報を抽出してER図を描く方法

## 自己紹介

こんにちは、データーサイエンティストの鄧(でん)と申します。入社してから機械学習や最適化問題など色々面白いプロジェクトをやらせて頂いていあっという間に一年がすぎました。前回のアドベントカレンダーはBigQuery上でLisp系言語を実装する話ですが、今回は技術的なドキュメント整備の話をさせて頂きたいと思います。

## 課題

トレタでは組織が拡大してコミュニケーション工数が増えつつあります、その上でドキュメント、データー周りに関してはER図などの設計図をちゃんと残しておいたほうが新人の教育や仕様書の一部として使いやすいのではとの意見がありました。

トレタのサーバーサイドで使われているMySQL(*1)にはリレーション情報をforeign keyとして登録することで整合性を担保することが可能です。一部MySQL Workbenchのようなツールはこのリレーション情報を抽出してER図を自動で生成してくれる機能もありますが、残念ながら現状トレタのRailsアプリはリレーション情報をモデル側で保持していてMySQLに書き込んでおりません。Rails ERDみたいなツールも検討してみましたがトレタのテーブルの構成には対応できませんでした。

幸い今回はRailsのモデル側ではこのような記述があるのでこれを使ってテーブルの構成を再現することに成功しました。

class Restaurant < ApplicationRecord
  belongs_to :restaurant_group
  has_one :company, through: :restaurant_group
  has_many :tables, class_name: 'RestaurantTable', dependent: :destroy, inverse_of: :restaurant
end

## アプローチ

1. モデル(Active Record)のreflect_on_all_associationsメソッドを使ってリレーションを抽出し
2. MySQL(*1)から各項目の型やメタデータを抽出(*2)し
3. 中身のデータは必要ないため小さめのCloud SQLを使ってテーブル構造とリレーションを再現しました
4. あとはMySQL Workbenchのreverse engineer機能を使って雛形を抽出し、必要に応じてER図を描くだけです

## 結果

最初はテーブル数が多すぎて画面が崩れる場合が多いと思いますが、状況に応じて必要な部分だけ描けばわかりやすいER図が描けると思います。

## 今後

今回はデータ構造とリレーションを抽出しましたが、Rails側のコメントなどにも重要なものが示されている場合もあるので時間があればコメントも自動的に抽出できるようになるといいですね。

## notes

1. 厳密にはAWS Aurora MySQL Compatible Edition
2. `mysqldump -u user -p --no-data dbname > schema.sql`

## scripts

require 'active_record'
require 'countries'

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

# Mocked Rails.
class Env
  def production?
    false
  end
end

class Rails
  def self.root
    Pathname.new('/')
  end

  def self.env
    Env.new
  end
end

# Local Dependencies.
require '../../app/models/concerns/model_observable.rb'
require '../../app/models/concerns/phone_call.rb'
require '../../app/models/concerns/smoking.rb'

Dir['../../app/models/*.rb'].map { |file| require file }

constants = Module.constants.select do |constant_name|
  constant = eval constant_name.to_s
  if not constant.nil? and constant.is_a? Class and constant.superclass == ApplicationRecord
    constant
  end
end

# Extract metadata via reflection.
metadata = constants.map do |name|
  model = eval(name.to_s)
  model.reflect_on_all_associations.select{ |x| !x.options.key? :through }.map do |x|
    {
      :from => model.table_name,
      :label => x.macro,
      :to => x.name,
      :foreign_key => x.foreign_key,
      :options => x.options
    }
  end
end

puts metadata.flatten.to_json



この記事が気に入ったらサポートをしてみませんか?