[Update: 10/15/07 - incorporated changes by David Vrensk (and a few more from me). Now it merges in associations into the arc, and also deals with inheritance (e.g. STI).]
While googling for articles on Rails associations, I happened upon
this gem of a script by Matt Biddulph. I loved it so much I made it a rake task! Once you install GraphViz like this:
sudo port install graphviz
and put dot.rake in your lib/tasks directory, then running this:
rake dot
produces diagrams like this:

And you can also import the DOT source into OmniGraffle for further editing, like this:
open -a "OmniGraffle" model.dot
Here’s the source for dot.rake:
# dot.rake
# Creates a DOT format file showing the model objects and their associations
# Authors:
# Matt Biddulph - http://www.hackdiary.com/archives/000093.html
# Alex Chaffee - http://www.pivotalblabs.com/articles/2007/09/29/dot-rake
# David Vrensk - david@vrensk.com
# Usage:
# rake dot
# To open in OmniGraffle, run
# open -a 'OmniGraffle' model.dot
# or
# open -a 'OmniGraffle Professional' model.dot
desc "Generate a DOT diagram of the ActiveRecord model objects in 'model.dot'"
task :dot => :environment do
Dir.glob("app/models/*rb") { |f|
require f
}
File.open("model.dot", "w") do |out|
out.puts "digraph x {"
#out.puts "tnode [fontname=Helvetica,fontcolor=blue]"
out.puts "tnode [fontname=Helvetica]"
out.puts "tedge [fontname=Helvetica,fontsize=10]"
Dir.glob("app/models/*rb") { |f|
f.match(//([a-z_]+).rb/)
classname = $1.camelize
klass = Kernel.const_get classname
if (klass.class != Module) && (klass.ancestors.include? ActiveRecord::Base)
if klass.include? ActiveRecord::Acts::List::InstanceMethods
scope = klass.new.scope_condition.sub(/(_id?) .*/,'').camelize
out.puts "t#{classname} [label="#{classname}n(list in #{scope})"]"
elsif klass.superclass != ActiveRecord::Base
out.puts "t#{classname} -> #{klass.superclass.name} [arrowhead=empty]"
else
out.puts "t#{classname}"
end
klass.reflect_on_all_associations.select { |a| a.macro.to_s.starts_with? 'has_' }.each do |a|
target = a.name.to_s.camelize.singularize
if a.klass.name != target
target = a.klass.name
label = ",label="as #{a.name}""
else
label =""
end
case a.macro.to_s
when 'has_many'
out.puts "t#{classname} -> #{target} [arrowhead=crow#{label}]"
when 'has_and_belongs_to_many'
out.puts "t#{classname} -> #{target} [arrowhead=crow,arrowtail=crow#{label}]" if classname < target
when 'has_one'
out.puts "t#{classname} -> #{target} [arrowhead=diamond#{label}]"
else
$stderr.puts "No support for #{a.macro.to_s} in #{classname}"
end
end
end
}
out.puts "}"
end
system "dot -Tpng model.dot -o model.png"
system "/Applications/Graphviz.app/Contents/MacOS/dot -Tpng model.dot -o model.png" unless $?.success?
puts "Could not write model.png. Please install graphviz (http://www.graphviz.org)." unless $?.success?
end
Put that in a file called “dot.rake” and put it in your lib/tasks directory and Dot’s your uncle. Aunt. Whatever…
Any suggestions? Should I be writing the output file to a subdirectory, like maybe db, instead?
(BTW, it looks like OmniGraffle doesn’t support the font style features of DOT, so all the nodes are 12-point black on import :-( .)
very nice. but I’m afraid that for bigger applications the graph would be too big to comprehend. an option to only generate relations starting from chosen model class and traversing them only to given depth would be even nicer :)
December 12, 2007 at 11:50 pm
Pixelglow has developed a very nice GUI front-end for the Macintosh port of graphviz, you might want to check it out. After generating the dot-file as described, you can easily decorate it with different fonts, colours etcetera with the GUI tool.
It won Best Mac OS X Open Source Product in the 2004 Apple Design Awards.
December 12, 2007 at 11:50 pm
this would be great as a sake task
December 12, 2007 at 11:50 pm
Szeryf, Mike — Great suggestions! If either of you wants to make a patch, I’d be happy to incorporate it. Otherwise you’ll have to wait for my inspiration to strike again…
December 12, 2007 at 11:50 pm
Here you are! A nice rake task for your snippet:
http://pastie.caboo.se/103671
Greetings!
December 12, 2007 at 11:50 pm
mmmm i have just realized your code is now a tasks…. weird :) Anyway, i spent only a couple of minutes writing the tasks so no problem :)
December 12, 2007 at 11:50 pm
Emmanuel – yeah, I had the same idea… :-)
December 12, 2007 at 11:50 pm
This is wonderful, and something I have been looking for! I have modified it a bit:
At the moment, the rendition of the edge end points is different in GraphViz and OmniGraffle. I might look into that later. If I do, I will get a proper blog, I promise.
Here’s the code: http://pastie.textmate.org/106872
December 12, 2007 at 11:50 pm
Wow, thanks to David Vrensk! This is fun. I’ve pulled in your changes, fixed a bug (now it works whether you installed GraphViz DOT via “port install” or via the Mac application), added support for inheritance (so intermediate classes between the model class and ActiveRecord::Base are displayed, and pointed to via UML-style empty arrowheads). David, can you replace your pastie with mine?
December 12, 2007 at 11:50 pm
Thanks, glad you liked it! Unfortunately I can’t find a way to replace the pastie. I could do “Reply” to it, but it didn’t create a link or anything. I can’t find enough about the API to see if there is a way to do it.
Hopefully more people read the post than the comments, and I don’t think the pastie-link is in heavy circulation yet.
Now I’ll go try your changes!
December 12, 2007 at 11:50 pm
This looks great!
Unfortunately, I just started using hasmanypolymorphs and that breaks it. I get this msg:
No support for hasmanypolymorphs in Circle
where Circle is the obj that has the hasmanypolymorphs.
:-(
December 12, 2007 at 11:50 pm
It is very nice for individual entity like models and controllers etc.
But Unfortunately, dot.rake is not working for rails 2.1.0
error:
rake aborted!
uninitialized constant ActiveRecord::Acts
Regards,
Girish
September 14, 2008 at 3:09 am