Environment Specific Rails Seeds.rb

The default Rails seeds.rb setup works great for smaller projects but what happens when?

  1. The file grows very large due to the number of models you need to seed.
  2. You have to create order dependant records.
  3. You need different data for each environment.

It becomes a hard to maintain string of if/else statements and you end up with something like this:

# db/seeds.rb
admin_role = Role.find_or_create_by(name: 'admin')
contributor_role = Role.find_or_create_by(name: 'contributor')

photo_category = Category.find_or_create_by(name: 'photo')
video_category = Category.find_or_create_by(name: 'video')

if Rails.env.development?
  100.times do |i|
    User.create!(email: "user_#{i}@example.com", role: contributor_role)
    Photo.create!(title:  "Photo #{i}", category: photo_category)
    Video.create!(title:  "Video #{i}", category: video_category)
    Comment.create!(text:  "cool photo", user_id: User.pluck(:id).sample, target_id: Photo.pluck(:id).sample)
    Comment.create!(text:  "cool video", user_id: User.pluck(:id).sample, target_id: Video.pluck(:id).sample)
  end
end

if Rails.env.development? || Rails.env.staging?
  User.create!(email: "[email protected]", admin: true, role: admin_role)
end

So how do you make seeds.rb easier to maintain and improve readability?
Try out this strategy my team has been using for the past couple years...

Create Seeds Folder

First, create the folder structure to hold environment specific seed files. db/seeds/shared will hold seeds that are created for all environments.

mkdir db/seeds
mkdir db/seeds/shared && touch db/seeds/shared/.keep
mkdir db/seeds/production && touch db/seeds/production/.keep
mkdir db/seeds/development && touch db/seeds/development/.keep
mkdir db/seeds/test && touch db/seeds/test/.keep
mkdir db/seeds/staging && touch db/seeds/staging/.keep

Update Seeds.rb

Next, tell seeds.rb to load shared seeds and seeds for the current environment.
Notice the .sort when loading the files this is so you can enforce a seed order and fix problem #2.
Also its useful to add an output statement to track which file is currently running.

Dir[Rails.root.join("db/seeds/shared/*.rb")].sort.each do |file|
  puts "Processing #{file.split('/').last}"
  require file
end

Dir[Rails.root.join("db/seeds/#{Rails.env}/*.rb")].sort.each do |file|
  puts "Processing #{file.split('/').last}"
  require file
end

Create Seed Files

Next, split your current seeds.rb into environment specific or shared files.
Note the filenames: 0000_model.rb and 0010_model.rb leave a large gap between filenames
so you can inject new models into the order without renaming everything.

Shared

# db/seeds/shared/0000_roles.rb
puts '  Creating Roles...'
Role.find_or_create_by(name: 'admin')
Role.find_or_create_by(name: 'contributor')

# db/seeds/shared/0010_categories.rb
puts '  Creating Categories...'
Category.find_or_create_by(name: 'photo')
Category.find_or_create_by(name: 'video')

Development

# db/seeds/development/0000_users.rb
puts '  Creating Super User...'
User.create!(email: "[email protected]", admin: true, role: Role.find_by(name: 'admin'))

puts '  Creating Contributor Users...'
100.times do |i|
  User.create!(email: "user_#{i}@example.com", role: Role.find_by(name: 'contributor'))
end

# db/seeds/development/0010_photos.rb
puts '  Creating Photos...'
100.times do |i|
  Photo.create!(title:  "Photo #{i}", category: Category.find_by(name: 'photo'))
end

# db/seeds/development/0020_videos.rb
puts '  Creating Videos...'
100.times do |i|
  Video.create!(title:  "Video #{i}", category: Category.find_by(name: 'video'))
end

# db/seeds/development/0030_comments.rb
puts '  Creating Photo Comments...'
100.times do
  Comment.create!(text:  "cool photo", user_id: User.pluck(:id).sample, target_id: Photo.pluck(:id).sample)
end

puts '  Creating Video Comments...'
100.times do
  Comment.create!(text:  "cool video", user_id: User.pluck(:id).sample, target_id: Video.pluck(:id).sample)
end

Staging

# db/seeds/staging/0000_users.rb
User.create!(email: "[email protected]", admin: true, role: Role.find_by(name: 'admin'))

Run db:seed

Last, run db:seed as you usual and see the magic...

> RAILS_ENV=development bin/rails db:seed
Processing 0000_roles.rb
  Creating Roles...
Processing 0010_categories.rb
  Creating Categories...
Processing 0000_users.rb
  Creating Super User...
  Creating Contributor Users...
Processing 0010_photos.rb
  Creating Photos...
Processing 0020_videos.rb
  Creating Video...
Processing 0030_comments.rb
  Creating Photo Comments...
  Creating Video Comments...

Additional Improvements

Idempotency

Ensure you use find_or_create_by to keep seeds idempotent.This enables devs to run db:seed when new seeds are added without having to first destroy their existing database.

Helpers

Setup helper functions you can share between seed files:

  1. Create the file:

    touch db/seeds/helpers.rb
    
  2. Add some helper functions:

    # db/seeds/helpers.rb
    module Seeds
    module Helpers
    module_function
    
    def admin_role
      @admin_role ||= Role.find_by(name: "admin")
    end
    
    def editor_role
      @editor_role ||= Role.find_by(name: "editor")
    end
    end
    end
    
  3. Require and use the helpers:

# db/seeds/development/0000_users.rb
require_relative '../helpers'

puts '  Creating Super User...'
User.create!(email: "[email protected]",
             admin: true,
             role: Seeds::Helpers.admin_role)

Final Thoughts

I hope you find this simple strategy useful and have an easier time managing your seeds.rb. It has worked great for my team over the past couple years and is an improvement over using a single file to support everything.