Eric / Brooklyn

Visual

Blog Index

Random 5

Stripe and Tap

Multi-tab Websockets

Ruby vs. Ruby

Cookies

Web Accounts

Emoji Bits

Honeypot Bots

Bad Content

JS Data Chutzpah

Sendgrid Ban

Clean URLs

Git

Concerns

Rails UJS

Extending Devise

ENV Variables

See All

Ruby vs. Ruby

April 2020

Web Dev Flamewars

Ruby vs Python vs Javascript vs Rails vs Node vs React vs Vue vs Elixir.

Some developers enjoy these debates. I’m not sure how they’re really debates though — the answer is always the same. Some combination of it depends and whatever floats your boat.

Well, let’s talk a bit about the boats Ruby floats.

Matz

Ruby was created for a single reason — to make Yukihiro Matsumoto happy.

Well I think he’s happy.

Fortunately his happiness trickled down to others so we have nice things like Rails, Sinatra, Stripe, Twitter, Github, AirBnb, DHH’s Twitter rants, and this blog post.

The Language

Let’s look at some regular every day Ruby things. Nothing fancy, just doing the chores.

Seeding a Database using Ruby on Rails

Typically, when we’re building a web application, we’ll want to seed a development database with some stuff that resembles the data that our users will input.

This helps when looking for bugs, testing load times, and fine tuning design and layout.

With Bounce, we need to seed the dev database with users (students and teachers), subjects (things that are taught), teacher_subjects (a table that connects teachers to their saved subjects), relationships (students following teachers), chats, messages, and so on.

For now, we’ll just look at the users and teacher_subjects.

Quick Loop

For a Rails application, our seeding happens in our seeds.rb file:

NUM_USERS = 100

NUM_USERS.times do
	# create the Users
	
	# then, create the TeacherSubjects
end

We want to loop 100 times — just a quick 100.times and we’re ready to go.

Quick side note: NUM_USERS is a constant variable, and it’s convention (not mandatory) to give it all caps, as a way of keeping-track-of-things.

Constants are useful when we want to share information between methods, so as you might expect, Ruby will complain if you create a constant inside a method.

Variables, more generally

There’s no need for declaring type when creating a variable. Just name it, and set it equal to something.

my_var = 10 # regular variable
NUM_USERS = 100 # constant variable

This means that Ruby is dynamically typed — a variable’s type depends on whatever data is assigned to it.

my_var = 10
puts my_var.class
> Integer

my_var = "hello"
puts my_var.class
> String

my_var = [2, 3, 4]
puts my_var.class
> Array

# "puts" means "put string", which just displays results on a new line

The alternative to dynamic type is static type — where things are a bit more cumbersome (we explicitly declare type and it doesn’t change), but also less bug prone.

Dynamically Typed: Ruby, Javascript. Python, PHP

Statically Typed: C, TypeScript, Java

Let’s create some users

# seeds.rb

NUM_USERS = 100

def rand_bool
	[true, false].sample
end

NUM_USERS.times do |i|
	
	t = rand_bool
	s = rand_bool
	
  User.create!(
    email: "t+#{i}@bounce.so",
    username: Faker::Internet.unique.username(separators: %w[_]),
    profile: Faker::Lorem.paragraphs(number: 7).join,
    password: 'foobar',
    teacher: t,
    student: s
  )
	
	# create teacher subjects
	
end

Right under our constant at the top, we define a method rand_bool, but let’s set that aside for now.

Next, we may have noticed this dangly |i| that’s hanging after our do. This allows us to reference the index state of our loop. This will start at 0 and end at 99.

Other Loops

If we peak ahead at the seed file, it looks like we’re using this i value in the email field. But what if we didn’t want to start at 0 and go to 99?

Let’s say we wanted to go from 1 to 100.

Using a Ruby range (a..b):

(1..100).each do |i|
	# do stuff
end 

or

1.upto(100).each do |i|
	# do stuff
end 

(there’s also downto)

or if we wanted to adjust our index manually, we could:

100.times do |i|
	i += 1
	# do stuff
end

or

i = 1
until i > 100
	# do stuff
  i += 1
end

or

i = 1
while i < 101
	# do stuff
  i += 1
end

Ok I’ll stop for now — the point is we have a lot of simple small tools in the shed.

Validations

Let’s take a step back and ask why we even added the |i| to our loop.

