yield_selfを使ってActiveRecordのRelation chainをいい感じに書く

# tl;dr

- ActiveRecordで動的にRelation chainが変わるような場合を考える
 - ユーザ入力によってfilterやorderが変わったりするよくあるケース
- `yield_self`を使ってchainをスマートに書く

# Using `yield_self` for composable ActiveRecord relations

元ネタはこちらの記事です。ほぼこれの紹介といってよい。
https://robots.thoughtbot.com/using-yieldself-for-composable-activerecord-relations

# `yield_self`について

`yield_self`はRuby2.5で追加されたmethodで`tap`と同じようなユースケースで使用されるけど、`tap`と違って`yield`の戻り値がそのまま`yield_self`の戻り値となるところが違います。
これをRelation chainに使うことでいい感じに書けるという話です。

これだけだとよくわからんので、サンプルコードで考えてみます。

## Model

ブログサイトで考えてみます。
User(ユーザ)、Article(記事)、Tag(記事につけるタグ)の3つのModelにします。
migrationファイルは以下みたいな。

```ruby:create_users.rb
class CreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :name, null: false

      t.timestamps
    end
  end
end
```

```ruby:create_articles.rb
class CreateArticles < ActiveRecord::Migration[5.2]
  def change
    create_table :articles do |t|
      t.string :title, null: false
      t.text :body, null: false
      t.references :author, foreign_key: {to_table: :users}, null: false

      t.timestamps
    end
  end
end
```

```ruby:create_tags.rb
class CreateTags < ActiveRecord::Migration[5.2]
  def change
    create_table :tags do |t|
      t.string :name, null: false
    end
  end
end
```

以下がN:Mのマッピングテーブル。よくある感じです。

```ruby:create_article_tags.rb
class CreateArticleTags < ActiveRecord::Migration[5.2]
  def change
    create_table :article_tags do |t|
      t.references :article, null: false
      t.references :tag, null: false
      t.datetime :created_at, null: false

      t.index :article, :tag, unique: true
    end
  end
end
```

## FormObject

ブログ記事の一覧ページ用のFormObjectを以下のように考えます。

```ruby:article_form.rb
class ArticleForm
  include ActiveModel::Model

  attr_reader :title, :order
  attr_accessor :tag_ids

  def initialize(_attributes = {})
    super
    @tag_ids ||= []
  end
end
```

記事のタイトル(title)、タグ(tag_ids)による検索と表示順(order)を指定できるとします。

# 安直なRelation chain

なにも考えないやつです。
ここからリファクタしていく想定。

```ruby
class QueryBuilder
  delegate :title, :tag_ids, :order, to: :@article_form

  def initialize(article_form)
    @article_form = article_form
  end

  def query
    relation = base_relation

    relation = relation.where(title: title) if title.present?
    relation = relation.where(tags: { id: tag_ids }) if tag_ids.present?
    relation = relation.order(created_at: order) if order.present?

    relation
  end

  private

  def base_relation
    Article.includes(:author, :tags)
  end
end
```

これぐらいならまだよいかもしれませんが、もっと複雑に検索条件があるような場合は`query`に分岐をひたすら足していくことになってつらくなってきます。
`query`内で`relation`に都度代入しなおしているのも気になりますね。

## `tap`を使ってみる

手始めに`query`を`tap`を使ってリファクタしてみます。

```ruby
def query
  base_relation.tap do |relation|
    relation.merge! Article.where(title: title) if title.present?
    relation.merge! Article.where(tags: { id: tag_ids }) if tag_ids.present?
    relation.merge! Article.order(created_at: order) if order.present?
  end
end
```

代入の代わりに`merge!`が登場してきました。
まあ少しはよくなったかも(?)ってぐらいでしょうか。
`yield_self`にいく前に上記から`where`、`order`の羅列をmethodに分解してみます。

```ruby
class QueryBuilder
  delegate :title, :tag_ids, :order, to: :@article_form

  def initialize(article_form)
    @article_form = article_form
  end

  def query
    base_relation.tap do |relation|
      relation.merge! matching_title if title.present?
      relation.merge! matching_tags if tag_ids.present?
      relation.merge! order_created_at if order.present?
    end
  end

  private

  def base_relation
    Article.includes(:author, :tags)
  end

  def matching_title
    Article.where(title: title)
  end

  def matching_tags
    Article.where(tags: { id: tag_ids } )
  end

  def order_created_at
    Article.order(create_at: order)
  end
end
```

`merge!`で破壊的なのと、せっかく`matching_xxx`とか分割したのにif文は`query`にあるのが気になりますね。
`tap`が自分自身を返す影響がでているので、ここまでくると`yield_self`の使い所がみえてきます。

## `yield_self`版

```ruby
class QueryBuilder
  delegate :title, :tag_ids, :order, to: :@article_form

  def initialize(article_form)
    @article_form = article_form
  end

  def query
    base_relation
      .yield_self(&method(:matching_title))
      .yield_self(&method(:matching_tags))
      .yield_self(&method(:order_created_at))
  end

  private

  def base_relation
    Article.includes(:author, :tags)
  end

  def matching_title(relation)
    return relation unless title.present?
    relation.where(title: title)
  end

  def matching_tags(relation)
    return relation unless tag_ids.present?
    relation.where(tags: { id: tag_ids } )
  end

  def order_create_at(relation)
    return relation unless created_at.present?
    relation.order(create_at: order)
  end
end
```

一気にやっちゃいましたが、`query`がmethod chainになってだいぶみやすくなりました。

`yield_self`のmethod chainのところをみていきましょう。
`method`は動的method呼び出しのためにmethodのSymbol表記を渡すと`Method`オブジェクトを返します。
`&`をつけることで`Proc`に変換して`yield_self`に渡します。
`yield_self`にすることで前の`yield`の返り値が各methodの引数となります。
これで`merge!`を使う必要がなくなるので、FormObject(ArticleForm)の値による分岐を分割したmethod内にいれることができます。

おまけで`inject`を使うとloopにできます。(あんまり読み易いとはいえないですが)

```ruby
def query
  %i[matching_title matching_tags order_create_at].inject(base_relation){|relation, matcher|
    relation.yield_self(&method(matcher))
  }
end
```