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
この記事が気に入ったらサポートをしてみませんか?