As mentioned earlier, this |i| is just a variable we create, that iterates by 1 with our loop. We could have called it anything — |user_number|, |iterator|, |x|.

As part of our User model, we require that a user provides an email — this is how users log in. No email, no account.

As you’d expect, we need these emails to be unique, so we validate for uniqueness.

class User < ApplicationRecord

	validates :email, presence: true, uniqueness: { case_sensitive: false }
	
	...
end

This validation makes sure the email exists, and is unique, with case_sensitive: false ensuring that eric@mail.com is considered the same as Eric@mail.com .

The truth is that we don’t actually have that code in our User model, because Devise handles this for us using devise :validatable (but it would have worked!).

String Interpolation

So, we need to create unique emails, and one way to do this is to add this iterator to a test email string (so the emails will look like t+1@bounce.so, t+2@bounce.so, etc)

email: "t+#{i}@bounce.so"

What we see here is string interpolation. We want to insert a variable inside a string.

I suppose we could add strings together like

email: "t+” + i.to_s + ”@bounce.so"

but this is a pain. Also note that we would’ve had to convert i to a string via to_s. Anyways this is a bad idea so we’re interpolating instead.

But why did we create these weird interpolated emails instead of using Faker? (Faker is a Gem that helps us create dummy info for our models — which we are using for other attributes).

It’s because we want to test email deliverability in our staging environment, so we want to use real emails when seeding. I have t@bounce.so set up, and gmail allows the appended + namespace.

Trying to send emails to Faker emails will get us banned from Sendgrid in a matter of seconds. I learned this lesson the hard way - I rambled about it elsewhere on the blog.

Raising Exceptions

We’ve gotten ahead of ourselves again.

Rails create will create a User object and save it to the database, assuming all validations pass.

  User.create!(
    email: "t+#{i}@bounce.so",
    username: Faker::Internet.unique.username(separators: %w[_]),
    profile: Faker::Lorem.paragraphs(number: 7).join,
    password: 'foobar',
    teacher: t,
    student: s
  )

The exclamation mark at the end of create isn’t mandatory, but is helpful for debugging purposes, because a failed attempt at creation (likely due to a failed validation) will raise an exception.

This just means that our computer will loudly complain in the terminal, and halt execution of our seeds.rb. Then we fix things and try again.

The alternative is no ! and things will fail silently, and we’ll be left with a partially seeded database (up until the point the exception was raised).

Note that we only want to use ! when creating Users in our seeds.rb, and not when creating users in our controller. This is because we don’t want our app to fail when a user’s registration info fails validation — we handle that situation more gracefully in our User model and controller.

Faker

Let’s look back at the big picture:

NUM_USERS = 100

def rand_bool
	[true, false].sample
end

(1..NUM_USERS).times do |i|
	
	t = rand_bool
	s = rand_bool
	
  User.create!(
    email: "t+#{i}@bounce.so",
    username: Faker::Internet.unique.username(separators: %w[_]),
    profile: Faker::Lorem.paragraphs(number: 7).join,
    password: 'foobar',
    teacher: t,
    student: s
  )
	
	# create teacher subjects
	
end

Inside our User.create!, we create our username and profile using the Faker Gem. Note that we have a uniqueness constraint on username in the User model so we need to tell Faker.

Faker::Internet.unique.username(separators: %w[_])

The %w[_] is just some regex we use to say we only want letters, numbers, and underscores in our usernames.

Ruby Methods

The last two attributes of our new user are teacher and student which are saved as booleans. Users on Bounce can be teachers, students, or both.

Instead of hard-coding a boolean into these fields, we seem to be calling something called rand_bool.

This method is defined at the top of the seed file.

def rand_bool
	[true, false].sample
end

Parentheses are not required when declaring a method.

Inside our method rand_bool (short for random boolean) we sample from an array. All this does is return a random element from the array it was called upon — in this case, either true or false

Instance Methods

The sample we see above is also a method (parentheses are optional when there are no arguments), though this method is a bit different, in that it appears to be attached to the back of an array.

This is what’s called an instance method. This means it can be called on instances of a particular class — in this case, [true, false] is an instance of the class Array.

Classes are essentially just blueprints for how data can be represented (i.e. Array, String, Integer, and you can even create your own).

Implicit Returns

Our rand_bool method may seem a bit small — are we missing something?

