Rails: Gutentag tag_names with N+1 problem

袁浩 Harry Yuan
3 min readNov 10, 2020

--

Before you go

GutenTag is a handy gem for adding tags to your model. However, if don't use it with care, you might get yourself into unexpected N+1 problem.

GutenTag creates two tables:

  1. gutentag_taggings
  2. gutentag_tags

You can see its columns in your schema.rb or via rails console

Gutentag::Tagging 
# => Gutentag::Tagging(id: integer, tag_id: integer, taggable_id: integer, taggable_type: string, created_at: datetime, updated_at: datetime)
Gutentag::Tag
# => Gutentag::Tag(id: integer, name: string, created_at: datetime, updated_at: datetime, taggings_count: integer)

What it actually does is that it associates your model as taggable via polymorphism, having the below relations:

SomeModel has_many tags, through taggings, as taggable

Scenario

I have an API which renders with tag_names and I found it very slow. I’ve already included the tags association. I also double checked with Bullet and it didn’t report any problem.

The original setup was like:

class ArticlesController < ApplicationController
def index
@articles = Article.includes(:tags).all
end
end
class ArticleSerializer < ActiveModel::Serializer
attributes :id, :title
attributes :tags
def tags
object.tag_names
end
end

I went to see logs and found that even if the :tags association was already included, ActiveRecord still fetches each tag on every iteration:

I’ve already done includes(:tags)
=> @articles = Article.includes(:tags).all
=> @articles.map{|article| article.tags.map(&:name)}
Article Load (0.6ms) SELECT "articles".* FROM "articles" WHERE "articles"."user_id" = 3446 LIMIT 1 [["user_id", 3446]]
Article Load (0.6ms) SELECT "articles".* FROM "articles" WHERE "articles"."user_id" = 3446 LIMIT 1 [["user_id", 3446]] (0.7ms) SELECT "gutentag_tags"."name" FROM "gutentag_tags" INNER JOIN "gutentag_taggings" ON "gutentag_tags"."id" = "gutentag_taggings"."tag_id" WHERE "gutentag_taggings"."taggable_id" = 1238 AND "gutentag_taggings"."taggable_type" = 'Article' [["taggable_id", 1238], ["taggable_type", "Article"]] (0.7ms) SELECT "gutentag_tags"."name" FROM "gutentag_tags" INNER JOIN "gutentag_taggings" ON "gutentag_tags"."id" = "gutentag_taggings"."tag_id" WHERE "gutentag_taggings"."taggable_id" = 1238 AND "gutentag_taggings"."taggable_type" = 'Article' [["taggable_id", 1238], ["taggable_type", "Article"]] Article Load (0.5ms) SELECT "articles".* FROM "articles" WHERE "articles"."user_id" = 3541 LIMIT 1 [["user_id", 3541]] Article Load (0.5ms) SELECT "articles".* FROM "articles" WHERE "articles"."user_id" = 3541 LIMIT 1 [["user_id", 3541]] (0.7ms) SELECT "gutentag_tags"."name" FROM "gutentag_tags" INNER JOIN "gutentag_taggings" ON "gutentag_tags"."id" = "gutentag_taggings"."tag_id" WHERE "gutentag_taggings"."taggable_id" = 733 AND "gutentag_taggings"."taggable_type" = 'Article' [["taggable_id", 733], ["taggable_type", "Article"]] (0.7ms) SELECT "gutentag_tags"."name" FROM "gutentag_tags" INNER JOIN "gutentag_taggings" ON "gutentag_tags"."id" = "gutentag_taggings"."tag_id" WHERE "gutentag_taggings"."taggable_id" = 733 AND "gutentag_taggings"."taggable_type" = 'Article' [["taggable_id", 733], ["taggable_type", "Article"]] Article Load (0.7ms) SELECT "articles".* FROM "articles" WHERE
...
no

What to do ?

I came up with a naive yet quick approach, which is to access tag’s name from the included tags association, instead of using the tag_names instance method.

TL;DR

  1. Include tags
  2. Map to name

If we apply it to this scenario, it will look like this:

class ArticlesController < ApplicationController
def index
@articles = Article.includes(:tags).all
end
end
class ArticleSerializer < ActiveModel::Serializer
attributes :id, :title
attributes :tags
def tags
object.tags.map(&:name) # this won't cause n+1 anymore
end
end
# access via iteration
@articles.map{|article| article.tags.map(&:name)}

After that nothing explodes anymore, and my API response time reduced 550ms .

@articles.map{|article| article.tags.map(&:name)}
# => [["sport"], ["news"], ["food"], [], ["programming"], ...]

--

--

袁浩 Harry Yuan

Software Engineer | Ruby on Rails 喜歡學習前後端技術。希望文章白話到阿嬤都看得懂。