はじめに
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 を使っています。
$ 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 モデルを作る
$ bin/rails g model Author name
Publisher モデルを作る
$ bin/rails g model Publisher name
Book モデルを作る
Book モデルは、Author と Publisher に属します。
$ bin/rails g model Book title author:references publisher:references
Author モデルを変更する
Author モデルを変更します。
has_many
を追加します。
to_param
メソッドを定義します。(今回の事象を確認するために必要なメソッドです。)
class Author < ApplicationRecord
has_many :books
def to_param
"#{id}-#{name.parameterize}"
end
end
Publisher モデルを変更する
Publisher モデルを変更します。
has_many
を追加します。
scope を1つ追加し、Author の id から、紐づく出版社を取得します。
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
を追加するのが通常だと思われます。
class Author < ApplicationRecord
...
has_many :publishers, ->{ distinct }, through: :books
...
end
seed データを作成する
seed データを作成します。今回は、最低限、必要なデータだけ用意します。
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) を追加します。
Rails.application.routes.draw do
resources :authors, only: %i[index show]
end
AuthorsController を追加する
AuthorsController を追加します。
show
メソッドでは、Publisher も取得するようにします。
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 を作ります。
<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 の詳細画面を作成する
出版社を一緒に表示します。
<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 データを登録します。
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
何が発生しているかというと、
Publisher.with_author('1-dave-thomas')
がエラーになっています。
scope を使わずに、要点を抜き出して書くと
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