1

I'm still new in Ruby / Rails and I need your help to be sure my seed is good before running my command rails db:seed

Here my seed:

require 'faker'

puts "Creating 10 fake user heros..."
10.times do
  user = User.create!(
    username: Faker::DcComics.hero,
    password: "123456",
    email: "#{user.username}@gmail.com",
    bio: "Hi, I'm #{user.username}. Faker::Movie.quote",
    avatar: Faker::LoremFlickr.image(size: "900x900", search_terms: ['dccomics'])
  )
  puts "#{user.username} created!"
end

puts "Creating 10 fake hero services..."
10.times do
  service = Service.create!(
    name: "#{Faker::Verb.ing_form} service",
    description: "I'll use my #{Faker::Superhero.power} to accomplish the mission",
    address: "#{Faker::Address.street_address}, #{Faker::Address.city}",
    photos: Faker::LoremFlickr.image(size: "900x900", search_terms: ["#{Faker::Verb.ing_form}"]),
    user_id: rand(user.id)
  )
  puts "#{service.name} created!"
end

puts "Finished!"

I think I'll have several issues especially:

  • My interpolation : I'm not sure I'm allowed to use it with Faker
  • I'd like to use the same username given in username and email, am I doing right?
  • My user model doesn't have an avatar in the DB table but has_one_attached :avatar, may I give it a seed?
  • Related question about my service model (has_many_attached :photos). How can I generate several pictures for it?
  • I'd like to give a random user_id from one of those created above to my service model, I don't really know how could I proceed...
dbugger
  • 15,868
  • 9
  • 31
  • 33
raphael-allard
  • 205
  • 2
  • 9
  • 1
    You have development and testing databases which can be deleted and recreated if something goes wrong. Try it! – Schwern Nov 17 '20 at 22:04
  • Hi Schwern, I did! I tried to launch my seeds but I got the ```rails aborted```message and I can't figure out why – raphael-allard Nov 17 '20 at 22:23
  • `rails aborted` comes with an error message with a big stack trace of what methods were called to get to the error. There's a lot of stuff from inside Rails. Ignore that. Look at the parts which are in your code like `db/seed.rb` or `app/models/user.rb` and ignore the rest. It's probably ```undefined method `username' for nil:NilClass```. – Schwern Nov 17 '20 at 23:04

2 Answers2

3

My interpolation : I'm not sure I'm allowed to use it with Faker

Interpolation works with any expression. Even multiple statements. "#{ a = 1 + 1; b = 2 + 2; a + b}" will produce "6". But don't do that, it's confusing.

name: "#{Faker::Verb.ing_form} service",

is equivalent to

name: Faker::Verb.ing_form.to_s + " service"

In a few places you're missing the interpolation.

bio = "Hi, I'm #{user.username}. Faker::Movie.quote"

That should be...

bio = "Hi, I'm #{user.username}. #{Faker::Movie.quote}"

In other places the interpolation is not necessary.

search_terms: ["#{Faker::Verb.ing_form}"]

Faker::Verb.ing_form already returns a string and does not need to be interpolated.

search_terms: [Faker::Verb.ing_form]

I'd like to use the same username given in username and email, am I doing right?

