rails-erd如何生成association图
在rails-erd的那些rake file中,会发现一个如下的,它就是rake erd所调的任务
namespace :erd do
task :check_dependencies do
# ...
end
task :options do
(RailsERD.options.keys.map(&:to_s) & ENV.keys).each do |option|
RailsERD.options[option.to_sym] = case ENV[option]
# ...
end
end
end
task :load_models do
Rake::Task[:environment].invoke
Rails.application.eager_load!
# ...
end
task :generate => [:check_dependencies, :options, :load_models] do
file = RailsERD::Diagram::Graphviz.create
end
end
desc "Generate an Entity-Relationship Diagram based on your models"
task :erd => "erd:generate"
主要来看,就是load_models,然后generate
RailsERD::Diagram::Graphviz.create会从ActiveRecord::Base.descendants获取所有model,然后交给Diagram去组织
module RailsERD
class Diagram
class Graphviz < Diagram
module RailsERD
class Diagram
class << self
def create(options = {})
new(Domain.generate(options), options).create
end
module RailsERD
class Domain
class << self
def generate(options = {})
new ActiveRecord::Base.descendants, options
end
Diagram在new时,会获得包含所有model的一个domain对象,以及options
class Diagram
def initialize(domain, options = {})
@domain, @options = domain, RailsERD.options.merge(options)
end
在rake时什么参数都不带的话,options如下
irb(main):014:0> pp RailsERD.options
{:attributes=>:content,
:disconnected=>true,
:filename=>"erd",
:filetype=>:pdf,
:indirect=>true,
:inheritance=>false,
:markup=>true,
:notation=>:simple,
:orientation=>:horizontal,
:polymorphism=>false,
:sort=>true,
:warn=>true,
:title=>true,
:exclude=>nil,
:only=>nil,
:only_recursion_depth=>nil,
:prepend_primary=>false,
:cluster=>false}
new之后,便是create了
class Diagram
def create
generate
save
end
def generate
instance_eval(&callbacks[:setup])
if options.only_recursion_depth.present?
depth = options.only_recursion_depth.to_i
options[:only].dup.each do |class_name|
options[:only]+= recurse_into_relationships(@domain.entity_by_name(class_name), depth)
end
options[:only].uniq!
end
filtered_entities.each do |entity|
instance_exec entity, filtered_attributes(entity), &callbacks[:each_entity]
end
filtered_specializations.each do |specialization|
instance_exec specialization, &callbacks[:each_specialization]
end
filtered_relationships.each do |relationship|
instance_exec relationship, &callbacks[:each_relationship]
end
end
def save
instance_eval(&callbacks[:save])
end
这里的callbacks,都是用类方法来定义的
class Diagram
class << self
protected
def setup(&block)
callbacks[:setup] = block
end
def each_entity(&block)
callbacks[:each_entity] = block
end
def each_relationship(&block)
callbacks[:each_relationship] = block
end
def each_specialization(&block)
callbacks[:each_specialization] = block
end
def save(&block)
callbacks[:save] = block
end
private
def callbacks
@callbacks ||= Hash.new { proc {} }
end
关于它们的使用方法,注释里有简单演示了一下
require "rails_erd/diagram"
class YumlDiagram < RailsERD::Diagram
setup { @edges = [] }
each_relationship do |relationship|
return if relationship.indirect?
arrow = case
when relationship.one_to_one? then "1-1>"
when relationship.one_to_many? then "1-*>"
when relationship.many_to_many? then "*-*>"
end
@edges << "[#{relationship.source}] #{arrow} [#{relationship.destination}]"
end
save { @edges * "\n" }
end
YumlDiagram.create
#=> "[Rubygem] 1-*> [Ownership]
# [Rubygem] 1-*> [Subscription]
# [Rubygem] 1-*> [Version]
# [Rubygem] 1-1> [Linkset]
# [Rubygem] 1-*> [Dependency]
# [Version] 1-*> [Dependency]
# [User] 1-*> [Ownership]
# [User] 1-*> [Subscription]
# [User] 1-*> [WebHook]"
再回到generate中。这里的relationship是这样来的:
class Diagram
def generate
# ...
filtered_relationships.each do |relationship|
instance_exec relationship, &callbacks[:each_relationship]
end
end
def filtered_relationships
@domain.relationships.reject { |relationship|
!options.indirect && relationship.indirect?
}
end
# ...
class Domain
def relationships
@relationships ||= Relationship.from_associations(self, associations)
end
def associations
@associations ||= models.collect(&:reflect_on_all_associations).flatten.select { |assoc| check_association_validity(assoc) }
end
即是,通过每个model的reflect_on_all_associations来获得的。而reflect_on_all_associations则是用has_xxx、belongs_to来加入的
而绘图时用到的one_to_one?等关系判断,以及source、target,则是这样得到的
module RailsERD
class Domain
class Relationship
class << self
def association_owner(association)
association.options[:as] ? association.options[:as].to_s.classify : association.active_record.name
end
def association_target(association)
association.options[:polymorphic] ? association.class_name : association.klass.name
end
end
attr_reader :source
attr_reader :destination
delegate :one_to_one?, :one_to_many?, :many_to_many?, :source_optional?,
:destination_optional?, :to => :cardinality
def initialize(domain, associations) # @private :nodoc:
@domain = domain
@reverse_associations, @forward_associations = partition_associations(associations)
assoc = @forward_associations.first || @reverse_associations.first
@source = @domain.entity_by_name(self.class.send(:association_owner, assoc))
@destination = @domain.entity_by_name(self.class.send(:association_target, assoc))
@source, @destination = @destination, @source if assoc.belongs_to?
end
# ...
module RailsERD
class Domain
class Relationship
class Cardinality
extend Inspectable
inspection_attributes :source_range, :destination_range
N = Infinity = 1.0/0 # And beyond.
CLASSES = {
[1, 1] => :one_to_one,
[1, N] => :one_to_many,
[N, 1] => :many_to_one,
[N, N] => :many_to_many
}
CLASSES.each do |cardinality_class, name|
class_eval <<-RUBY
def #{name}?
cardinality_class == #{cardinality_class.inspect}
end
RUBY
end
# Returns an array with the cardinality classes for the source and
# destination of this cardinality. Possible return values are:
# [1, 1], [1, N], [N, N], and (in theory)
# [N, 1].
def cardinality_class
[source_cardinality_class, destination_cardinality_class]
end
def source_cardinality_class
source_range.last == 1 ? 1 : N
end
# The cardinality class of the destination (right side). Either +1+ or +Infinity+.
def destination_cardinality_class
destination_range.last == 1 ? 1 : N
end
总的来说,可以用到的方法有:model的reflect_on_all_associations获取ActiveRecord::Reflection::XXXReflection,然后从这些Reflection获取source和target,而数量对应关系,则用rails-erd的工具类Cardinality来判断