Seeding Like a Boss

Spread the word
Tweet about this on TwitterShare on Google+Share on FacebookPin on PinterestShare on RedditShare on TumblrEmail this to someonePrint this page

Ruby on Rails seeding

One of the most time consuming aspects of building and testing Rails applications is having valid and usable data in development. This is where the seeds.rb file comes into play. It’s a wonderful feature of Ruby on Rails that makes testing workflows and data saving, editing and manipulating easier. Seeding your Rails application gives you a place to start.

So what do we mean by seeding? It’s a ruby file that creates instances of needed models and saves them to your database, be it development, staging or even production. We run the seed using rake, like so: $ rake db:seed. This will read our seeds.rb file and run the ruby code within it.

Keeping It Simple

Let’s take a look at a very simple seed file.

  puts "Creating users..."
  User.create(first_name: "Joe", last_name: "Smo", email: "joe@smo.com", password: 'password', role: User::Roles::ADMIN, phone: "8888388228", confirmed_at: Time.now)
  User.create(first_name: "Jenn", last_name: "Blanche", email: "jenn@blanche.com", password: 'password', role: User::Roles::STANDARD, phone: "8885551238", confirmed_at: Time.now)

  puts "Creating movies..."
  Movie.create(title: "Aliens", director: "James Cameron", year: 1986, genre: "Sci Fi")
  Movie.create(title: "Sunshine", director: "Danny Boyle", year: 2007, genre: "Sci Fi")
  Movie.create(title: "Airplane!", director: "Jim Abrahams, David Zucker", year: 1980, genre: "Comedy")

Simple enough. This is assuming that our Rails application has at least two models called User and Movie. We just use the ActiveRecord method create() for each of them to create two users and three movies. We add the puts "Creating <items>..." to notify the developer the current stage of the seeding. This will also give us some indication of where any problems may have happened when we run our seed file.

Go ahead and run this to make sure it works. Open up your terminal and cd to your project. Then run the command $ rake db:seed. You should see our two feedback messages Creating users… and Creating movies…. Then, if you check your project’s database, you should see the users and movies.

What?! There could be a problem already? Yes… yes there could be. What if the database already has a user with the email of joe@smo.com or the movie Aliens? We need to make sure that if the database already has any of the entries we’ve put in our seed file, that it will just skip over them. This will also help prevent someone from duplicating records if they accidentally run the seed twice.

This is pretty simple to avoid:

  puts "Creating users..."
  if User.find_by_email("joe@smo.com").nil?
    User.create(first_name: "Joe", last_name: "Smo", email: "joe@smo.com", password: 'password', role: User::Roles::ADMIN, phone: "8888388228", confirmed_at: Time.now)
    puts "User Joe Smo created"
  else
    puts "User with email joe@smo.com already exists"
  end

  if User.find_by_email("jenn@blanche.com").nil?
    User.create(first_name: "Jenn", last_name: "Blanche", email: "jenn@blanche.com", password: 'password', role: User::Roles::STANDARD, phone: "8885551238", confirmed_at: Time.now)
    puts "User Jen Blanche created"
  else
    puts "User with email jenn@blanche.com already exists"
  end

  puts "Creating movies..."
  Movie.create(title: "Aliens", director: "James Cameron", year: 1986, genre: "Sci Fi") if Movie.find_by_title("Aliens").nil?
  Movie.create(title: "Sunshine", director: "Danny Boyle", year: 2007, genre: "Sci Fi") if Movie.find_by_title("Sunshine").nil?
  Movie.create(title: "Airplane!", director: "Jim Abrahams, David Zucker", year: 1980, genre: "Comedy") if Movie.find_by_title("Airplane!").nil?

Here we added our if statements to check for any users already existing with the emails we’re about to use. The find_by_<attribute>() method is a bit of Rails magic that makes our lives easier and is how we check that we don’t already have a user or movie with the same value for our unique attributes. We did the same with the movies but we didn’t give an else and a note that it was skipped.