Nope — Ruby has implicit returns from methods. This means that whatever is returned on the last line of a method is automatically returned by that method.

In our case, the sample returns true or false, which then gets returned to our teacher and student attributes via our t and s variables (more on these in a bit).

If it wasn’t already clear, the purpose of rand_bool is to randomly assign roles to our seeded users — as mentioned before, they can be students, teachers, or both.

Teacher Subjects

We created some teachers and some students. In the case that the created user is a teacher, we want to add some TeacherSubjects.

As mentioned earlier, this model connects teachers with their saved subjects (via a join table containing foreign keys to the users table and the subjects table).

Guard Clause

Instead of wrapping code in a conditional expression, Ruby prefers guard clauses if possible.

For us, this means that we aren’t going to say:

NUM_USERS.times do
	...
	t = rand_bool # boolean indicating whether user is a teacher
	...
	if t
		# add TeacherSubjects
	end
end

Instead, we’ll say:

NUM_USERS.times do
	...
	t = rand_bool
	...
	next unless t
	# add TeacherSubjects
end

Next just skips to the next iteration, and we want to skip unless the user is a teacher, because in that case, let’s add some TeacherSubjects.

If and unless work similarly, except exactly opposite, if that makes sense.

$ “testing 123” if true
=> testing 123

$ “testing 456” unless true
=> nil

$ “testing 789” if false
=> nil

$ “this one is confusing” unless false
=> “this one is confusing”

This little code snippet also shows how we can put our conditional after our return statement for code that fits on a single line.

Okay, let’s see this guard clause in action:

(1..NUM_USERS).times do |i|

	t = [true, false].sample
	s = [true, false].sample

	User.create!(
		...
		student: s,
		teacher: t
	)

  next unless t
	# create TeacherSubjects
end

Alright so let’s finally add these TeacherSubjects. We’ll probably do something like:

(1..NUM_USERS).times do |i|
...
	t = [true, false].sample
	...
	next unless t
	
	num_subs = rand(1..7)
	num_subs.times do
		TeacherSubject.create!(
		  user_id: i,
		  subject_id: rand(1..Subject.count)
		)
	end
	...
end

But if we did this, we’d run into an issue in short order.

The ! is our tip off. If TeacherSubject.create! fails a validation, the seeding will halt. We haven’t mentioned this yet, but as you might guess, we have a uniqueness constraint on TeacherSubject. A teacher cannot save the same subject twice (this is handled on the database side with a multi column index)

To back up a bit — num_subs is just a temporary variable we create to say — let’s expect a teacher to save somewhere between 1 and 7 subjects.

Then, we loop num_subs times, and create a TeacherSubject each time.

The problem is, there’s a chance that rand(1..Subject.count) will return the same number twice within the num_subs.times loop. If that happens, we’ll effectively be trying to have a teacher save the same subject twice.

A Solution

If you’re still with me, this is the most fun part of the whole post.

We need to come with a way that we don’t reuse a random number.

Let’s create an array of numbers, from 1 to Subject.count (these are the subject_ids for the subjects).

(1..Subject.count).to_a

Then, let’s randomize this array:

(1..Subject.count).to_a.shuffle

Shuffle does what you’d expect, shuffling the array. It’s similar to the earlier sample, in that they’re both instance methods on array objects.

Now we have an array of random numbers, from 1 to the total number of subjects. Each time we create a TeacherSubject for a teacher, we’ll pop the last value of the array, and save it as the subject_id.

Pop returns the last value of an array, and then removes it.

$ arr = [2, 5, 3]
$ arr.pop
=> 3
$ arr
=> [2, 5]

Here’s the pop in action.

NUM_USERS = 100

(1..NUM_USERS).times do |i|

    t = [true, false].sample
    s = [true, false].sample

    User.create!(
        ...
        student: s,
        teacher: t
    )

    next unless t
    
    # add TeacherSubjects
    sub_ids = (1..Subject.count).to_a.shuffle

    num_subs = rand(1..7)
    num_subs.times do
    TeacherSubject.create!(
      user_id: i,
      subject_id: sub_ids.pop
    )
    end
end

So each teacher will have her own personal shuffled array.

Since we pop the last element of the sub_ids array during each iteration of the num_subs loop (remember it gets removed), we’ll never reference the same subject for any given teacher.

Conclusion

We have reached the end of this meandering post about Ruby, Rails, and trying to seed a database without breaking something and everything.