はじめに
こちらは バイセルテクノロジーズ Advent Calendar 2022 の 9 日目の記事です。前日の記事は藤井さんの「WebComponentで始めるUIコンポーネントの共通化」でした。
こんにちは。バイセルテクノロジーズ テクノロジー戦略本部に所属している稲川です。
私が所属しているプロジェクトでは Ruby on Rails (以下、Rails) を使って プロダクトを開発しています。
開発しているプロダクトではお客様の予約スケジュールを管理する機能があります。
この機能に関するテーブル構造が複雑で、データを取得するのに苦労するのですが Rails の eager_load を scope で使うことにより楽に一括取得することができました。
今回お話しするRailsのscopeやeager_loadは、web上でいくつかの使用方法が見受けられます。 ですが、業務上で使用する実際のテーブル構造(複雑なテーブル構造)を想定した使い方を紹介する 具体的な参考事例が少ない印象です。
本記事がRailsを使用しているエンジニアの中でも、 複雑なテーブル構造に対してscopeやeager_loadの使い方に困っているエンジニアの参考になれば幸いです。
要件
今回は従業員の稼働日の予定も考慮してお客様が予約をとることをシステムの要件とします。
ER図と一括取得したい情報は以下の通りです。
一括取得したい情報
- 従業員(Staff)の一覧を取得
- 従業員の稼働日(WorkingDate)を日付指定で取得
- 対応可能な従業員がいる場所(Place)を取得
- 従業員が担当している予約(Appointment)を取得
- 予約に関連するステータス(Status)、種別(Type)、区分(Classification)を取得
実装と確認結果
以下の方法で場所のIDと日付を引数としてscopeにて予約日付を判断するのに必要な情報を 取得できるように実装しました。
class WorkingDate scope :select_schedules, lambda { |place_ids, dates| # 一括取得した後で予約に必要な情報を都度、取得するとN+1問題が発生するためeager_loadにて必要最低限の情報を取得 eager_load(:staff, :place, { appointments: [{ appointment_working_dates: :classification }, :status, :type] }, { appointment_working_dates: :classification } ).where(date: dates) .joins(:place, :staff) # 関連データ取得の際、必要最低限の条件で絞り込むためjoinsを使用 .where( places: { id: place_ids }, staffs: { active: true } ) } end
実装したscopeをrails console上で確認
全体件数と意図したデータが取得できていることを確認します。 また、実行計画も確認しました。
(以下、ローカルデータで検証した結果となります。)
全体件数の確認結果
> WorkingDate.select_schedules([1, 2], ["2022-12-24", "2022-12-25"]).count => 75
データ取得の確認結果
> WorkingDate.select_schedules([1, 2], ["2022-12-24", "2022-12-25"]) [#<WorkingDate:0x00007fa7bc13b760 id: "20221201_24_2_22_NjMjODjijT_3sMfPaj8oWBrkyZkQoC_PlkQQDKzaqpM", staff_id: 24, place_id: 2, date: Sat, 24 Dec 2022, created_at: Thu, 01 Dec 2022 10:24:49 JST +09:00, updated_at: Thu, 01 Dec 2022 10:24:49 JST +09:00>, #<WorkingDate:0x00007fa7bc13a720 id: "20221201_1_1_22_NjMjODjijT_3sMfPaj8oWBrkyZkQoC_PlkQQDKzaqpM", staff_id: 1, place_id: 1, date: Sat, 24 Dec 2022, created_at: Thu, 01 Dec 2022 10:24:49 JST +09:00, updated_at: Thu, 01 Dec 2022 10:24:49 JST +09:00>, #<WorkingDate:0x00007fa7bc139758 id: "20221201_22_2_22_NjMjODjijT_3sMfPaj8oWBrkyZkQoC_PlkQQDKzaqpM", staff_id: 22, place_id: 2, date: Sat, 24 Dec 2022, created_at: Thu, 01 Dec 2022 10:24:49 JST +09:00, updated_at: Thu, 01 Dec 2022 10:24:49 JST +09:00>, ・ ・ ・ データ量が多いため、割愛します。
実行計画の確認結果
eager_loadを使う時のメリット、デメリットについて
eager_loadの仕様は LEFT_OUTER_JOINで指定したデータを結合して関連テーブルの配列を取得しキャッシュします。
また、メリット、デメリットの詳細は以下となります。
メリット
- belongs_to、has_oneなどの関連データを取得する際に有効
- 1回のSQLでまとめて取得した方が効率的な場合な場合には有効
- 関連先のデータを条件して絞り込みを行う場合には有効
- クエリの数が1個で実行されるため、場合によってはpreloadより速い
- 取得した関連先のデータをLEFT JOINでキャッシュ可能
デメリット
- クエリが1つになるためデータ量が多い場合には読み込み時のメモリを多く消費する
- has_manyの関連データを読み込むのには不向きなのでpreloadの方が有効
eager_loadを調べていく中で、preloadに関しても学んだので preloadを使うときのメリットとデメリットについても記載させていただきます。
preloadを使った時のメリット、デメリットについて
preloadの仕様は 指定した関連データ毎に複数のクエリに分けて関連テーブルの配列を取得しキャッシュします。
また、メリット、デメリットの詳細は以下となります。
メリット
- has_manyの関連データの場合、eager_loadよりもpreloadの方がSQLを分割して取得するため有効
- 複数のassociationをeager loadingするときとか、あまりJOINしたくないデータ量の多いテーブルを扱うのに有効
- 取得した関連先のデータをキャッシュ可能
デメリット
- 関連先の条件を指定してpreloadができない
- has_manyの関連先を読み込むのには不向き
所感
今回のscopeの実装で複雑な関連データを取得する方法を学ぶことができました。 また、eager_loadとpreloadの違いを学ぶ良い機会になりました。
今後は関連データをキャッシュして取得する必要がある場合には has_manyの場合はeager_loadを使い belongs_toやhas_oneの場合は preloadを使うようにしたいと思います。
参考にした文献
https://tech.stmn.co.jp/entry/2020/11/30/145159 https://qiita.com/k0kubun/items/80c5a5494f53bb88dc58 https://qiita.com/ryosuketter/items/097556841ec8e1b2940f
まとめ
いかがでしたでしょうか。
これまでもRailsのscopeを使ってテーブルのデータを抽出する方法で 基本的なscopeの使い方は他のwebサイトでもよく見かけることがありました。 これらを組み合わせてscopeを組み立てると 複雑なテーブルの一括処理ができるようになりました。
また、今回はscopeの中でincludesを使うこともできたのですが 実装したscopeをリリースした後の処理パフォーマンスを考慮し 関連データの取得も必要最低限にするためあえてeager_loadを使用して 情報を取得することにしました。
Railsにてシステムを組み立てて任意のテーブルとそれに関連付けされたテーブルの レコードを取得する方法に困っている方がおりましたら こちらの記事が少しでも参考になれば 嬉しいです。
BuySell Technologiesではエンジニアを募集しています。
明日の バイセルテクノロジーズ Advent Calendar 2022 は近藤さんによる「Databricksで綺麗にメダリオンアーキテクチャを構築するために実装ルールを決めた話」です。