Rails6 のちょい足しな新機能を試す102（ActiveRecord Type Casting編）

# はじめに

Rails 6 に追加された新機能を試す第102段。 今回は、 ActiveRecord Type Casting編です。
Rails 6 では、 データベースの connection adapter の Type Casting のバグが修正されました。

Ruby 2.6.5, Rails 6.0.0, Rails 5.2.3 で確認しました。
データベースは、 PostgreSQL 10.7 を使っています。

```shell
$ rails --version
Rails 6.0.0
```

今回は、ちょっと複雑で長くなります。

モデルが、 Author, Publisher, Book の3つがあります。
Author は、Book を複数持ちます。
Publisher は、 Book を複数持ちます。

Author の 一覧画面と詳細画面を作ります。
Author の 詳細画面には、 Author が書いた本の出版社の一覧も表示します。

# Rails プロジェクトを作る

```
$ rails new rails_sandbox
cd rails_sandbox
```

# Author モデルを作る

```shell
$ bin/rails g model Author name
```

# Publisher モデルを作る

```shell
$ bin/rails g model Publisher name
```

# Book モデルを作る

Book モデルは、Author と Publisher に属します。

```shell
$ bin/rails g model Book title author:references publisher:references
```

# Author モデルを変更する

Author モデルを変更します。
`has_many` を追加します。
`to_param` メソッドを定義します。(今回の事象を確認するために必要なメソッドです。)

```ruby:app/models/author.rb
class Author < ApplicationRecord
  has_many :books

  def to_param
    "#{id}-#{name.parameterize}"
  end
end
```

# Publisher モデルを変更する

Publisher モデルを変更します。
`has_many` を追加します。
scope を1つ追加し、Author の id から、紐づく出版社を取得します。

```ruby:app/models/publisher.rb
class Publisher < ApplicationRecord
  has_many :books

  scope :with_author, ->(author_id) { distinct.joins(books: :author).where(authors: { id: author_id }) }
end
```

この scope の中の、 `joins(books: :author).where(authors: { id: author_id  })`  の部分が今回のバグ修正の箇所になります。

なお、普通はこんなことしないで、 Author に以下の `has_many` を追加するのが通常だと思われます。

```ruby:app/models/author.rb
class Author < ApplicationRecord
  ...
  has_many :publishers, ->{ distinct }, through: :books
  ...
end
```

# seed データを作成する

seed データを作成します。今回は、最低限、必要なデータだけ用意します。

```ruby:db/seeds.rb
pragmatic, addison = Publisher.create(
  [
    { name: 'Pragmatic Bookshelf' },
    { name: 'Addison-Wesley Professional' }
  ]
)

author = Author.create(name: 'Dave Thomas')

Book.create(
  [
    {
      title: 'Agile Web Development with Rails 6',
      publisher: pragmatic,
      author: author
    },
    {
      title: 'Agile Web Development with Rails 5.1',
      publisher: pragmatic,
      author: author
    },
    {
      title: 'Pragmatic Programmer',
      publisher: addison,
      author: author
    }
  ]
)
```

# ルーティングを追加する

ルーティングとして、 author の一覧 (index) と詳細 (show) を追加します。

```ruby:config/routes.rb
Rails.application.routes.draw do
  resources :authors, only: %i[index show]
end
```

# AuthorsController を追加する

AuthorsController を追加します。
`show` メソッドでは、Publisher も取得するようにします。

```ruby:app/controllers/authors_controller.rb
class AuthorsController < ApplicationController
  def index
    @authors = Author.all
  end

  def show
    @author = Author.find(params[:id])
    @publishers = Publisher.with_author(params[:id])
  end
end
```

ここで、 `@publishers` を取得するときに、 `Publisher.with_author(@author.id)` としていないところに注意してください。
そうしないと、今回のバグ修正が確認できません。


# Author の一覧画面を作る

Author の一覧画面の View を作ります。

```erb:app/views/authors/index.html.erb
<h1>Author</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
    </tr>
  </thead>
  <tbody>
    <% @authors.each do |user| %>
      <tr>
        <td><%= link_to user.name, user %></td>
      </tr>
    <% end %>
  </tbody>
</table>
```

ここで、詳細画面のリンクを作成するために、 `link_to user.name, user` としているところがポイントになります。


# Author の詳細画面を作成する

出版社を一緒に表示します。

```erb:app/views/authors/show.html.erb
<h1>Author: <%= @author.name %> </h1>

<table>
  <thead>
    <tr>
      <th>Publisher Name</th>
    </tr>
  </thead>
  <tbody>
    <% @publishers.each do |publisher| %>
      <tr>
        <td><%= publisher.name %></td>
      </tr>
    <% end %>
  </tbody>
</table>
```

# seed データを登録する

データベースを作成し、マイグレーションを実行して seed データを登録します。

```ruby
bin/rails db:create db:migrate db:seed
```

# 動作確認

rails server を実行して、 http://localhost:3000/authors にアクセスし、 `Dave Thomas` のリンクをクリックして詳細画面を表示します。
詳細画面を表示したときの URL が http://localhost:3000/authors/1-dave-thomas となっていることに注意してください。

# Rails 5 では

詳細画面を表示したときに、 `ActiveRecord::StatementInvalid` のエラー画面が表示されます。

```
PG::InvalidTextRepresentation: ERROR:  invalid input syntax for integer: "1-dave-thomas"
: SELECT DISTINCT "publishers".* FROM "publishers" INNER JOIN "books" ON "books"."publisher_id" = "publishers"."id" INNER JOIN "authors" ON "authors"."id" = "books"."author_id" WHERE "authors"."id" = $1
```

何が発生しているかというと、

```ruby
Publisher.with_author('1-dave-thomas')
```

がエラーになっています。

scope を使わずに、要点を抜き出して書くと

```ruby
Publisher.joins(books: :author).where(authors: {id: '1-dave-thomas'})
```

を実行したときに、 `authors.id` の比較条件が `1-dave-thomas` という文字列のため、エラーになっています。

Rails 6 では、 `1-dave-thomas` が authros.id の型 (integer) に合わせて `1` に変換される (Type Casting) ように修正されたため、エラーになりません。

# 試したソース

試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try102_activerecord_typecastiong

# 参考情報

* [Fall back to type casting from the connection adapter](https://github.com/rails/rails/pull/36314)