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

# これはなに？
Rails6で導入されたActionTextにより簡単にブログ機能を実装できるようになりました。
大変ありがたいお話なのですが、検索機能を定番のgemであるransackを使って実装しようとしたらエラーになり使えませんでした。

その時の対処法を記載します。

# どうすればいいのか？
結論から言うと、**ransack使わない**ですね。タイトルと若干逸脱してる気もしますが、動けばいいと思います。

# 手順
こっから具体的な解決までのプロセスです。

## 事象
まずransack使うとどうなるかって話ですね。下記のようなエラーになります。

```shell
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使ってるとうまく検索できないんだろうなというのは容易に想像できます。

```ruby
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`で落ちていたのでした。

```ruby
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を見てみる。

https://github.com/activerecord-hackery/ransack

んー、なんかなさそう。頑張ればいけるのか。頑張りたくないからgem使ってるんだが。

### Google先生に聞いてみよう
gemの使い方を熟読するのは面倒だったのでstackoverflowを見てみると、ドンピシャの質問がありました。

https://stackoverflow.com/questions/59316606/how-ransack-can-search-for-rails6-action-text-content/60696169#60696169

**淡白すぎない?**
しかも掲示してるコードだと動かないですね。ガッデム。

### 自分で頑張ろう
調べてもransackでの解決方法が分からなかったので、scopeを定義してクエリで解決することにしました。

```ruby: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_id`と`record_type`のそれぞれ2つです。それぞれ結合先のモデル名と該当レコードのIDですね。

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

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

```

```erb: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 %>
```

## 動かしてみる
でっきるかなあ？ でっきるかなあ?

```shell
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する形でもいいかと思います。