form objectで検索フォームを実装する。（複数単語、or 検索）


## 何をしたか
`form object`で検索フォームを実装しました。
**「赤い　りんご」**のように、空白で区切られた検索ワードが入力された時には、空白で区切って複数単語のOR検索にしました。

実装環境は以下の通りです。

- `Rails 5.2.3`
- `Ruby 2.6.0`

## まずは１単語の検索フォームを作る

今回作るのは、投稿`post`の検索フォームです。

まずはシンプルに、１単語の検索フォーム（フォームオブジェクト**ではないもの**）から作り始めました。

実際に書いたコードは以下の通りです。（装飾等に使用したclassは省いています。以下同。）

```ruby:/controllers/posts_controller.rb
def search
  @posts = Post.where('body LIKE ?', "%#{params[:body]}%")
end
```
```ruby:/views/posts/index_html.slim
= form_with url: search_posts_path, method: :get, local: true do |f|
 = f.search_field :body
 = f.submit

 # ページ下方で = render @posts をして、検索結果を呼び出しています
```
非常にシンプルですね:relaxed:

この辺の基本的なコードの読み解き方は、以下の記事にまとめておりますので、よろしければご覧ください。

[Railsの検索メソッドの基礎（whereでLIKE句を使う）](https://qiita.com/tanutanu/items/74b0760137fe7d6e134a)

## form objectにする
では、こちらのコードを`form object`にしていきます。

```ruby:/controllers/posts_controller.rb
def search
  @posts = SearchForm.new(search_post_params)
end

# 中略

def search_post_params
  params.fetch(:q, '').permit(:body) # 解説します
end
```
```ruby:/views/posts/index_html.slim
= form_with form_with model: search_form, url: search_posts_path, scope: :q, method: :get, local: true do |f|
 = f.search_field :body  # ↑上記、解説します 
 = f.submit
```

```Ruby:/forms/search_form.rb
class SearchForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :body, :string

  def search
    Post.includes(:user, :images).where('body LIKE ?', "%#{body}%")
  end
end
```
`form object`にすることで、検索ロジックをフォームオブジェクトに移動できました:relaxed:

### 解説：`scope: :q`と`fetch(:q, '')`について

ところで、viewにあった`scope: :q`が謎ですよね。（↓この部分です）

```ruby:
= form_with form_with model: search_form, url: search_posts_path, scope: :q, method: :get, local: true do |f|
```
これは、「送るパラメーターをどのハッシュでまとめるのか」を指定している部分で、

**▼`scope: :q`がない時のparams**

```shell

Processing by PostsController#search as HTML
  Parameters: {"utf8"=>"✓", "body"=>"りんご", "commit"=>"SEARCH"}
Unpermitted parameters: :utf8, :commit
```

**▼`scope: :q`が　ある時のparams**

```sh
Processing by PostsController#search as HTML
  Parameters: {"utf8"=>"✓", "q"=>{"body"=>"りんご"}, "commit"=>"SEARCH"}
```
のように、`scope: :q`の記述によって、検索単語を`"q"=>{ ... }`の中にまとめることができます。

キーワードを入力するフォールドが一つの検索フォームだとほとんど意味がありませんが、入力するフィールド数が２つ、３つにになってくると、

```sh:
"q"=>{ "フィールド名"=>"検索単語", "フィールド名"=>"検索単語", ... }
```
の形で`params`がまとまってくれるので、便利です。:relaxed:

また、先ほどは`controller`の`strong paramater`に以下のように設定していましたが...

```ruby:controller
def search_post_params
  params.fetch(:q, '').permit(:body)
end
```

これによって、`params`の中から、`"q"=>{ ... }`の部分だけを取り出しています。なお、`fetch(:q, '')`の`''`の部分は、キーである`:q`がなかった時のデフォルト値です。

▼`fetch`の詳しい説明はこちら
[instance method Hash#fetch](https://docs.ruby-lang.org/ja/2.2.0/method/Hash/i/fetch.html)

## 複数単語検索にする

それでは、これを複数単語のor検索に変えていきます。

色々やり方はありましたが、今回は[こちらの記事と、そのコメント](https://qiita.com/Orangina1050/items/ca4e5dc26e0a32ee3137)を参考に、以下のように実装しました。

```ruby:/forms/search_form.rb
class SearchForm

  # 変わっていないので中略

  def search
    keywords = body.split(/[[:blank:]]+/)

    @posts = Post.none

    keywords.each do |keyword|
      @posts = @posts.or(Post.where('body LIKE ?', "%#{keyword}%"))
    end

    return @posts
  end
end
```

何点かピックアップして解説します。

- `.split(/[[:blank:]]+/)`...検索ワードを空白で複数単語に分け、配列に入れます。
- `Post.none`...空の**Active Record**を作ります。空の配列を作る`[]`のActive Record版です。
- `@posts.or(...)`...詳細下述します。

### 解説：`@posts.or(...)`について

この部分については、記述が複雑になるのでピックアップします。`Rails`の`or`メソッドは、データベースへの複数検索の`or`を実行するメソッドです。

例えば、以下のコードで

```ruby
Fruit.where(name: 'バナナ').or(Fruit.where(name: 'グレープ'))
```

以下のようなSQLが発行されます。(サンプルのコードは[こちら](https://techacademy.jp/magazine/24839)からお借りしました)

```sql:
Fruit Load (1.5ms) SELECT `fruits`.* FROM `fruits` WHERE (`fruits`.`name` = 'バナナ' OR `fruits`.`name` = 'グレープ')
```

実装に用いたコードでは、このように書くことで、

```ruby:
@posts = Post.none

keywords.each do |keyword|
  @posts = @posts.or(Post.where('body LIKE ?', "%#{keyword}%"))
end

return @posts
```

`keyword`の数だけ

```Ruby:
Post.where(body: 'keyword').or(Post.where(body: 'keyword').or(Post.where(...)))
```

を繰り返し、複数語検索を実現しています。

実際にはリファクタリングを繰り返し、上記`or`のロジックは消えてしまったのですが、面白い書き方だったので記事にしてとっておくことにしました:relaxed: