LoginSignup
2
3

More than 3 years have passed since last update.

Rails6のActionTextでransackがうまく使えないので検索できるようにする

Posted at

これはなに?

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構成

blogstitleと言うカラムしかもってなくて、ブログのコンテンツであるbodyaction_text_rich_textsが管理してます。

なので、bodyblogsはカラムとして保持していないためメソッドが生成されてなくて、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を定義してクエリで解決することにしました。

app/models/blog.rb
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_idrecord_typeのそれぞれ2つです。それぞれ結合先のモデル名と該当レコードのIDですね。

ViewとControllerはとてもシンプルです

app/controllers/blogs_controller.rb
class BlogsController < ApplicationController
  def index
    @blogs = Blog.search(params["q"])
  end
end

app/views/blogs/index.html.erb
<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する形でもいいかと思います。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3