つぶやきアプリを作ってみよう⑫

つぶやき検索機能を実装します。

投稿が増えてくるとみたい投稿を探すのが大変です。昨日
「検索したテキストを含む投稿」を表示させられると便利なので、検索機能を実装しましょう。

今回はsearchという命名で、7つの基本アクション以外のアクションを定義します。

ここでcollectionとmemberを学習しておきましょう。

##collectionとmember
collectionとmemberは、ルーティングを設定する際に使用できます。
これを使用すると、生成されるルーティングのURLと実行されるコントローラーを任意にカスタムできます。

collectionはルーティングに:idがつかない、memberは:idがつくという違いがあります。
1つずつ使用例を見ていきましょう。

```rb:【例】collectionで定義した場合

Rails.application.routes.draw do
  resources :tweets do
    collection do
      get 'search'
    end
  end
end
```

```rb:例】collectionのルーティング
Prefix           Verb    URI                                 Pattern
search_tweets    GET    /tweets/search(.:format)              tweets#search
```

ルーティングに:idが付いていないことがわかります。
続いてmemberの場合を見てみましょう。

```rb:【例】memberで定義した場合
Rails.application.routes.draw do
  resources :tweets do
    member do
      get 'search'
    end
  end
end
```

```rb:【例】memberのルーティング
Prefix           Verb    URI                                 Pattern
search_tweet      GET    /tweets/:id/search(.:format)       tweets#search
```

URLの指定先が、collectionは:idなし、memberが:idありとなっていることが確認できます。
今回の検索機能の場合は:idを指定して特定のページにいく必要がないので
collectionを使用してルーティングを設定しましょう。

```rb:config/routes.rb

Rails.application.routes.draw do
  devise_for :users
  root to: 'tweets#index'
  resources :tweets do
    resources :comments, only: :create
    collection do
      get 'search'
    end
  end
  resources :users, only: :show
end
```

次にビューを編集します。

```erb:app/views/tweets/index.html.erb

<%= form_with(url: search_tweets_path, local: true, method: :get, class: "top_contents") do |form| %>
  <%= form.text_field :keyword, placeholder: "投稿を検索する", class: "post" %>
  <%= form.submit "検索", class: "search-btn", class: "post"%>
<% end %>
<div class="contents row">
  <% @tweets.each do |tweet| %>
<%= render partial: "tweet", locals: { tweet: tweet } %>
  <% end %>
```

次にTweetモデルに、検索する処理を記述したメソッドを定義します。
メソッド名はsearchメソッドとします。
検索したキーワードが含まれている投稿を取得するために、whereメソッドとLIKE句を利用します。

##whereメソッド
モデルが使用できる、ActiveRecordメソッドの1つで
引数部分に条件を指定することで、テーブル内の「条件に一致したレコードのインスタンス」を配列の形で取得できます。
引数の条件には、「検索対象となるカラム」を必ず含めて、条件式を記述します。

```rb:【例】whereメソッド
モデル.where('検索対象となるカラムを含む条件式')
```

##LIKE句
曖昧な文字列の検索をするときに使用するもので、whereメソッドと一緒に使います。

|文字列|意味|
|---|---|
|%|任意の文字列（空白文字列含む）|
|_|任意の1文字|

|実行例|詳細|
|---|---|
|where('title LIKE(?)', "a%")|aから始まるタイトル|
|where('title LIKE(?)', "%b")|bで終わるタイトル|
|where('title LIKE(?)', "%c%")|cが含まれるタイトル|
|where('title LIKE(?)', "d_")|dで始まる2文字のタイトル|
|where('title LIKE(?)', "_e")|eで終わる2文字のタイトル|


whereメソッドとLIKE句を使用して検索の処理を作ります。

```rb:app/models/tweet.rb
class Tweet < ApplicationRecord
  has_one_attached :image
  
  validates :text, presence: true, unless: :was_attached?
  def was_attached?
    self.image.attached?
  end

  validates :text, presence: true
  belongs_to :user
  has_many :comments

  def self.search(search)
    if search != ""
      Tweet.where('text LIKE(?)', "%#{search}%")
    else
      Tweet.all
    end
  end
end
```

引数のsearchは、検索フォームから送信されたパラメーターが入るため、
if search != ""と記述し、検索フォームに何か値が入力されていた場合を条件としています。

検索フォームに何も入力をせずにボタンを押すと、引数で渡されるsearchの中身は空になり
elseに該当し、Tweet.allという記述はすべての投稿を取得して表示するためのものです。

ビジネスロジックは基本的にモデルに置きます
実際の開発現場でも、テーブルとのやりとりに関するメソッドはモデルに置くという意識が必要です。
コントローラーはあくまでモデルの機能を利用し処理を呼ぶだけで、複雑な処理は組みません。

##tweetsコントローラーを編集しましょう

```rb:app/controllers/tweets_controller.rb
class TweetsController < ApplicationController
  before_action :set_tweet, only: [:edit, :show]
  before_action :move_to_index, except: [:index, :show, :search]

  def index
    @tweets = Tweet.includes(:user).order("created_at DESC")
  end

  def new
    @tweet = Tweet.new
  end

  def create
    Tweet.create(tweet_params)
  end

  def destroy
    tweet = Tweet.find(params[:id])
    tweet.destroy
  end

  def edit
  end

  def update
    tweet = Tweet.find(params[:id])
    tweet.update(tweet_params)
  end

  def show
    @comment = Comment.new
    @comments = @tweet.comments.includes(:user)
  end

  def search
    @tweets = Tweet.search(params[:keyword])
  end

  private
  def tweet_params
    params.require(:tweet).permit(:image, :text).merge(user_id: current_user.id)
  end

  def set_tweet
    @tweet = Tweet.find(params[:id])
  end

  def move_to_index
    unless user_signed_in?
      redirect_to action: :index
    end
  end
end
```

Tweetモデルに書いたsearchメソッドを呼び出しています。
searchメソッドの引数にparams[:keyword]と記述して、検索結果を渡しています。

未ログイン時に検索をするとトップページへリダイレクトされてしまうのを回避するために、
before_actionのexceptオプションに:searchを追加しています。


検索結果を表示できるようにsearch.html.erbを作成しましょう
app/views/tweetsディレクトリの配下にsearch.html.erbを作成します。

```erb:app/views/tweets/search.html.erb

<%= form_with(url: search_tweets_path, local: true, method: :get, class: "top_contents") do |form| %>
  <%= form.text_field :keyword, placeholder: "投稿を検索する", class: "post" %>
  <%= form.submit "検索", class: "post" %>
<% end %>
<div class="contents row">
  <% @tweets.each do |tweet| %>
    <%= render partial: "tweet", locals: { tweet: tweet } %>
  <% end %>
</div>
```
これで検索機能は完成です。