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.