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

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

【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というか、まだまだ色々分かってないので詰まるとなかなか大変です。

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

PostgreSQLのALTER TABLEに苦戦

PlayframeworkではDDLを予め用意しておくことでアプリケーションの起動時にDDLを実行してテーブルを自動生成してくれます(evolutions)。
Java版だとModelからDDLを自動生成してくれますが、Scala版だとDDLは自分で書く必要があります(という認識)。

自作アプリケーション作成時に、ローカルではH2、本番(Heroku)ではPostgreSQLを使用しています。
ところが、ローカルでは実行できていたDDLがHerokuではコケるという事象が。
PostgreSQLのエラーログがいまいちわかりづらくてなかなか原因が把握しづらかったのですが、どうやら以下の通りらしい。

H2とPostgreSQLではALTER TABLEの文法が異なる

今回コケたのはALTER TABLE文で、カラムの型変更をしようとしてました。

/* H2 */
alter table TABLE1 alter column COLUMN1 (DATATYPE); 
/* PostgreSQL */
alter table TABLE1 alter column COLUMN1 [SET DATA] TYPE (DATATYPE);

(DATATYPE)は変更後の型、[]内の有無は任意です。

さらに、今回カラムの型変更と同時にデフォルト値の設定をしようとしたのですが、H2では1文で実行できる一方、PostgreSQLでは2文に分けないと実行できないようです。

/* H2 */
alter table TABLE1 alter column COLUMN1 TIMESTAMP SET DEFAULT CURRENT_TIMESTAMP;
/* PostgreSQL */
alter table TABLE1 alter column COLUMN1 [SET DATA] TYPE TIMESTAMP;
alter table TABLE1 alter column COLUMN1 SET DEFAULT CURRENT_TIMESTAMP;

この例ではTIMESTAMP型に変更し、デフォルト値に実行時のTIMESTAMPを指定しています。

データベースによりSQLの文法が変わってくるので、環境間で異なるデータベースを使うのは極力避けましょうってことですね。

参考

[H2] http://www.h2database.com/html/grammar.html#alter_table_add
[PostgreSQL] https://www.postgresql.jp/document/9.4/html/sql-altertable.html

Kindle(無印)が3,980円という衝撃(4/2まで)

いやはや、久々の衝動買いですわ。。

Amazonが現在タイムセール中(明日4/2まで!)なのですが、なんとKindle(無印)が5,000円引きで3,980円というのです。
※プライム会員限定です。

Amazon.co.jp: Kindleクーポンキャンペーン: Kindleストア

Kindleには他にもPaperwhiteなど上位機種があるのですが、比較検討も程々にポチってしまいました。
私はiPdaAir2も所持しており、KindleアプリはiPhoneやiPadAir2で利用しているので特に新しいことができるわけでもないのですが。

Kindleオーナーライブラリで月1冊無料で読める

Kindle端末を持つことで新たにできることといえば、Kindleオーナーライブラリが利用できるということ。
これもプライム会員であることが条件になりますが、Kindle端末上で月1冊無料で読むことができるのです。
端末代3,980円ならこれだけで元が取れるというもの。

タイムセールは明日4/2までなので、迷ってる方はお早めに!

Kindle Paperwhite Wi-Fi、ブラック

Kindle Paperwhite Wi-Fi、ブラック

Paperwhiteは5,800円引きの8,480円。

Paperwhiteのマンガモデルは6,300円引きの9,980円。