Environment Specific Rails Seeds.rb
The default Rails seeds.rb setup works great for smaller projects but what happens when?
- The file grows very large due to the number of models you need to seed.
- You have to create order dependant records.
- 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:
Create the file:
touch db/seeds/helpers.rbAdd 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 endRequire 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.