Benito Serna Tips and tools for Ruby on Rails developers

Active Record Playground Runner Introduction

August 8, 2022

Have you ever wanted to just create an active record example to someone in your team without thinking in the database setup?

Or maybe send two different models designs with some examples to see the difference, but the setup what just to difficult?

Or like me, create a bunch of examples to teach an Active Record concept?

If you have had one of this problems, maybe this tool can help you.

A tool to run Active Record examples

I want to introduce a new tool that I am working on.

It will help you play with Active Record and Postgres, without the thinking in the database setup.

You will be able to declare the schema, models, seeds and examples in just one file, like this:

schema do
  create_table :posts do |t|
    t.string :title
    t.text :body
  end

  create_table :comments do |t|
    t.text :body
    t.integer :post_id
  end

  add_index :comments, :post_id
end

models do
  class Post < ActiveRecord::Base
    has_many :comments
  end

  class Comment < ActiveRecord::Base
    belongs_to :post
  end
end

seeds do
  posts = create_list(Post, count: 10) do
    {
      title: FFaker::CheesyLingo.title,
      body: FFaker::CheesyLingo.paragraph
    }
  end

  create_list_for_each_record(Comment, records: posts, count: 20) do |post|
    {
      post_id: post.id,
      body: FFaker::CheesyLingo.sentence
    }
  end
end

example "first" do
  Post.includes(:comments).limit(5).map do |post|
    puts post.comments.count
  end
end

example "second" do
  Post.includes(:comments).limit(5).map do |post|
    puts post.comments.size
  end
end

And the run it with:

bundle exec run_playground file_name.rb

And it will:

And print an output like:

Setup
-----
-- create_table(:posts)
   -> 0.3001s
-- create_table(:comments)
   -> 0.0092s
-- add_index(:comments, :post_id)
   -> 0.0041s


Example: first
--------------
D, [2022-08-08T18:13:46.118881 #89680] DEBUG -- :   Post Load (4.5ms)  SELECT "posts".* FROM "posts" LIMIT $1  [["LIMIT", 5]]
D, [2022-08-08T18:13:46.143351 #89680] DEBUG -- :   Comment Load (1.0ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN ($1, $2, $3, $4, $5)  [["post_id", 1], ["post_id", 2], ["post_id", 3], ["post_id", 4], ["post_id", 5]]
D, [2022-08-08T18:13:46.156379 #89680] DEBUG -- :   Comment Count (3.8ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 1]]
20
D, [2022-08-08T18:13:46.158504 #89680] DEBUG -- :   Comment Count (1.0ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 2]]
20
D, [2022-08-08T18:13:46.161012 #89680] DEBUG -- :   Comment Count (1.3ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 3]]
20
D, [2022-08-08T18:13:46.163467 #89680] DEBUG -- :   Comment Count (0.5ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 4]]
20
D, [2022-08-08T18:13:46.165299 #89680] DEBUG -- :   Comment Count (1.0ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 5]]
20


Example: second
---------------
D, [2022-08-08T18:13:46.166206 #89680] DEBUG -- :   Post Load (0.3ms)  SELECT "posts".* FROM "posts" LIMIT $1  [["LIMIT", 5]]
D, [2022-08-08T18:13:46.167795 #89680] DEBUG -- :   Comment Load (0.7ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN ($1, $2, $3, $4, $5)  [["post_id", 1], ["post_id", 2], ["post_id", 3], ["post_id", 4], ["post_id", 5]]
20
20
20
20
20

Why this tool?

I prepared a lot of examples for the ebook Avoid n+1 queries on rails using the first version of my Active Record Playground. A template to help in the setup of an environment with Active Record.

With the old template it was is hard to write a lot of examples with the same database schema, and it was hard to setup a project for each schema.

With this tool is much more easier to create examples with different schemas because you don’t need to setup a whole project directory for each schema.

It makes it easier to create more and more diverse examples. And also helping the people that see the examples to focus just on the example code instead of the project setup.

The code repository

You can find the code on: https://github.com/bhserna/active_record_playground_runner

Installation

For now, the gem is only on github, and if you want to install it you will need to add this to your Gemfile:

gem 'active_record_playground_runner', "~> 0.1.0", github: "bhserna/active_record_playground_runner", branch: "main"

How to use it

Schema

In the schema you will be able to define a schema using the methods that rails uses to define the schema in a regular rails application.

Models

Here you can define the records or plain old ruby objects that you will need on the seeds.

Seeds

You can use your models to create the state you need for your examples.

You will be able to use the FFaker gem. It will be already required.

Seeds Helpers

In the seeds from the example, there are two methods that are part of the gem, create_list and create_list_for_each_record.

seeds do
  posts = create_list(Post, count: 10) do
    {
      title: FFaker::CheesyLingo.title,
      body: FFaker::CheesyLingo.paragraph
    }
  end

  create_list_for_each_record(Comment, records: posts, count: 20) do |post|
    {
      post_id: post.id,
      body: FFaker::CheesyLingo.sentence
    }
  end
end

create_list receives a record class, a count keyword and a block. The block will be executed to create each record the number of times specified by count.

create_list_for_each_record also expects a list of records and for each record it will run the block count times.

Both methods will first build data as an array of hashes, and then use insert_all to create the records. Be aware that if you use this methods the callbacks will not be fired.

Examples

You can define as many examples as you want, you can pass a name if you need it:

example do
  Post.includes(:comments).limit(5).map do |post|
    puts post.comments.count
  end
end

example "My example" do
  Post.includes(:comments).limit(5).map do |post|
    puts post.comments.size
  end
end

If you need to define changes to the model classes, you will be able to do it inside the example:

example "third" do
  class Post < ActiveRecord::Base
    has_one :latest_comment, -> { order(id: :desc) }, class_name: "Comment"
  end

  Post.preload(:latest_comment).limit(5).map do |post|
    puts post.latest_comment
  end
end

Run a standalone playground

If you want to run a playground without the need to install the gem apart or with a Gemfile, you can use bundler/inline and run the playground like this:

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem 'active_record_playground_runner', "~> 0.1.0", github: "bhserna/active_record_playground_runner", branch: "main"
end

require "active_record_playground_runner"

ActiveRecordPlaygroundRunner::Playground.new("My playground name") do
  schema do
    create_table :posts do |t|
      t.string :title
      t.text :body
    end

    create_table :comments do |t|
      t.text :body
      t.integer :post_id
    end

    add_index :comments, :post_id
  end

  models do
    class Post < ActiveRecord::Base
      has_many :comments
    end

    class Comment < ActiveRecord::Base
      belongs_to :post
    end
  end

  seeds do
    posts = create_list(Post, count: 10) do
      {
        title: FFaker::CheesyLingo.title,
        body: FFaker::CheesyLingo.paragraph
      }
    end

    create_list_for_each_record(Comment, records: posts, count: 20) do |post|
      {
        post_id: post.id,
        body: FFaker::CheesyLingo.sentence
      }
    end
  end

  example "first" do
    Post.includes(:comments).limit(5).map do |post|
      puts post.comments.count
    end
  end

  example "second" do
    Post.includes(:comments).limit(5).map do |post|
      puts post.comments.size
    end
  end
end.setup.run.destroy

And then run the file like any other ruby file:

ruby my_playground.rb

Related articles

Download a free ebook to learn the basics of n+1 queries on Rails basics

Learn just enough fundamentals to be fluent preloading associations with ActiveRecord, and start helping your team to avoid n+1 queries on production.