ActiveRecord Joins

Random thing I missed while turning all those selects into regular queries using joins: if you use the names of associations to join tables (ex. recipes.joins(:ingredients)), it creates an INNER join. Why is this important? Here’s my example:

I have recipes with ingredients, and each ingredient might have a subingredient (gluten, soy, etc). Every recipe has ingredients, so an inner join is fine there. However, every recipe does not have subingredients. Since I was doing a recipes.joins(ingredients: :subingredients), I was only getting recipes that did not have subingredients... definitely not what I wanted. Here’s the code:

BEFORE:
Code
Recipe.joins(ingredients: :subingredients).joins(:ingredients)

Resulting SQL
SELECT "recipes".* FROM "recipes" INNER JOIN "ingredient_measurements" ON "ingredient_measurements"."recipe_id" = "recipes"."id" INNER JOIN "ingredients" ON "ingredients"."id" = "ingredient_measurements"."ingredient_id" INNER JOIN "ingredients_subingredients" ON "ingredients_subingredients"."ingredient_id" = "ingredients"."id" INNER JOIN "subingredients" ON "subingredients"."id" = "ingredients_subingredients"."subingredient_id"

AFTER:
Code
Recipe.joins(:ingredients).joins('LEFT JOIN "ingredients_subingredients" ON "ingredients_subingredients"."ingredient_id" = "ingredients"."id" LEFT JOIN "subingredients" ON "subingredients"."id" = "ingredients_subingredients"."subingredient_id"')

Resulting SQL
SELECT "recipes".* FROM "recipes" INNER JOIN "ingredient_measurements" ON "ingredient_measurements"."recipe_id" = "recipes"."id" INNER JOIN "ingredients" ON "ingredients"."id" = "ingredient_measurements"."ingredient_id" LEFT JOIN "ingredients_subingredients" ON "ingredients_subingredients"."ingredient_id" = "ingredients"."id" LEFT JOIN "subingredients" ON "subingredients"."id" = "ingredients_subingredients"."subingredient_id"

UPDATE: As Darren (manvsmachine) mentions, you can also use .eager_load to create LEFT OUTER joins. However, that does give you a different query. It seems to have the same-ish results, but the SQL output is slightly different. It looks something like this:

Code
Recipe.eager_load(:subingredients)

Resulting SQL
SELECT "recipes"."id" AS t0_r0, "recipes"."name" AS t0_r1, "recipes"."instructions" AS t0_r2, ... "recipes"."image" AS t0_r14, "subingredients"."id" AS t1_r0, "subingredients"."name" AS t1_r1, "subingredients"."created_at" AS t1_r2, "subingredients"."updated_at" AS t1_r3 FROM "recipes" LEFT OUTER JOIN "ingredient_measurements" ON "ingredient_measurements"."recipe_id" = "recipes"."id" LEFT OUTER JOIN "ingredients" ON "ingredients"."id" = "ingredient_measurements"."ingredient_id" LEFT OUTER JOIN "ingredients_subingredients" ON "ingredients_subingredients"."ingredient_id" = "ingredients"."id" LEFT OUTER JOIN "subingredients" ON "subingredients"."id" = "ingredients_subingredients"."subingredient_id"

Optimizing Your Rails App

NOTE: This is only a short post on one thing that I did that drastically reduced memory leaks and helped the performance of my app. if you want to go more in depth, I would recommend Alexander Dymo’s Ruby Performance Optimization.

About a month ago, I became the lead (and only dev) at a small startup. I emphasize small because that’s important to what comes later. Soon after I started, I was seeing a load of Heroku R14 errors... to the tune of 200+ per day. At first I just paid for one more dyno... then another dyno. When I got to seven, I realized that I needed to do something else and fast, because we couldn’t keep upping the dynos every time we got a little bit more traffic. I was also still seeing a TON of R14 errors. Again, I’ll emphasize small, because we really don’t have the large user base that would be actually causing us problems like this. We were just having massive memory leaks. So, time to dig into the code!

The main issue seemed to be a function that saved all recipes that a user could eat (i.e.: they weren’t allergic, didn’t contain any of their dislikes, etc). We were originally determining this by doing something like this:

recipes = Recipe.all
recipes.reject! do | recipe |
  recipe.contains_allergens?
end
recipes.reject! do | recipe |
  recipe.contains_dislikes?
end
# and so on...

So what's the better, safer way to do this? Just build a query! ActiveRecord only runs a query when you perform an action on it, so I could build the whole query and then have my only action be saving it to User.recipes. Example:

user_recipes = Recipe.joins(:ingredients)
user_recipes = user_recipes.where('ingredients.id not in (?)', allergies.map(&:id))
user_recipes = user_recipes.where('ingredients.id not in (?)', dislikes.map(&:id))
# and so on...

This is a gross simplification of the actual code. I know that seems like a silly trivial change, but, no joke, it sped our tests alone up by 4 minutes. Why? Because previously, when we were doing all the rejects, Ruby was saving a new variable in each iteration. Also, while I forgot to test my hypothesis, I'm pretty sure that since recipes was an attribute of User and we were assigning to a 'variable' recipes within the User model, that it was actually saving during each call... also super memory intensive.

So what can you do? If you have some code in your app that you think is slow, you can test it using this benchmark script. If you have that added to your lib directory, just call it like this:

Measure.run do
  Recipe.last #whatever code you want to test
end

The output will look like this:
{"2.2.2":{"gc":"enabled","time":0.04,"gc_count":0,"memory":"0M"}}

To translate:
{ruby-version: {"gc": are_garbage_collectors_enabled?, "time": time_it_took_to_run, "gc_count": total_num_of_garbage_collectors, "memory": amount_of_memory_used }}

Raising ActiveRecord::Rollback

I had a bit of code that I was running in a transaction block. Like so:

ActiveRecord::Base.transaction do
  @rating = Rating.new(rater: @user, rateable: @movie, score: params[:score])
  if !@rating.save
    Rails.logger.error("Error saving rating")
    raise ActiveRecord::Rollback
  end
end

I kept trying to test this by doing:

expect { post :create etc }.to raise_error ActiveRecord::Rollback

Total failure. Couldn’t figure out why for a while. Here’s the reason: raising ActiveRecord::Rollback just triggers a rollback of the transaction and does so silently. What I should’ve done is raise a different type of error (TBD) and that will still trigger a rollback of the transaction but the error will actually be raised and we can return an appropriate page to the user.

More info on StackOverflow.