See more posts

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:

zsh
$ rails generate task export models

The resulting file should look something like this:

ruby
# 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.

ruby
# 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:

zsh
$ 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.

ruby
# 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
ruby
# 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.

ruby
# 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:

ruby
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:

ruby
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
# ...

Thanks for reading! See more posts Leave feedback