How to export Active Record models
If you've ever switched testing libraries, to something like rspec, or factory_bot, a major slowdown is generating all of the associated files. Similarly, if you're using a javascript framework it may be useful to provide a schema to the frontend, or generate javascript test data. Luckily ActiveRecord
provides a robust api for introspection.
I'll walk you through how I created a simple rake task that can be used to export your ActiveRecord models.
Create a new rake task
To create a new rake task, run the rails task generator:
$ rails generate task export models
The resulting file should look something like this:
# lib/tasks/export.rake
namespace :export do
desc "Export models"
task models: :environment do
# ..
end
end
Getting a list of models
The next step is getting a list of models. This is where ruby really shines. Because everything in ruby is an object, we can do things like Model.methods - Object.methods
to inspect the available methods. Similarly we can list class descendants with Object. descendants
. Models in rails inherit from ActiveRecord::Base
, or ApplicationRecord
in Rails versions > 5. With this knowledge it's as simple as calling ApplicationRecord.descendants
One caveat however is that models in rails are lazy loaded, the classes won't be available immediately in a rake task invocation. First we need to eager load our models. Let's try it out.
# lib/tasks/export.rake
namespace :export do
desc "Export models"
task models: :environment do
Rails.application.eager_load!
ApplicationRecord.descendants.each do |model_class|
puts model_class.to_s
end
end
end
A very simple rails app may output the following:
$ rake export:models
User
Post
Comment
If you want to export a diagram of your entity relationships, check out this blogpost: Creating Entity Relationship diagrams with Sinatra/ActiveRecord
Listing columns for models
Looking at ActiveRecord::Base
's methods we can see #columns
listed. This provides an array of column objects with the class of ActiveRecord::ConnectionAdapters::Column
.
We can use the #name
and #type
methods to describe our model's columns.
# lib/tasks/export.rake
namespace :export do
desc "Export models"
task models: :environment do
Rails.application.eager_load!
ApplicationRecord.descendants.map do |m|
cols = m.columns.map { |c| [c.name, c.type] }
end
end
end
Listing model relations
Columns are all well and good, but what about model relations? Active record uses a few different classes to describe these. We can get those relations by looking at the #reflections
method which returns a hash of Attribute => ActiveRecord::Reflection
. Now we can then use the #macro
method to describe the relation.
These macros may be one of the following:
:has_many
:belongs_to
:has_one
# lib/tasks/export.rake
namespace :export do
desc "Export models"
task models: :environment do
Rails.application.eager_load!
ApplicationRecord.descendants.map do |m|
cols = m.columns
.map { |c| [c.name, c.type] }
relations = m.reflections
.map { |attr, ref| [attr, ref.macro] }
end
end
end
Exporting as json
Now that we've gathered the details, we can export the whole thing as JSON.
# lib/tasks/export.rake
namespace :export do
desc "Export models"
task models: :environment do
Rails.application.eager_load!
models = ApplicationRecord.descendants.map do |m|
cols = m.columns
.map { |c| [c.name, c.type] }
.to_h
relations = m.reflections
.map { |attr, ref| [attr, ref.macro] }
.to_h
{
name: m.to_s,
columns: cols,
relations: relations
}
end
puts JSON.pretty_generate(models)
end
end
The possibilities are limitless
With this level of meta programing we can do all sorts of things. I've listed a few examples below.
Export factory_bot factories:
namespace :export do
desc "Generate factories"
task factories: :environment do
Rails.application.eager_load!
ApplicationRecord.descendants.each do |m|
foreign_keys = m.reflections.values.map(&:foreign_key)
cols = m.columns
.reject { |c| c.name == m.primary_key }
.reject { |c| foreign_keys.include? c.name }
.map { |c| "#{c.name}:#{c.type}" }
.join(' ')
relations = m.reflections
.map { |col, ref| "#{col}:#{ref.macro}" }
.join(' ')
cmd = "bundle exec rails g factory_bot:model #{m.to_s} #{cols} #{relations}"
puts cmd
system cmd
end
end
end
Use ERB to generate corresponding Javascript classes:
require 'erb'
# ...
template = <<~JAVASCRIPT
class #{m.to_s} {
# .. awesome code here
}
JAVASCRIPT
File.open("app/assets/javascript/models/#{m.to_s.underscore}.js", 'w') do |file|
file.write ERB.new(template).result(binding)
end
# ...