LoginSignup
3
0

More than 3 years have passed since last update.

Rails6 のちょい足しな新機能を試す93(ActiveRecord extract_associated編)

Posted at

はじめに

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 を追加します。

app/models/author.rb
class Author < ApplicationRecord
  has_many :books, dependent: :destroy
end

seed データを作る

seed データを作ります。

db/seeds.rb
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 rubytitle に "Rails" がつく Book を検索する scope rails を追加します。(共通の処理を with_title で別の scope として切り出しました。)

app/models/book.rb
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 を追加します。

app/models/author.rb
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

参考情報

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0