ActiveModel::Serializersの痒い所に手が届く使い方

# はじめに
ここ半年ぐらい`ActiveModel::Serializers`を使った開発をしていて
痒い所に手が届く書き方をいくつか見つけたので紹介していきます
基本的な使い方はすでにいろんな方の記事があるので省略します

# 1. キー名を変えたい
- `id`
- `first_name`
- `last_name`
- `employee_number`

といったカラムを持つ`User`モデルがあったときに
`first_name`を`name`として出力したい場合

### a. `:key`オプションを使う
```ruby
class UserSerializer < ActiveModel::Serializer
  attributes :id, :employee_number
  attribute :first_name, key: :name
end
```

### b. メソッドを定義する
```ruby
class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :employee_number

  def name
    object.first_name
  end
end
```

### c. ブロックで渡す
```ruby
class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :employee_number

  attribute :name do
    object.first_name
  end
end
```

※objectはSerializer`new`などで渡されたオブジェクト(レコード)自身

- `attribute`だけでなく`belongs_to` `has_one` `has_many`でも同様の使い方が可能
- メソッドやブロックは処理も書けるので分岐や加工がしやすくて便利

# 2. `has_many`で紐づくモデルをSerializerの中でorderするとSQLキャッシュのN+1が起きる問題をなんとかしたい
```ruby:before
class User < ActiveRecord::Base
  has_many :posts
end

class Post < ActiveRecord::Base
  belongs_to :user
end

class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :employee_number
  has_many :posts, serializer: PostSerializer do
    object.posts.order(created_at: :desc)
  end
end
```
と書くと事前に`preload`などをしてN+1対策をしていても
`CollectionSerializer`で大量に出力する際にSQLキャッシュのN+1が発生します
ログの中に`CACHE (0.0ms)  SELECT ....`みたいなのが大量に出て
キャッシュされているとはいえやはり遅くなる

そんな時の対策はこちら

### a. モデルのアソシエーションにデフォルトのオーダーを仕込む
```ruby:after
class User < ActiveRecord::Base
  has_many :posts, -> { order(created_at: :desc) }
end

class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :employee_number
  has_many :posts, serializer: PostSerializer
end
```
としてやると最初にオーダーされてキャッシュのN+1が発生しません

ただ、これだと他のところでこのorderが効いて欲しくない時があるかも
そんな時はモデルにもう一個別のアソシエーションを定義します

### b. モデルにオーダー用のアソシエーションを定義する
```ruby:after
class User < ActiveRecord::Base
  has_many :posts
  has_many :for_order_posts, -> { order(created_at: :desc) }, class_name: :Post
end

class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :employee_number
  has_many :for_order_posts, serializer: PostSerializer
end
```

もちろん`preload`でのアソシエーションも`:for_order_posts`に直します
個人的にはこれが好きです

# 3. `has_many`のアソシーションの最新1件を含めたいけどこれまたSQLキャッシュのN+1が起きる問題
```ruby:before
class User < ActiveRecord::Base
  has_many :posts
end

class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :employee_number
  attribute :newest_post, serializer: PostSerializer do
    object.posts.order(updated_at: :desc).limit(1)
  end
end
```
ってやるとやっぱりSQLキャッシュのN+1が発生するので
やっぱりここも別のアソシエーションを定義します

### a. `has_one`と`scope`をモデル側に作る
```ruby:after
class User < ActiveRecord::Base
  has_many :posts
  has_one :newest_post, -> { order(updated_at: :desc) }, class_name: :Post
end

class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :employee_number
  attribute :newest_post, serializer: PostSerializer
end
```

`has_one`なので勝手に`limit(1)`がかかります

# 4. `attribute`で出力するか否かの条件をつけたい
### a. `:if`オプションを使う
```ruby
class UserSerializer < ActiveModel::Serializer
  attributes :id, :name, :employee_number
  attribute :name, if: -> { object.has_attribute?(:name) }
end
```
ってやると`name`カラムが存在するときだけ出力してくれたり

ポリモーフィックで
`User`モデルが`ChildBoy`と`ChildGirl`というモデルを`Child`という名前で持っていた時に
`ChildBoy`の時は`ChildGirl`を出さない、みたいな使い方もできます

# 5. おまけ(入れ子になったSerializerのテストコード)
当たり前かもしれませんが自分はいつも入れ子になったSerializerのrspecを書く時はいつもこうやってます

```ruby
it 'serializes User' do
  expect(UserSerializer.new(user).to_json).to be_json_including(
    id: user.id,
    first_name: user.first_name,
    last_name: user.last_name,
    posts: user.posts.map do |post|
      JSON.parse(PostSerializer.new(post).to_json)
    end
  )
end
```

もちろん`PostSerializer`がテストされている前提です

`PostSerializer.new(post).to_hash`でもいいんですが
jsonに変換した時に日時のフォーマットが変わっちゃったりするので`JSON.parse`してます