私は今フィヨルドブートキャンプでチーム開発のプラクティスをやっているのですが、そこでは実際みんなが使っているブートキャンプのアプリの開発に参加します。

ブートキャンプアプリはacts-as-taggable-onというgemを使っていて、私が担当したissueではそのgemが絡むことが多かったので、ちょっと困ったところや調べたところをメモしておこうと思います。

taggins_countについて

fixtureで設定したタグの数がテーブルに入らない

gemの導入方法とかはさておき、とりあえずこのgemを入れると、taggingsとtagsの二つのテーブルができます。tagsテーブルの方はid、name、taggings_countの3つのカラムがあって、そのタグが登録されている数がtaggings_countに入っている。はずです。

tagsテーブル taggingsテーブル
id id
name(タグの名前) tag_id(タグのid)
taggings_count(そのタグが登録されている数) tag_type(モデル名)
  taggable_id(登録されているもののid)
  tagger_type
  tagger_id
  context
  create_at

だけど、初めてリポジトリクローンして立ち上げて開いたら画面上ではタグがあるのに、テーブルでは全てのタグのカウントがゼロになっていてえ???ってなってしばらく悩んでしまいました。

fixtureで登録されているタグのカウント数はテーブルのtaggings_countには反映されないです。(これは先人に教えてもらった)

でも手動?で登録すればちゃんと反映されるので本番の方では大丈夫です。数はちゃんと入ってます。ただ、ちょっと問題がある。

別モデルで登録したタグと数が混ざる

タグは複数のモデルで付けられます。ただし、別々のモデルで同じ名前のタグを登録した場合taggings_countのカウントが混ざる。「猫」とタグ付けされた人が3人いて、「猫」とタグ付けされた記事が2本あったら、tagsテーブルでは「猫」のカウントが5となるだけで、gemのメソッドを使ってもこれを分けて取ることができません。

ActsAsTaggableOn::Tag.most_used
ActsAsTaggableOn::Tag.least_used

例えばこれはタグの中で登録数が多いもしくは少ない順にタグを取得できるメソッドですが、この数も全モデルでの数になるので、ユーザーが登録しているタグの中で数が一番多いタグが欲しい、といった場合は使えない。多分どのメソッドもこれは考慮されていないと思われます。

じゃあどうやって取得するの?ということで、これはgemのissueにも上がっているようです。(これも先人に教えてもらった)

https://github.com/mbleigh/acts-as-taggable-on/issues/719#issuecomment-172490087

ActsAsTaggableOn::Tag
            .joins(:taggings)
            .select('tags.id, tags.name, COUNT(taggings.id) as count')
            .group('tags.id, tags.name, tags.taggings_count')
            .where(taggings: { taggable_type: 'User'})

taggable_typeはモデルの名前が入っているので、taggingsテーブルを連結してそれで絞り込めば数が取れるということですね。bootcampのコードもこれをアレンジして使っています。このgemのメソッドでどういうSQLが発行されているのかはチェックしたほうがいい感じだと思われる。

余談だけど、こうやって取得したタグの数だけ欲しくて、.pluck(:count)ってやったらできなくて、.pluck('COUNT(taggings.id) as count')か、.map(&:count)でやるしかなかった。pluckは元々あるカラムしかカラム名指定することができないんですね〜。

tag_listについて

tag_listのattributeがnilになる

READMEにもあるとおり、タグはこんな感じでtag_listに配列として登録されるんですが、

@user.tag_list.add("awesome")   # add a single tag. alias for <<
@user.tag_list.remove("awesome") # remove a single tag
@user.save # save to persist tag_list
@user.tag_list # => ["awesome"]

コンソールでユーザーの中身を見るとtagは登録されていてもいなくても常にnilになっています。

#<User id: 1234, name: "yamada", tag_list: nil>

user.tag_list?はいつもfalseです。 それをやりたかったところは、私はuser.tag_list.empty?ていう形で書きました。 これもissueが上がっていました。バージョンアップしてもそのままな気がする…。

https://github.com/mbleigh/acts-as-taggable-on/issues/1024

N+1問題

ユーザーを全部取得してユーザーごとに登録しているタグを表示したいなーなどと思った時

controller

def index
   @users = User.all
end

view

<% @users.each do |user| %>
    タグ:<%= user.tag_list.join(', ') %>
<% end %>

こうやって書くとですね、毎回2回ずつSQL発行されててN+1問題が発生しています。

bootcampではbulletを入れているのでN+1問題が発生しているとメッセージが表示されて、この場合taggingsをincludeしてね!という表示がでます。ただ、tagginsをincludeするとこの警告は消えるんですけど、確認すると実はまだ毎回1回ずつSQLが発行されていました。

https://github.com/mbleigh/acts-as-taggable-on/issues/91#issuecomment-168273770

これもissueに上がっていて、tagsをincludeしてmapでtagの名前だけ取るのだ、という解決策があるみたいです。

上のを修正するとこんな感じ。

controller

def index
   @users = User.all.preload(:tags)
end

view

<% @users.each do |user| %>
    タグ:<%= user.tags.pluck(:name).join(', ') %>
<% end %>

他の記事を参考にpreloadとpluckを使っています。bootcampのアプリでは今のところ私が担当した部分だけこれで書いてみています。パフォーマンスが結構変わるなら全部書き直したほうがいいのかなぁ。

includeではなくpreloadとeager_loadを使用する方がいいのかなという参考記事はこちらとか。

これもSQL確認しといた方がいいよ〜、て感じでしょうか。

個人的には、tagsやtaggingsテーブルへのデーターの登録が全部gemの中で行われているので、それをgemのメソッドを使わないでアレンジしないといけない時に、どこのモデルやコントローラーに書くのがいいのかっていうのがちょっと悩みました。いろいろ勉強になりました。