Railsで検索機能を自作で実装する方法

# はじめに

以前書いた、下記の続き。

[Railsのroutingにおけるscope / namespace / module の違い \- Qiita](https://qiita.com/ryosuketter/items/9240d8c2561b5989f049)

今回は、gemを使わず簡単な検索機能を自作する方法について書きます。

# 環境

- macOS HighSierra　10.13.6
- Rails 5.2.0
- Ruby 2.5.1
- mysql 5.7.22
  - mysqlはdockerに作成
- Docker version 18.03.1-ce

# 検索機能の概要

- フォームに検索キーワード（文字列）を入力
- フォームの検索キーワード（文字列）をController → Modelにパラメータとして渡す
- パラメータとして渡って来た文字列をMySQLであいまい検索（`SELECT ??? FROM table WHERE LIKE '%文字列%'`）をかける
- 検索結果をViewに表示

# View側の実装

まず、検索できるフォームをViewに設置します。

```erb:app/views/top/index.html.erb
・・・
<h2>検索</h2>
<%= form_tag(root_path, method: :get) do %>
  <%= text_field_tag :search %>
  <%= submit_tag 'Search', name: nil %>
<% end %>
・・・
```

1つづつ説明して行きます。

form_tagとは、ActionView::Helpersを継承した、簡単に検索入力フォームを作成できるヘルパーの1つです。form_tagは２つ引数を取っていて、検索結果を表示するURL(今回は名前付きルートの指定)と、パラメータとして送るためにHTTPメソッド（GET）を指定しています。

text_field_tagもヘルパーの1つです。以下のHTMLをレンダリングしています。

```html
<input type="text" name="search" id="search">
```

submit_tagもヘルパーで、これは入力ボタンです。nameは識別子のようなものなので、複数のsubmitボタンがない限り`nil`でいいと思います。

参考: [Railsで１フォームに複数のサブミット\(Submit\)ボタンを配置するTips \- Rails Webook](https://ruby-rails.hatenadiary.com/entry/20141225/1419515057)

## この時点で検索して見ると

まだ、絞り込みはできないのですが、入力フォームに「test」と入力してSubmitすると、

![スクリーンショット 2019-03-24 06.52.34.png](https://qiita-image-store.s3.amazonaws.com/0/14191/73024754-f790-da0e-5631-a98d5ec7053b.png)

の様に出力され、ログには下記の様に出力されます。

```bash
Started GET "/?utf8=%E2%9C%93&search=test" for 127.0.0.1 at 2019-03-24 06:52:27 +0900
Processing by TopController#index as HTML
  Parameters: {"utf8"=>"✓", "search"=>"test"}
  Rendering top/index.html.erb within layouts/application
  Article Load (3.8ms)  SELECT `articles`.* FROM `articles`
  ↳ app/views/top/index.html.erb:10
  Rendered top/index.html.erb within layouts/application (15.6ms)
Completed 200 OK in 40ms (Views: 32.6ms | ActiveRecord: 3.8ms)
```

これを見ると、指定した通り`search`という名前で`test`という文字列をGETパラメータで渡していることがわかります。今度はControllerやModelで受け取って処理して見ましょう。

# Controller側の実装

`.search(params[:search])`を追記しました。これによって、Viewのformで取得したパラメータをModelに渡せます。

```rb:app/controllers/top_controller.rb
class TopController < ApplicationController
  def index
    @articles = Article.all.search(params[:search])
  end
end
```

`.search`メソッドは自作メソッドです。このままだとエラー（`NoMethodError`）になります。なので、Modelにメソッドを書きます。

# Model側の実装

```rb:app/models/article.rb
class Article < ApplicationRecord
  def self.search(search)
    search ? where('title LIKE ?', "%#{search}%") : all
  end
end
```

自分自身の値を返すので、クラスメソッドです。

では、試しに、フォームに「title 1」と検索してみましょう。

<a href="https://gyazo.com/ef81abe0958c288631c16d5b4488aa5c"><img src="https://i.gyazo.com/ef81abe0958c288631c16d5b4488aa5c.gif" alt="Image from Gyazo" width="314"/></a>

これによって発行されるSQLは、ログから見ることができます。

```sql
SELECT `articles`.* FROM `articles` WHERE (title LIKE '%title 1%')
```

SQLを見るとtitleカラムから特定の文字列`title 1`が含まれるデータのあいまい検索（ワイルドカードを用いた部分一致検索）を行ったことが分かります。

## Railsのセキュリティ対策の話

DB操作の際にSQLを自前で組み立てたりするとSQLインジェクションの可能性があります。

例えば、下記の様に書くことも可能なのですが、

```rb:app/models/article.rb
class Article < ApplicationRecord
  def self.search(search)
    search ? where("title='#{search}'") : all
  end
end
```

この場合、検索フォームに

```
'); select * from articles;--('
```

と書いてsubmitした場合吐き出されるログは、下記の様になります。

```sql
SELECT `articles`.* FROM `articles` WHERE (title='');
select * from articles;
--('')
```

上を、MySQL上で実行すると、2行目のSELECT文は普通に実行されてしまいます。そうなると、`DROP TABLE articles;`（テーブル削除コマンド）という、やられては困るコマンドも実行される可能性があるということです。

そこで、

`where('title LIKE ?', "%#{search}%")`

`?`というSQLプレースホルダーという機能を利用することで、SQL構文を決定した後に引数の文字列展開などが起こるので、結果的に、引数`search`からのデータがエスケープされ、SQLインジェクションを防ぐ、という方法が慣習になっているそうです。

今回は、以上です！

# 参考

- [【Rails入門】form\_tagの使い方まとめ \| 侍エンジニア塾ブログ（Samurai Blog） \- プログラミング入門者向けサイト](https://www.sejuku.net/blog/29083#form_tagform_for)
- [Railsでセキュリティのことを考えずに作ったwebアプリは脆弱性があるのか調べてみた\(脆弱性検証編\) \- 男女比はカレーと福神漬けと同じくらい](http://euglena1215.hatenablog.jp/entry/2016/09/22/171850)
- [SQLインジェクションを考慮しつつ、selectするカラムを動的に変更する \- Qiita](https://qiita.com/jnchito/items/e3a144b07f578cda5ee2)