これはなに?
Rails6で導入されたActionTextにより簡単にブログ機能を実装できるようになりました。
大変ありがたいお話なのですが、検索機能を定番のgemであるransackを使って実装しようとしたらエラーになり使えませんでした。
その時の対処法を記載します。
どうすればいいのか?
結論から言うと、ransack使わないですね。タイトルと若干逸脱してる気もしますが、動けばいいと思います。
手順
こっから具体的な解決までのプロセスです。
事象
まずransack使うとどうなるかって話ですね。下記のようなエラーになります。
Completed 500 Internal Server Error in 42ms (ActiveRecord: 1.4ms | Allocations: 5041)
05:42:11 web.1 |
05:42:11 web.1 |
05:42:11 web.1 |
05:42:11 web.1 | ActionView::Template::Error (undefined method `body_cont' for #<Ransack::Search:0x00007fc354bc9ac0>):
05:42:11 web.1 | 1: <h1>Blogs</h1>
05:42:11 web.1 | 2:
05:42:11 web.1 | 3: <%= search_form_for @q do |f| %>
05:42:11 web.1 | 4: <%= f.search_field :body_cont %>
05:42:11 web.1 | 5:
05:42:11 web.1 | 6: <%= f.submit class: "btn btn-outline-primary" %>
05:42:11 web.1 | 7: <% end %>
undefined method
で500エラーですね。とても残念です。
因みに、title_cont
だとうまくいきました。
原因調査
まず、該当モデルの構成はこんな感じです。title_cont
だとうまく検索できたので、ActionText使ってるとうまく検索できないんだろうなというのは容易に想像できます。
class Blog < ApplicationRecord
belongs_to :user
has_many :comments
has_many :favorites
has_rich_text :body
validates :title, presence: true
validates :body, presence: true
end
DB構成
blogs
はtitle
と言うカラムしかもってなくて、ブログのコンテンツであるbody
はaction_text_rich_texts
が管理してます。
なので、body
をblogs
はカラムとして保持していないためメソッドが生成されてなくて、undefined method
で落ちていたのでした。
create_table "action_text_rich_texts", force: :cascade do |t|
t.string "name", null: false
t.text "body"
t.string "record_type", null: false
t.bigint "record_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true
end
create_table "blogs", force: :cascade do |t|
t.string "title"
t.bigint "user_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["user_id"], name: "index_blogs_on_user_id"
end
解決方法
原因がわかったので、内部結合していい感じにransackで取得できないかと思い、READMEを見てみる。
んー、なんかなさそう。頑張ればいけるのか。頑張りたくないからgem使ってるんだが。
Google先生に聞いてみよう
gemの使い方を熟読するのは面倒だったのでstackoverflowを見てみると、ドンピシャの質問がありました。
淡白すぎない?
しかも掲示してるコードだと動かないですね。ガッデム。
自分で頑張ろう
調べてもransackでの解決方法が分からなかったので、scopeを定義してクエリで解決することにしました。
class Blog < ApplicationRecord
belongs_to :user
has_many :comments
has_many :favorites
has_rich_text :body
validates :title, presence: true
validates :body, presence: true
# このscopeです
scope :search, -> (search_param = nil) {
return if search_param.blank?
joins("INNER JOIN action_text_rich_texts ON action_text_rich_texts.record_id = blogs.id AND action_text_rich_texts.record_type = 'Blog'")
.where("action_text_rich_texts.body LIKE ? OR blogs.title LIKE ? ", "%#{search_param}%", "%#{search_param}%")
}
end
ちょっと詳しく解説すると、action_text_rich_texts
と内部結合してbody
の中身をLIKE句で検索してます。
結合条件は、record_id
とrecord_type
のそれぞれ2つです。それぞれ結合先のモデル名と該当レコードのIDですね。
ViewとControllerはとてもシンプルです
class BlogsController < ApplicationController
def index
@blogs = Blog.search(params["q"])
end
end
<h1>Blogs</h1>
<%= form_tag blogs_path, method: :get do %>
<%= text_field_tag :q %>
<%= submit_tag "Search", class: "btn btn-outline-info btn-sm" %>
<% end %>
<% @blogs.each do |blog| %>
<div class="my-3">
<h2>
<%= link_to blog.title, blog %>
<small class="pl-1">
by <em><%= blog.user.name %></em>
</small>
</h2>
</div>
<% end %>
動かしてみる
でっきるかなあ? でっきるかなあ?
Started GET "/blogs?q=world&commit=Search" for 172.21.0.1 at 2020-05-16 06:47:08 +0000
06:47:08 web.1 | Processing by BlogsController#index as HTML
06:47:08 web.1 | Parameters: {"q"=>"world", "commit"=>"Search"}
06:47:08 web.1 | User Load (1.7ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["id", 1], ["LIMIT", 1]]
06:47:08 web.1 | Rendering blogs/index.html.erb within layouts/application
06:47:08 web.1 | Blog Load (2.8ms) SELECT "blogs".* FROM "blogs" INNER JOIN action_text_rich_texts ON action_text_rich_texts.record_id = blogs.id AND action_text_rich_texts.record_type = 'Blog' WHERE (action_text_rich_texts.body LIKE '%world%' OR blogs.title LIKE '%world%' )
06:47:08 web.1 | ↳ app/views/blogs/index.html.erb:9
06:47:08 web.1 | User Load (1.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
06:47:08 web.1 | ↳ app/views/blogs/index.html.erb:14
06:47:08 web.1 | Rendered blogs/index.html.erb within layouts/application (Duration: 39.4ms | Allocations: 1560)
06:47:08 web.1 | Completed 200 OK in 100ms (Views: 79.1ms | ActiveRecord: 5.9ms | Allocations: 4093)
できたっぽい
まとめ
シンプルにテーブルのカラムを条件に検索したいならransack使うのが良さげですが、ちょっと複雑な条件での検索は難しそうです。僕がちゃんと検索できてないだけな気もしますが。
ransackだとformから世話してくれるので、titleだけの検索なら速攻で実装できて快適でした。
でもまあ、自前で検索機能作ってもそんなに手間じゃないので、Module化して各モデルでincludeする形でもいいかと思います。