ワーパパエンジニアの学び手帳

ワーパパエンジニアの業務外での学びとかガジェットネタとか

【Ruby on Rails】acts-as-taggable-onでタグ付けしたレコードのcount取得に苦戦

今現在Railsの勉強をしてまして、Railsチュートリアルを一通り終えて作ったものをベースにちょっと改良を加えています。

今やろうとしているのは、投稿した内容にタグ付けし、タグで投稿を検索できるようにするというもの。

f:id:us_key:20170413223426g:plain
投稿のフォームはこんな感じ。
タグ機能acts-as-taggable-onというgem、タグ表示のUIはBootstrap Tags InputというjQueryプラグインを使用しています。

目次

動作環境

  • Ruby 2.3.0p0
  • Rails 5.0.0.1
  • acts-as-taggable-on 4.0.0

使用方法

こちらのブログ記事を参考にしました。

ruby-rails.hatenadiary.com 動作環境のバージョンは異なりますが問題なく使えました。

実装

こんな感じの検索フォームを用意しました。
f:id:us_key:20170413225603g:plain
見た目はいまいちですが。。
検索条件でタグを指定するとそのタグを使用している投稿が表示されます。

参考記事にも書かれていますが、タグでのレコード抽出方法にはいくつかあります。

検索条件に指定したタグのいずれかを含むレコードを検索

#Model(post.rb)
class Post < ApplicationRecord
  acts_as_taggable_on :labels
  acts_as_taggable #この2行の指定でタグ関連テーブルと紐付けられる

###中略###

  scope :find_by_tag, -> tags {
    #タグが入力されている場合のみタグを条件に抽出
    if tags.present?
      tagged_with(tags, any: true) #タグ付けレコードの取得方法を指定
    end
  }

end

#Controller(posts_controller.rb)
class PostsController < ApplicationController

###中略###

  def search
    @tags = params[:tags]
    @posts = current_user.posts.find_by_tag(@tags) #current_userはログインユーザー取得のヘルパーメソッド
    respond_to do |format|
      format.js
    end
  end

end
/* js.erb(search.js.erb) */
var searchResultHtml = "";
var count = 0;
// 検索結果が存在する場合のみカウントを取得
<%if @posts.exists? %>
  searchResultHtml = '<%=j render @posts%>';
  count = <%=@posts.count%>
<%end%>
$('#post-search-result-table').html(searchResultHtml);
$('span#search-result-count').html(count.toString());

タグと関連付けたModelに対しtagged_with(tags, any: true)というメソッドを使うことで、検索条件で指定したタグのいずれかを含むレコードが検索できます。
今回の実装ではajaxで検索結果を表示するようviewというかformで指定しているため、controllerでインスタンス変数にセットした検索結果をjs.erbで画面に反映しています。

検索条件に指定したタグすべてを含むレコードを検索

上記の実装でtagged_with(tags, any: true)としていた個所をtagged_with(tags, match_all: true)とします(“any"を"match_all"に変えている)。

詰まったところ

どちらの取得方法でも件数を取得するとこで詰まりました。。

検索条件に指定したタグのいずれかを含むレコードを検索した場合の件数取得

上記の実装でタグを条件に含む検索を実行すると、見事にエラー。こんなログ出ました(見やすいように改行を入れています)。

ActionView::Template::Error (SQLite3::SQLException: near "*": syntax error: 
SELECT COUNT( posts.*) FROM "posts" WHERE "posts"."user_id" = ? 
AND (EXISTS (SELECT 1 FROM taggings posts_taggings_7e240de 
WHERE posts_taggings_7e240de.taggable_id = posts.id 
AND posts_taggings_7e240de.taggable_type = 'Post' 
AND posts_taggings_7e240de.tag_id in (2)
))):

おいおいまさかのSyntax error…
ぱっと見何がダメなのか分からなかったのですが、よくよく見てみると。。。最後のカッコが1つ多いような。。。

GitHubリポジトリ見てみたら、issueに挙がってました。

.tagged_with([], any: true).count throws MySQL error · Issue #649 · mbleigh/acts-as-taggable-on · GitHub

issueはopenのままなんですが、解決方法を書いてくれてる人が。
どうやら.count.count(:all)とすれば解消するようです。。

/* 修正後のsearch.js.erb */
var searchResultHtml = "";
var count = 0;
<%if @posts.exists? %>
  searchResultHtml = '<%=j render @posts%>';
  count = <%=@posts.count(:all)%> // .count⇒.count(:all)
<%end%>
$('#post-search-result-table').html(searchResultHtml);
$('span#search-result-count').html(count.toString());

修正後の内容で発行されるクエリはこんな感じになりました。カッコの数が正しくなってます。あとcountの取り方が変わってますね(ここだけ見ても結果は同じですが)。

SELECT COUNT(*) FROM "posts" WHERE "posts"."user_id" = ?
AND (EXISTS (SELECT 1 FROM taggings posts_taggings_a94a8fe
 WHERE posts_taggings_a94a8fe.taggable_id = posts.id
 AND posts_taggings_a94a8fe.taggable_type = 'Post'
 AND posts_taggings_a94a8fe.tag_id in (1)
))

検索条件に指定したタグすべてを含むレコードを検索した場合の件数取得

こちらも件数がちゃんと取得できておらず。
こちらはログに出力されたクエリがこんな感じになってました。

/* タグあり、match_all指定 */
SELECT COUNT(*) AS count_all,
       posts.id AS posts_id
FROM "posts"
JOIN taggings posts_taggings_a94a8fe
ON  posts_taggings_a94a8fe.taggable_id = posts.id
AND posts_taggings_a94a8fe.taggable_type = 'Post'
AND posts_taggings_a94a8fe.tag_id = 1
LEFT OUTER JOIN taggings posts_taggings_group
ON  posts_taggings_group.taggable_id = posts.id
AND posts_taggings_group.taggable_type = 'Post'
WHERE "posts"."user_id" = ?
GROUP BY posts.id
HAVING (COUNT(posts_taggings_group.taggable_id) = 1)
ORDER BY "posts"."created_at" DESC

どうもcountだけじゃなくpostのIDも取得されてしまってるようです。
結果はHashとして、{“1” => “1”,“1” => “2”}(件数 => ID)のような形で返却されます。
postのIDごとにgroup byしてるけど、件数はすべて1件ずつになる気がするので.count.lengthでHashの要素数取れば合計の件数が取れるかなぁ。
しかしタグを指定していないと、こんな感じでcountだけを取得するクエリになっています。

/* タグなし */
SELECT COUNT(*)
FROM "posts"
WHERE "posts"."user_id" = ?

タグが指定されてるかどうかで件数の取り方を変えなあかん、てことでこんな実装になりました。汚い。

/* match_all指定時のjs.erb実装 */
var searchResultHtml = "";
var count = "0";
<%if @posts.exists? %>
  searchResultHtml = '<%=j render @posts%>';
  // タグを検索条件に含めるとModel#countの結果がHashとなってしまうため
  // hashの長さを取得(件数分hashの要素が作られる想定)
  <%if @tags.present?%>
    count = <%=@posts.count.length%>;
  <%elsif%>
    count = <%=@posts.count%>
  <%end%>
<%end%>
$('#post-search-result-table').html(searchResultHtml);
$('span#search-result-count').html(count.toString());

うーん詰まりまくったけどひとまずこんな感じでOKかなぁ。

railsというかrubyというか、まだまだ色々分かってないので詰まるとなかなか大変です。

長くなってしまいましたが今回はこの辺で。