The Seed Controversy

Published 10th April 2016 at 07:39pm UTC (Last Updated 22nd March 2020 at 08:59pm UTC)

In the Rails community there is some controversy over where to put seed data. Best practice advises that these are placed in the db/seeds.rb file within a Rails application, which is great if all you want to do is run rake db:seed once throughout the entire life of your application or are ok with deleting everything in your database should you ever have to run the rake db:seed command again.

So far, so terrible.

Many Rubyists have opted to place seed data in their migration files so as to avoid this issue. This is however, not considered to be 100% safe and is fraught with its own problems. Yes, you could just create a migration file, with a down as well as an up method...

OR OR OR,

you could create a deseed environment in the db/seeds.rb file. It's an interesting compromise between the two sides of the debate - essentially add some variant of an if/else statement like so:

if ENV['deseed'] 
  #Insert "down" method that reverses the
  #changes made to the database
else
  #The "up" method that seeds the data you want
end

With a block like that added to the db/seeds.rb file, you can seed data the old fashioned way...

rake db:seed

..and you can also remove seed data with the following command:

rake db:seed deseed=true

Awesome, right? So how exactly would we reverse a create action or even a find_by_or_create action? You might think that simply using the find_by and destroy method would be the way to do it. However, this won't work if your database already contains duplicates of the rows you'd like to delete as the find method is an alias for the detect method which will essentially stop looking once it's found the first row that matches the condition in the block.

Now that's kind of rubbish.  

Instead, try using the where method followed by destroy_all.

if ENV["deseed"]
  User.where(name: "Jimmy",
    instrument: "vocals",
    admin: true).destroy_all
  User.where(name: "Lindsey",
    instrument: "bass",
    admin: false).destroy_all
else
  User.find_or_create_by(name: "Jimmy",
    instrument: "vocals", admin: true)
  User.find_or_create_by(name: "Lindsey",
    instrument: "bass", admin: false)
end

Note if using where you may want to avoid matching float values as this may produce unexpected results in the underlying SQL. Make sure any floating point digits that you intend to match are stored in the database as a decimal instead.

#example migration file
class CreateItems < ActiveRecord::Migration
  def change
    t.string :name
    t.decimal :price, precision: 10,
      scale: 2
  end
end

 

#Example create method for the seed file
Item.find_or_create_by(name: "quill",
  price: BigDeimal.new("5.99"))

Values stored in this way will appear as integers in the Rails Console but the SQL database you use should store the exact values correctly. The where method will work correctly in the underlying SQL.

 

Acknowledgements:

Gediminas Zubovicius for inadvertently giving me the idea for this blog at the London Ruby Hack Night Cort3z's SO answer Ryan Bigg's answer regarding the find method in another Stack Overflow question. I honestly didn't know that find did that so this was quite helpful.

  I hope that was informative for at least some of you. This is the first proper blog post I've written on programming so let me know what you think of it in the comments section below!

Post a comment