Railsチュートリアル 第14章 ユーザーをフォローする - ステータスフィード - フィードを初めて実装する

# フィードに必要となるSQLクエリと、対応する`where`メソッドの引数

要件は以下です。

* 対象ユーザーがフォローしているユーザーのユーザーidを持つマイクロポストを全て選択する
* かつ、対象ユーザー自身のマイクロポストも全て選択する

以上の要件を満たす最小限のSQLクエリを模式的に書くと、以下のようになります。

```sql:
SELECT * FROM microposts
WHERE user_id IN (<list of ids>) OR user_id = <user id>
```

「ログインしているユーザーがフォローしているユーザーのid」というのは、多くの場合複数となります。ゆえに、それらをクエリ問い合わせに使う場合、単一の値ではなく集合として与えなければなりません。そのような場合に使うSQLのキーワードが`IN`となります。

Active Recordでは、`where`メソッドを使うことによって、SQL文の`WHERE`以下に相当する検索条件を与えることができます。上記のSQL文に対応する`where`メソッドの引数の与え方は以下のようになります。

```ruby:
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
```

`following_ids`の内容は、「ユーザーidをカンマ区切りで列挙したもの」となります。となると、次に問題となるのは、「カンマ区切りでユーザーidを列挙したものを文字列で得るためにはどうすればいいか」ということになりますね。

## 参考…自身のマイクロポスト全てを取得するためのSQL文と、`where`メソッドの引数の与え方

「対象ユーザー自身のマイクロポストを全て選択する」という要件を満たす最小限のクエリは、以下のような内容になります。

```SQL:
SELECT * FROM microposts
WHERE user_id = <user id>
```

上記のSQL文に対応する`where`メソッドの引数の与え方は以下のようになります。

```ruby:
Micropost.where("user_id = ?", id)
```

# ユーザーidの列挙を、カンマ区切りの文字列として得る方法

Rubyにおいては、「オブジェクトの列挙から、各オブジェクトの特定の属性値を文字列化し、その結果の列挙を配列として得る」という操作は、「当該オブジェクトの列挙に対して、ブロック内で`to_s`メソッドを呼び出す形で`map`メソッドを実行する」という処理を行うことによって実現できます。

```irb:
>> [1, 2, 3, 4].map { |i| i.to_s}
=> ["1", "2", "3", "4"]
```

上記の`map`メソッドの呼び出しでは、各要素に対して`to_s`メソッドが実行されています。`map`メソッド（や`each`メソッド等）に与えるブロックにおいて、内部の処理が「単一のメソッドを実行する」というものである場合、「`map`メソッド（や`each`メソッド等）の引数として、ブロック中で実行するメソッドのシンボルを`&`に続けて記述したものを与える」という省略表記を使うことが可能です。

```irb:
>> [1, 2, 3, 4].map(&:to_s)      
=> ["1", "2", "3", "4"]
```

上記結果に対して`join`メソッドを使うと、以下のように、idの集合をカンマ区切りで繋げた文字列を得ることができます。

```ruby:
>> [1, 2, 3, 4].map(&:to_s).join(", ")
=> "1, 2, 3, 4"
```

# 実際に、`User`オブジェクトの列挙からidの集合をカンマ区切りの文字列として得てみる

データベースの最初のユーザーを対象として、フォローしている全ユーザーのidを配列として得てみます。

```irb:
User.first.following.map(&:id)
=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
```

このような呼び出しは、実際のアプリケーションでも頻繁に行われます。そのため、Active Recordでは次のようなメソッドも用意されています[^following_idsのメソッド名]。

```irb:
>> User.first.following_ids
=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
```

ここまでで得られたのはあくまで配列です。最終的には、得られた配列を「カンマ区切りの文字列」に変換しなければなりません。お目当ての出力を得るためには、`join(', ')`をメソッドチェーンの最後につないでやる必要がある、ということですね。

```irb:
>> User.first.following_ids.join(', ')
=> "3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51"
```

これで「カンマ区切りでユーザーidを列挙したものを文字列で得る」という出力結果を得ることができました。

[^following_idsのメソッド名]: 当該メソッド名は、Active Recordで使用しているモデル名そのものが含まれています。ライブラリ作成時にモデル名がわかるはずがないので、「黒魔術」ことメタプログラミングの手法を使わなければ実現できないタイプの実装といえますね。

# `where`メソッドの引数内で`following_ids`メソッドを使う

`user.following_ids.join(', ')`というのは、あくまで説明用のコードであり、実際のRailsアプリケーションでこのようなコードを使うことはありません。実際のRailsアプリケーションにおいて、`where`メソッドの引数内で`following_ids`メソッドを使う場合、特に引数を取ることなく`following_ids`と書けばいいのです。`where`の第一引数で正しく`?`が使われていれば、それで「カンマ区切りでユーザーidを列挙したものを含めたSQLクエリを文字列で得る」ことができるのです。RDBMS依存の非互換性の一部も含め、全てはRailsがよしなにしてくれます。

```ruby:
Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
```

# `app/models/user.rb`の実装を変更する

ここまでの内容を踏まえ、実際に`app/models/user.rb`に加える変更の内容は以下のようになります。

```diff:app/models/user.rb
  class User < ApplicationRecord
    ...略

    # 試作feedの定義
    # 完全な実装は次章の「ユーザーをフォローする」を参照
    def feed
-     Micropost.where("user_id = ?", id)
+     Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
    end

    ...略
  end
```

# `test/models/user_test.rb`に対し、改めてテストを実行する

ここまでの実装を反映すれば、`test/models/user_test.rb`に対するテストが通るようになります。

```
# rails test test/models/user_test.rb
Running via Spring preloader in process 178
Started with run options --seed 54454

  15/15: [=================================] 100% Time: 00:00:01, Time: 00:00:01

Finished in 1.41146s
15 tests, 64 assertions, 0 failures, 0 errors, 0 skips
```

* ログインユーザー自身のマイクロポストがステータスフィードに含まれていること
* フォローしているユーザーのマイクロポストがステータスフィードに含まれていること
* フォローしていないユーザーのマイクロポストがステータスフィードに含まれていないこと

以上の機能が実現されている、ということになりますね！