Now, having not truncated the database, if you run $ rake db:seed, you will get the feedback User with email joe@smo.com already exists and User with email jenn@blanche.com already exists. Checking the database, you can also make sure that your movies have not been duplicated. Run $ rake db:migrate:reset and then $ rake db:seed again, to make sure that our seeds.rb will still create these records if they don’t already exist in the database.

The feedback from the terminal should read, Creating users…, two User <name> created and Creating movies…. Double check your database and make sure that these have been created and we’ll move on.

Creating has_many Relationships

Let’s say that we are making an application where users can log in and rate movies. We’ll want our users to have_many ratings and thus movies will also have_many ratings. This will connect users to movies via our rating system and will make our models look like this:

  class Movie < ActiveRecord::Base
    has_many :ratings, counter_cache: true
  end

  class User < ActiveRecord::Base
    has_many :ratings
  end

  class Rating < ActiveRecord::Base
    belongs_to :user
    belongs_to :movie
  end

Our seeds for the users and movies will pretty much stay the same. I’m going to add an attribute called ratings_count to go with our counter_cache to keep track of how many ratings our movies have had.

  puts "Creating users..."
  joe = User.create(first_name: "Joe", last_name: "Smo", email: "joe@smo.com", password: 'password', role: User::Roles::ADMIN, phone: "8888388228", confirmed_at: Time.now)
  jenn = User.create(first_name: "Jenn", last_name: "Blanche", email: "jenn@blanche.com", password: 'password', role: User::Roles::STANDARD, phone: "8885551238", confirmed_at: Time.now)
  bob = User.create(first_name: "Bob", last_name: "Fritz", email: "bob@fritz.com", password: 'password', role: User::Roles::STANDARD, phone: "7175550098", confirmed_at: Time.now)

  puts "Creating movies..."
  aliens = Movie.create(title: "Aliens", director: "James Cameron", year: 1986, genre: "Sci Fi", ratings_count: 0) if Movie.find_by_title("Aliens").nil?
  sunshine = Movie.create(title: "Sunshine", director: "Danny Boyle", year: 2007, genre: "Sci Fi") if Movie.find_by_title("Sunshine").nil?
  airplane = Movie.create(title: "Airplane!", director: "Jim Abrahams, David Zucker", year: 1980, genre: "Comedy") if Movie.find_by_title("Airplane!").nil?

  puts "Creating ratings..."
  puts "for Aliens:"
  aliens.ratings << Rating.new(user: joe, stars: 5) << Rating.new(user: jenn, stars: 4) << Rating.new(user: bob, stars: 5)
  
  puts "for Sunshine:"
  sunshine.ratings << Rating.new(user: joe, stars: 4) << Rating.new(user: jenn, stars: 3, ratings_count: 0) << Rating.new(user: bob, stars: 5)
  
  puts "for Airplane:"
  airplane.ratings << Rating.new(user: joe, stars: 3) << Rating.new(user: jenn, stars: 5, ratings_count: 0) << Rating.new(user: bob, stars: 4)

But we can probably clean this up a little, making it more dynamic and more DRY.

  ... omitted ...

  puts "Creating ratings..."
  [aliens, sunshine, airplane].each do |movie|
    puts "for #{movie.title}:"
    [joe, jenn, bob].each { |user| movie << Rating.new(user: user, stars: (0..5).sample) }
  end

Here I just looped through our movies and for each, have the puts "for <title>:". Next I looped through each user and pushed a new Rating with a randomized value between 0 and 5 for the stars attribute.

Prevent Specific Seeds for Production

