はじめに
Rails 6 に追加された新機能を試す第93段。 今回は、 ActiveRecord extract_associated
編です。
Rails 6 では、 preload(xxx).collect(xxx)
の short hand である extract_associated
が追加されました。
Ruby 2.6.4, Rails 6.0.0 で確認しました。
$ rails --version
Rails 6.0.0
プロジェクトを作る
$ rails new rails_sandbox
$ cd rails_sandbox
今回は Author モデル と Book モデルを作って rails console で確認してみます。
Author モデルを作る
name
の属性を持つ Author
モデルを作ります。
$ bin/rails g model Author name
Book モデルを作る
$ bin/rails g model Book title author:references
Author モデルを変更する
has_many
を追加します。
class Author < ApplicationRecord
has_many :books, dependent: :destroy
end
seed データを作る
seed データを作ります。
Author.create(
[
{
name: 'Dave Thomas',
books: Book.create(
[
{ title: 'Pragmatic Programmer' },
{ title: 'Programming Ruby' },
{ title: 'Agile Web Development with Rails 6' },
{ title: 'Agile Web Development with Rails 5.1' }
]
)
},
{
name: 'Yukihiro Matsumoto',
books: Book.create(
[
{ title: 'The Ruby Programming Language' }
]
)
},
{
name: 'Mark Lutz',
books: Book.create(
[
{ title: 'Programming Python' }
]
)
}
]
)
Book モデルを変更する
Book モデルに scope を追加します。
title
に "Ruby" がつく Book を検索する scope ruby
と title
に "Rails" がつく Book を検索する scope rails
を追加します。(共通の処理を with_title
で別の scope として切り出しました。)
class Book < ApplicationRecord
belongs_to :author
scope :with_title, ->(title) { where('title like ?', "%#{title}%") }
scope :ruby, -> { with_title('Ruby') }
scope :rails, -> { with_title('Rails') }
end
rails console を実行する
rails console を使って確認してみます。
Rubyの本を書いた著者を検索してみます。 extract_associated
を使ってみます。
irb(main):001:0> Book.ruby.extract_associated(:author)
Book Load (0.4ms) SELECT "books".* FROM "books" WHERE (title like '%Ruby%')
Author Load (0.5ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" IN ($1, $2) [["id", 10], ["id", 11]]
=> [#<Author id: 10, name: "Dave Thomas", created_at: "2019-09-27 05:13:39", updated_at: "2019-09-27 05:13:39">, #<Author id: 11, name: "Yukihiro Matsumoto", created_at: "2019-09-27 05:13:39", updated_at: "2019-09-27 05:13:39">]
Dave Thomas と Yukihiro Matsumoto の2人が検索できました。
Rails の本を書いた著者を検索してみます。
irb(main):002:0> Book.rails.extract_associated(:author)
Book Load (1.0ms) SELECT "books".* FROM "books" WHERE (title like '%Rails%')
Author Load (0.6ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = $1 [["id", 10]]
=> [#<Author id: 10, name: "Dave Thomas", created_at: "2019-09-27 05:13:39", updated_at: "2019-09-27 05:13:39">, #<Author id: 10, name: "Dave Thomas", created_at: "2019-09-27 05:13:39", updated_at: "2019-09-27 05:13:39">]
Dave Thomas のレコードが2件になりました。
distinct
が使えると良いのですが、 extract_associated
は、 Array を返すのでそうはいかないです。
rb(main):003:0> Book.rails.extract_associated(:author).distinct
Book Load (0.9ms) SELECT "books".* FROM "books" WHERE (title like '%Rails%')
Author Load (0.6ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = $1 [["id", 10]]
Traceback (most recent call last):
1: from (irb):3
NoMethodError (undefined method `distinct' for #<Array:0x000055e16560a1c0>)
uniq
を使えば、1件にはなります。
irb(main):004:0> Book.rails.extract_associated(:author).uniq
Book Load (1.0ms) SELECT "books".* FROM "books" WHERE (title like '%Rails%')
Author Load (0.6ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = $1 [["id", 10]]
=> [#<Author id: 10, name: "Dave Thomas", created_at: "2019-09-27 05:13:39", updated_at: "2019-09-27 05:13:39">]
ですが、Array として全部メモリに読み込まれてから、 uniq
で処理することになるのが少し気になるところです。(データが多くなるとメモリ不足になりそう。)
別の方法
extract_associated
を試すという目的からは、少しずれてしまいますが、別の方法も考えてみます。
Author に scope を追加する
Author に以下のように scope を追加します。
class Author < ApplicationRecord
has_many :books, dependent: :destroy
scope :of_books_with, ->(title) { joins(:books).where('books.title like ?', "%#{title}%") }
scope :of_ruby_books, -> { of_books_with('Ruby') }
scope :of_rails_books, -> { of_books_with('Rails') }
end
rails console で試してみます。今度は、distinct が使えます。
irb(main):019:0> Author.of_rails_books.distinct
Author Load (1.2ms) SELECT DISTINCT "authors".* FROM "authors" INNER JOIN "books" ON "books"."author_id" = "authors"."id" WHERE (books.title like '%Rails%') LIMIT $1 [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Author id: 10, name: "Dave Thomas", created_at: "2019-09-27 05:13:39", updated_at: "2019-09-27 05:13:39">]>
Book で絞り込んでから Author を検索する
Book で絞り込んでから、author_id を求め、その条件で、Author を検索します。
irb(main):020:0> Author.where(id: Book.rails.select(:author_id))
Author Load (1.2ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" IN (SELECT "books"."author_id" FROM "books" WHERE (title like '%Rails%')) LIMIT $1 [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Author id: 10, name: "Dave Thomas", created_at: "2019-09-27 05:13:39", updated_at: "2019-09-27 05:13:39">]>
実行されるSQLに DISTINCT
は含まれませんが、検索結果は、Dave Thomas が1人です。
試したソース
試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try093_extract_associated