在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来判断