Rails seeding is generally for development and/or staging environments, but that doesn’t mean there aren’t times where you’ll want some base data for your production environment. Say you have a help system in place for your application, and you want those help topics to be maintained, added to and removed within your application. It would make sense to have that data created by your seeds.rb initially, but you wouldn’t want your production application to seed dummy users. So let’s take a moment and add some code to prevent users from being created while we’re in production.

  unless Rails.env == 'production'
    puts "Creating users..."
    joe = User.create(first_name: "Joe", last_name: "Smo", email: "joe@smo.com", password: 'password', role: User::Roles::ADMIN, phone: "8888388228", confirmed_at: Time.now)
    jenn = User.create(first_name: "Jenn", last_name: "Blanche", email: "jenn@blanche.com", password: 'password', role: User::Roles::STANDARD, phone: "8885551238", confirmed_at: Time.now)
    bob = User.create(first_name: "Bob", last_name: "Fritz", email: "bob@fritz.com", password: 'password', role: User::Roles::STANDARD, phone: "7175550098", confirmed_at: Time.now)
  end
  
  ... omitted movies ...
  
  # and since we can't have ratings without users
  unless Rails.env == 'production'
    puts "Creating ratings..."
    [aliens, sunshine, airplane].each do |movie|
      puts "for #{movie.title}:"
      [joe, jenn, bob].each { |user| movie << Rating.new(user: user, stars: (0..5).sample) }
    end
  end

I use the env attribute of the Rails object to test against. This will return a string value of our current environment. If you stick to the Rails convention of naming your environments development, staging and production, then the above code will work for you. We just test that Rails.env is equal to production, if so, we skip over that block of code.

Skipping Rails Callbacks

A lot of times we set callbacks in our Rails models to update other related models, create needed records to go along with our model or even sending emails to notify users. Some of these examples, we may want to run with our seeds and some we may not. Most likely we don’t want our system to email out anything when we’re just seeding, so let’s write some code to prevent this from happening.

  class Rating < ActiveRecord::Base
    belongs_to :user
    belongs_to :movie

    after_create :notify_admins

    ... omitted ...

    private

    # we don't want this to be called if we're seeding our database
    def notify_admins
      AdminMailer.rating_created(self).deliver
    end
  end
  # seeds.rb
  Rating.skip_callback(:create, :after, :notify_admins)

  puts "Creating ratings..."
  [aliens, sunshine, airplane].each do |movie|
    puts "for #{movie.title}:"
    [joe, jenn, bob].each { |user| movie << Rating.new(user: user, stars: (0..5).sample) }
  end

  Rating.set_callback(:create, :after, :notify_admins)

Using the skip_callback method we can prevent rails from calling the notify_admins method after the model has been created. The first argument is the action being listened for, in our case :create, second is when we expect our method to run in relation to the action (:after) and lastly is that method that we’re preventing from being called. After we’ve created all our records, then we reset the callback.

User Input

Because you won’t need all the records in your seed all the time, I like to allow the developer to pick what records should be created or not. We’ll do this with a gem called highline. Go ahead and add it to your Gemfile, gem 'highline' and run $ bundle install. Now we’ll add some user prompts to our seed file:

  require 'highline/import'

  answer = ask("Do you wish to create Ratings for your movies? ") { |q| q.default = "no" }

  if answer.match(/^y(|es)$/)
    puts "Creating ratings..."
    [aliens, sunshine, airplane].each do |movie|
      puts "for #{movie.title}:"
      [joe, jenn, bob].each { |user| movie << Rating.new(user: user, stars: (0..5).sample) }
    end
  end

We set the return value from ask() to a variable answer, this will be whatever the user types in response to the question. We test that response for a RegExp match of y or yes. If so then we create our Ratings. Go ahead and run your $ rake db:seed with a fresh database and make sure it works.

Conclusion

There are probably all kinds of other tricks one can do to make their seeds.rb more robust. These are just a few nice ones that I have picked up along the way. With the recent addition of the highline gem, I felt it was time to put all of my tips and practices in a blog post to share with others.

Hit me up on Twitter if you have any questions or comments about this setup and definitely let me know if there are any tips that you use.

RESOURCES

Spread the word
Tweet about this on TwitterShare on Google+Share on FacebookPin on PinterestShare on RedditShare on TumblrEmail this to someonePrint this page