No, you can't reference user until it's already been created. If you try you will get something like undefined method `username' for nil:NilClass. user is nil and nil doesn't have a method called username.

Instead you could make a user, set its email, then save.

user = User.new(
  username: Faker::DcComics.hero,
  password: "123456",
  avatar: Faker::LoremFlickr.image(size: "900x900", search_terms: ['dccomics'])
)
user.email = "#{user.username}@gmail.com"
user.bio = "Hi, I'm #{user.username}. #{Faker::Movie.quote}"
user.save!

Or you can make the username first and store it in a variable to reference.

  username = Faker::DcComics.hero
  user = User.create!(
    username: username,
    password: "123456",
    email: "#{username}@gmail.com",
    bio: "Hi, I'm #{username}. #{Faker::Movie.quote}",
    avatar: Faker::LoremFlickr.image(size: "900x900", search_terms: ['dccomics'])
  )

My user model doesn't have an avatar in the DB table but has_one_attached :avatar, may I give it a seed?

I'm not super familiar with attachments in Rails, but I think it should work as you've written.


I'd like to give a random user_id from one of those created above to my service model, I don't really know how could I proceed...

The simple thing is to use sample.

user = User.all.sample

We can make this a little better using pluck to fetch just the ID.

user = User.pluck(:id).sample

However, this loads all Users. For a small test database that's not a problem. For production code that's a huge waste. Instead we can use order by random() limit 1. In Rails 6 it's kinda clunky.

User.order(Arel.sql('RANDOM()')).pluck(:id).first

See this answer for an explanation.

And note, if you already have a User loaded, pass the User, not its ID. This avoids Service having to load the User again.

10.times do
  user = User.order(Arel.sql('RANDOM()')).pluck(:id).first
  service = Service.create!(
    ...
    user: user
  )
end

But there's a better way...


Related question about my service model (has_many_attached :photos). How can I generate several pictures for it?

And here we reach the real leap. Instead of generating static test data, use a Factory. FactoryBot makes it easy to define "factories" to generate test data using Faker. You're already halfway there. And FactoryBotRails lets FactoryBot understand how to Rails models.

A User factory would look like this.

FactoryBot.define do
  factory :user do
    username { Faker::DcComics.hero }
    password { "123456" }
    email { "#{username}@gmail.com" }
    bio { "Hi, I'm #{username}. #{Faker::Movie.quote}" }
    avatar { Faker::LoremFlickr.image(size: "900x900", search_terms: ['dccomics']) }
  end
end

Note that we can make an email based on the username. Attributes can reference other attributes. That's because the attributes are actually little functions kinda like this:

class UserFactory do
  def username
    @username ||= Faker::DDcComics.hero
  end

  def email
    @email ||= "#{username}@gmail.com"
  end
end

And to make 10 of them...

users = FactoryBot.create_list(:user, 10)

You can override the defaults. If you want a user with a certain email address...

user = FactoryBot.create(:user, email: "c.kent@dailyplanet.com")

Since you can make users on the fly, you no longer need to seed specific data.

Now Service. Let's look at the Service factory. To make a Service we need Photos.

  factory :service do
    name { "#{Faker::Verb.ing_form} service" }
    description { "I'll use my #{Faker::Superhero.power} to accomplish the mission" }
    address { "#{Faker::Address.street_address}, #{Faker::Address.city}" }
    user
    # photos is presumably has-many_attached and so takes an Array.
    photos {
      [
       Faker::LoremFlickr.image(
         size: "900x900",
         search_terms: [Faker::Verb.ing_form]
       )
      ]
    }
  end

Have a look at user. FactoryBot will automatically fill that in with a User made using your User factory. The problem of making a Service with a random User is solved.

Our final step is to DRY up our factories by defining one to make a photo.

factory :photo, class: Faker::LoremFlickr do
  size { "900x900" }
  search_terms { Faker::Lorem.words }

  initialize_with do
    Faker::LoremFlickr.image(attributes)
  end
end

This is not a Rails model, so we need to teach FactoryBot how to make a photo by providing it with its class and how to initialize it. attributes is a Hash containing size and search_terms.

Also note that I used more generic search terms. Factories which need specific search terms will provide their own.

We can make as many photos as we want.

photos = FactoryBot.build_list(:photos, 3)

Using the Photo factory we can DRY up the User and Service factories. Here it is all together.

FactoryBot.define do
  factory :photo, class: Faker::LoremFlickr do
    size { "900x900" }
    search_terms { Faker::Lorem.words }

    initialize_with do
      Faker::LoremFlickr.image(attributes)
    end
  end

  factory :user do
    username { Faker::DcComics.hero }
    password { "123456" }
    email { "#{username}@gmail.com" }
    bio { "Hi, I'm #{username}. #{Faker::Movie.quote}" }
    avatar { build(:photo, search_terms: ['dccomics']) }
  end

  factory :service do
    name { "#{Faker::Verb.ing_form} service" }
    description { "I'll use my #{Faker::Superhero.power} to accomplish the mission" }
    address { "#{Faker::Address.street_address}, #{Faker::Address.city}" }
    user
    photos {
      build_list(:photo, 3, search_terms: [Faker::Verb.ing_form])
    }
  end
end

And now, when you need a service to test with, you can ask for one.

service = FactoryBot.create(:service)

Want a service with a different name?

service = FactoryBot.create(:service, name: "Universal Exports")

What about with no photos?

service = FactoryBot.create(:service, photos: [])

That's just scratching the surface of what FactoryBot can do.

Factories are far more flexible and convenient than seed files.

Schwern
  • 153,029
  • 25
  • 195
  • 336
  • Oh wow, thanks for all the explanations, it's really helpful and super interesting, I learned a lot! I'll try to use ```factory``` in my app, it really seems to be simpler, thanks again Schwern! – raphael-allard Nov 18 '20 at 07:48
  • 1
    Nitpicking, but the attributes work more like `def username ; @username ||= Faker::DDcComics.hero ; end`, i.e. they _memoize_ their value so you can reuse it across the factory. – Stefan Nov 18 '20 at 07:59
0

In the aborted message there must be a specific line where seeds is failing. Possibly, it can be the email. What if a username is: "The Joker"? Email can not be "The Joker@gmail.com@".

Try this:

require 'faker'

puts "Creating 10 fake user heros..."
10.times do
  user = User.create!(
    username: Faker::DcComics.hero,
    password: "123456",
    email: "#{username..gsub(/\s+/, "").downcase}@gmail.com",
    bio: "Hi, I'm #{user.username}. #{Faker::Movie.quote}"
  )
  puts "#{user.username} created!"
end

Yshmarov
  • 3,450
  • 1
  • 22
  • 41
  • Thx for your answer Yshmarov, you were right, it comes first from the ```email``` I tried your code but I still have an error message : ```NameError: undefined local variable or method `username' for main:Object```. Btw, why are you using ```split```? Is it not better to use this : ```"The Joker".gsub(/\s+/, "").downcase```? – raphael-allard Nov 17 '20 at 23:00
  • `split` is just an example of how you can modify the username. – Yshmarov Nov 17 '20 at 23:18
  • Actually your `gsub` approach is very good. `"The Joker".gsub(/\s+/, "").downcase => "thejoker"` – Yshmarov Nov 17 '20 at 23:19
  • Ok makes sense. Thanks for your help! – raphael-allard Nov 18 '20 at 07:11