【Ruby on Rails】paramsについて理解しながら検索機能を作ろう

# 【Ruby on Rails】paramsについて理解しながら検索機能を作ろう

## 目次
[1.はじめに](#1-はじめに)
[2.前提・テンプレ作成](#2-前提テンプレ作成)
[3.詳細画面の表示](#3-詳細画面の表示)
[4.検索機能の実装](#4-検索機能の実装)
[5.まとめ](#5-まとめ)

## 1. はじめに

本記事は、DMM WEBCAMP Advent Calendar 2023 6日目記事です。
DWCメンター・卒業生が記事を投稿しておりますので、是非他の記事もご覧ください！

### 執筆背景

Ruby on Railsを学習中に、よく`Post.find(params[:id])`みたいな記述を見かけると思います。
この`params`という記述は、ページ遷移やデータを作成する時に用いられ、さらにはタグによる絞り込み機能や検索機能にも登場します。

paramsがいまいち理解できない、活用方法が思い浮かばないという方に向けて、本記事を執筆いたしました。

### 目的
- paramsを理解する
- paramsを用いた検索機能を実装する

## 2. 前提・テンプレ作成

### 開発環境
- ruby 3.1.2
- rails 6.1.4

### モデル

モデル名はUserで、nameカラムを持っています。
![ER.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577430/8978e6ec-4903-d88d-6797-30aef18bddcc.png)

### コントローラ

```ruby:users_controller.rb
class UsersController < ApplicationController
  def index
    @users = User.all
  end
  
  def show
    @user = User.find(params[:id])
  end
end

...
```

### ルーティング

resourcesを使って次のようにします。
```ruby:routes.rb
Rails.application.routes.draw do
    resources :users
end
```

この記述で生成されるルーティングを確認しておきましょう。

```terminal:terminal
rails routes
```
こちらが出力された結果です。
```
       Prefix Verb   URI Pattern               Controller#Action
        users GET    /users(.:format)          users#index
              POST   /users(.:format)          users#create
     new_user GET    /users/new(.:format)      users#new
    edit_user GET    /users/:id/edit(.:format) users#edit
         user GET    /users/:id(.:format)      users#show
              PATCH  /users/:id(.:format)      users#update
              PUT    /users/:id(.:format)      users#update
              DELETE /users/:id(.:format)      users#destroy
...
```

<details><summary>この画面は結局どう見ればいいのか？という疑問を持つ方へ</summary>

- Verb
> どんな動作（リクエスト）をさせるかのかを決める。
> コンテンツを取得する（GET）やデータを送信する（POST）など。
- URI Pattern
> URLに似たようなもの。
> 東京都/港区/芝公園/４丁目/２-８みたいにインターネット上の住所を示す。
- Controller#Action
> どのコントローラのどのアクションが動くかを示す。
- Prefix
> URLなどにあだ名をつける。
> 東京都/港区/芝公園/４丁目/２-８と書かれても、で、結局どこの住所なの？長くて覚えられないよ。となってしまうので、`「東京タワー」の住所`と、あだ名をつけておく。
> 使用時には「あだ名」+ _path と書けるため、今回で言えば`users_path`などになる。

</details>



### view

一覧画面

```html:users/index.html.erb
<h1> 一覧画面 </h1>

<div>
    <h2>ユーザー一覧</h2>
    <% @users.each do |user| %>
        <div>
            <%= user.id %>人目: <%= link_to user.name, user_path(user.id) %>
        </div>
    <% end %>
</div>

...
```

詳細画面

```html:users/show.html.erb
<h1> 詳細画面 </h1>

<div>
    <h2>ユーザー詳細</h2>
    <div>
        <%= @user.id %>人目: <%= @user.name %>
    </div>

    <div>
      <%= link_to '一覧画面へ', users_path %>
    </div>
</div>

...
```

Userのデータは別途作成をお願いします。
面倒な方はデータを挿入することができるseedsを活用することをお勧めします。
<details><summary>seeds.rbを利用したデータの挿入</summary>

db/seeds.rbに以下の記述をした後に、記述した内容を読み込むコマンドを打ちます。

```ruby:db/seeds.rb
User.create([
  { name: '田中一郎' },
  { name: '田中次郎' },
  { name: '山中一郎' },
  { name: '山中次郎' }
])
```

```terminal:terminal
rails db:seed
```

</details>

### 画面確認

ここまでで問題がなければ、一覧画面に幾つか投稿がされていると思います。

![index-show.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577430/e8bf51ad-6925-1ff5-d2f5-cbaa580c340a.gif)


 
 それぞれの名前をクリックすると、詳細画面(users/ユーザーの番号)に飛ぶようになっています。
 また、例えばURLに直接`users/1`や`users/2`などを打ち込んでも画面遷移ができると思います。

:::note warn
そもそもUserが数名登録されていないといけません。
例えば5人目のユーザーがいない状態で`/users/5`にアクセスすると、エラーになります。

<details><summary>レコードがない場合、こんなエラーがでます。</summary>

![SS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577430/a215455f-3f46-713d-3326-53a16eae6689.png)


これは、`@user = User.find(params[:id])`の箇所でエラーが出ていて、具体的には`Couldn't find User with 'id'=5`と怒られています。
つまり、「Userに対して、idが5のものは見つけれらなかった」という内容です。

</details>
:::

## 3. 詳細画面の表示

さて、ここから画面遷移の流れを追ってみましょう。
今回、一覧画面（/users）から、詳細画面（/users/:id）に遷移しています。

例えば、一覧画面からuserの名前をクリックしたり、URLに直接`users/1`や`users/2`などを打ち込むと、ターミナルにこのような結果が出ていると思います。

```
Started GET "/users/1" for ::1 at 2023-11-08 22:08:48 +0900
Processing by UsersController#show as HTML
  Parameters: {"id"=>"1"}
  ...
```

ここに書かれている処理をなるべく日本語で追ってみます。


- Started GET /users/1
    - HTTPメソッドがGETで、URI（URL）が/users/1 という場所にアクセスした
- Processing by users#show
    - users_controllerのshowアクションという場所を動かします
- Parameters: {"id"=>"1"}
    - パラメータとして、合言葉(キー)を「id」として、値(バリュー)が「1」というのが送られた

今回は詳細画面（show.html.erb）に遷移していたので、users_controllerのshowアクションがきちんと動いていることも確認できました。

一度ルーティングの結果を思い出しながら、URLとログに残っているコントローラとアクションが対応しているかどうかを確認しておくといいでしょう。下のようになっているはずです。

- /users の時は、users#index
- /users/1やusers/2の時は、users#show

その時のターミナルに表示されているログとして、parametersも確認しましょう。
- `Started GET /users/1`の時には、`Parameters: {"id"=>"1"}`
- `Started GET /users/2`の時には、`Parameters: {"id"=>"2"}`

つまりルーティングで表示されていた`/users/:id`のうち、`:id`の部分が1や2の数字に置き換わっていることや、その数字がParametersで送られてきていることが確認できます。

さて、`users_controller#show`（users_controllerのshowアクションの意）に以下の記述がありました。

```ruby:users_controller.rb
...
def show
    @user = User.find(params[:id])
end
...
```

この記述を括弧の中身から見ていきます。
- `params(キー)`
>  パラメータの中から、合言葉（キー）で値（バリュー）を取り出す。
- `find( ID )`
> IDを指定してレコードを取得
今回はUserの中から、主キー（出席番号みたいに順番に割り振られている数字）を元に探す。

今回受け取っているparamsが、ターミナルを見て{id => 1}となっていれば、`params[:id]`という記述で得られる値は「1」になります。

それがfindメソッドに与えられているので、user.find(1)となり、1番目のユーザーを探すことになります。
見つけたユーザーが`@user`に渡され、さらにviewに渡され、詳細画面にユーザーが表示されます。

:::note warn
裏を返せば、いつどんな時でもparams[キー]が使えるわけではなく、あくまでパラメータが送られてくる時に限られます。

<details><summary>パラメータが送られてきていない時にparams[:id]を使うとこんなエラーになります。</summary>

とりあえず、indexアクションの時にはパラメータが送られてこない（後述）ので、indexアクションでparams[:id]を試してみましょう。

実際に一覧画面に遷移した時のターミナルを追ってみても、Parameters: {"id"=>"1"}なんて記述は見当たらないと思います。

```diff_ruby:users_controller.rb
class UsersController < ApplicationController
  def index
-   @users = User.all
+   @user = User.find(params[:id])
  end

...
```

上記のように書き換えたうえで一覧画面の`users`にアクセスすると下のようなエラー画面が出ます。

![スクリーンショット 2023-11-06 0.14.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577430/ae18ac20-3ea2-c6e9-11ee-a6c51cfc28af.png)

先ほどの、id=5のUserが見つからないエラーと似ていますが、微妙に違います。
`@users` = User.find(params[:id])の箇所で
Couldn't find User without an ID と書かれているので、「ID無しでUserは見つけられないよ」というエラーが出ています。

前述した通り、find(　)では、カッコの中に1や2などのIDを欲しています。
しかし、今回はそもそもパラメータが送られてきていないので、`params[:id]`が空っぽになってしまいます。
そのため、User.find(空っぽ)となってしまい結局誰を探せばいいのか分からないため、「ID無しでUserは見つけられないよ」というエラーが出ています。
</details>

:::

ここまで詳細画面表示におけるparamsについて流れを追ってみました。詳細画面表示時の`params[:id]`が何をしているのかイメージできたら幸いです。

## 4. 検索機能の実装

さて、paramsについて理解が深まったところで、検索機能に取り組んでいきます。
検索機能といっても難しいことはせずに、あくまでparamsの応用として実装に挑戦しましょう。

その前に、パラメータを２種類挙げます。
- パスパラメータ
> ルーティングに、`:id`のようなものが入っているURLにアクセスした時に送られてくるパラメータのこと。
今回であれば`/users/:id`という設定で、`users/1`にアクセスした時に、パスパラメータとして`{id => 1}`が送られてきます。
また、一覧画面のURLは`/users`であり、URLに`:id`が含まれていないため、このパスパラメータというのが送られてきません。
- クエリパラメータ
> URLに?qみたいな文字が入っている場合に送られてくるパラメータのこと。
youtubeなどで検索すると、`https://www.youtube.com/results?search_query=プログラミング`みたいなURLになります。

　今回作る検索機能では、パスパラメータでも実装できますがクエリパラメータで実装していきます。
なぜかというと、パスパラメータで作成する場合には`users/:serch_word`のような、ルーティングを新たに作り、viewやcontroller、actionも別途作らなければならないためです。

対してクエリパラメータの場合は、既存の`/users`というURLをそのまま活用することができます。

では、実際にクエリパラメータの送受信をしてみましょう。
まずは受け取り側です。

```diff_ruby:users_controller.rb
class usersController < ApplicationController

  def index
-     @users = User.all

+    q = params[:search_word]
+    if q.present?
+      @users = User.where(name: q)
+    else
+      @users = User.all 
+    end
  end

...
```

- `present?`
>  存在しているかを判断するメソッドです。
そもそもパラメータが送られてきていない時のために用意する必要があります。
- `where(カラム名: 条件)`
> データの中から条件にマッチしたものを全て抽出するメソッドです。
今回はUserデータの中から、nameカラムと検索ワードが一致しているものを探しています。

コントローラーの記述の説明をします。
params[キー]で実際に送られてきた値をqという変数に一度置いてあげて、
- もしqに中身があれば、Userデータの中から、nameカラムの中身とqが一致しているユーザだけを抽出して、それを`@users`としてviewに渡す。
- qが空っぽであれば、そのままユーザ全員を`@users`としてviewに渡す。

これで、paramsを使った検索機能完了になります。


```diff_html:index.html.erb
<h1> 一覧画面 </h1>
+ <div>
+   <h2>検索</h2>
+   <%= form_with url: users_path, method: :get do |f| %>
+     <%= f.text_field :search_word %>
+     <%= f.submit '検索' %>
+   <% end %>
+ </div>

<div>
    <h2>ユーザー一覧</h2>
    <% @users.each do |user| %>
        <div>
            <%= user.id %>人目: <%= link_to user.name, user_path(user.id) %>
        </div>
    <% end %>
</div>
```

今加筆したのは、`:search_word`というキーワードを、users_pathという場所に, HTTPメソッドをgetで送ります。という内容です。
まずは適当に名前を入れてみて検索してみたりしてみましょう。また、先ほどのパスパラメータの確認同様に、ターミナルを見てみましょう。

![search-test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3577430/e653eb62-29c3-5169-2537-a6517697d959.gif)

注目するところは、URLとターミナルのパラメータの部分です。
URLを見てみると、/usersの後ろに`search_word=田中一郎&commit=検索`というものがくっついています。
これで、あくまでメインのURLは/usersだけど、検索ワードがURLにくっついている、という状態になります。

さらに、ターミナルから、パラメータが送られていることも確認しましょう。
ターミナルの中から、下記のようなものを見つけられましたか？
```
Parameters: {"search_word"=>"鈴木一郎", "commit"=>"検索"}
...省略...
Parameters: {"search_word"=>"", "commit"=>"検索"}
...省略...
Parameters: {"search_word"=>"田中一郎", "commit"=>"検索"}
```
先ほどのパスパラメータの時と同様に、パラメータが送られてきていることが確認できました。

パラメータが送られてきているのであれば、あとはparamsで受け取ってあげましょう。
今一度コントローラの記述を見返して、パラメータを受け取っている流れを確認しましょう。

## 5. まとめ
本記事ではRuby on Railsにおけるparamsに焦点を当て、パラメータの受け渡しの流れを追うことで、検索機能の実装を行いました。

しかし現状の検索機能として、名前と検索文字が完全一致しているデータしか検索できません。
例えば、以下のような一部分だけに着目した検索はparamsとは別にもう一工夫する必要があります。
- 名前が「田中」から始まるユーザー
- 名前が「一郎」で終わるユーザー
- 名前に「中」が含まれているユーザー

検索機能の幅を効かせるためにもぜひ取り組んでみてください！