- 投稿日:2022-01-19T23:04:43+09:00
【Rails】eager_load, preloadの解説と、それに伴うスロークエリの改善【SQL Server】
概要 業務でRailsにてAPI開発をしているのですが、そこでとあるエンドポイントがタイムアウトになり始め、チームで問題となったのでその調査を行ったときの備忘録的な記事になります。 主な話題としてはActiveRecordのeager_loadとpreloadの解説と、スロークエリとなった部分をpreloadにて改善した記事になります。 また、実行環境は以下になります 注意: DBとしてSQLServer, アダプタでactiverecord-sqlserver-adapterを使っているので、違う環境だと結果が変わる可能性があります ruby 2.7.2p137 Rails 6.0.4.4 activerecord-sqlserver-adapter 6.0.0 activerecord 6.0.0 eager_load, preload, includesの復習 eager_loadとは eager_loadは、LEFT_OUTER_JOINで指定したデータを結合し、関連テーブルのデータ配列を取得してキャッシュする。 引数として渡した関連先の要素で絞り込みが可能。 INNER_JOINしたい場合はjoins + eager_loadで可能 preloadとは preloadは、指定した関連テーブル毎に別クエリを作成、関連テーブルのデータ配列を取得し、キャッシュする。 eager_loadと違い、関連先の要素で絞り込みを行なった場合は例外を投げる。 includesとは デフォルトではpreloadと同じ挙動、関連先のテーブルの要素で絞り込みを行った場合などはeager_loadと同じ挙動。 それぞれのメリット preload has_many関連を持つデータの事前読み込みを行う場合 複数の関連先の事前読み込みを行う場合 eager_loadでは常に結合処理を行うので、データ量が大きい場合はスロークエリになる可能性あり。 この場合は分割してSELECTするpreloadのほうが早くなる可能性がある。 eager_load 関連先の要素で絞り込みを行いたい場合 has_one, belongs_to関連など1クエリでデータを取得した方が効率が良いと考えられる場合 このような場合、外部結合しても取得するレコード数に変わりは無く、1クエリでデータが取得出来るためpreloadより効率が良い可能性がある。 「preloadとeager_loadのレスポンスタイムの違い」をわかりやすく検証している記事があったので貼ります。 https://qiita.com/ryosuketter/items/097556841ec8e1b2940f#%E6%A4%9C%E8%A8%BCpreload%E3%81%A8eager_load%E3%81%AE%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%A0%E3%81%AE%E9%81%95%E3%81%84 検証による結論 eager_loadは、1回のSQLでJOINした全データを取得するので、データ量の増加に合わせてレスポンスタイムは長くなる preloadも、データ量の増加に合わせてレスポンスタイムは長くなるが、eager_loadほどではないとグラフを見てわかる (preloadはJOINせず、SQLを分割して取得するので) なので、preloadはeager_loadよりも高速なレスポンスが期待できる (ただし、本記事の結論で述べた、preloadを使用した方がいい場面、できないことなどの条件に合致していれば) 補足 どの環境でも同等の結果になることは保証しない タイムアウトになった原因の調査 ここから本題 今回問題となったエンドポイントでは、アイテムの詳細が格納されているitem_detailテーブルとそれに関連するデータをJOINして返す必要がありました。 また、要件として、item_detailテーブルの関連先であるitemテーブルのis_hogeフラグがtrueのもののみ取得したい(要は関連先で絞り込みを行いたい)ため、この機能を実現しようとすると必然的にeager_loadを使うという選択肢になるかなと思います。 実際のコードでも使っていて、以下のような感じになっています。 ItemDetail.joins(:item_detail_categories, :item) .eager_load(:item_detail_categories, item: [:hoge, :fuga, :item_category]) .where(detail_id: detail_ids, flag: true).merge(Item.where(is_hoge: true)) で、調べてみたところ、(具体的な数は避けますが)この二つの関連テーブル(item_detailとitem_detail_category)は相当数レコードがある状態だったので、「じゃあこの二つのテーブルの結合がタイムアウトの直接的な原因か〜」と思ったのですが、実際にこの二つのテーブルを結合してみてもあまり遅くはありませんでした。 改めてeager_loadで吐かれるクエリを見てみると、SELECT句に全てのテーブルのカラムが列挙されているのがわかります。(要はSELECT * FROM ...と同じになる) 以下のような感じ SELECT [ItemDetail].[id] AS t0_r0, [ItemDetail].[name] AS t0_r1, [ItemDetail].[body] AS t0_r2, [ItemDetail].[status] AS t0_r3, [ItemDetail].[hoge] AS t0_r4, [ItemDetail].[fuga] AS t0_r5, ... [ItemDetailCategory].[id] AS t1_r0, [ItemDetailCategory].[name] AS t1_r1, [ItemDetailCategory].[item_detail_id] AS t1_r2, [ItemDetailCategory].[hoge] AS t1_r3, [ItemDetailCategory].[fuga] AS t1_r4, ... FROM [ItemDetail] INNER JOIN [ItemDetailCategory] ON [ItemDetailCategory].[item_detail_id] = [ItemDetail].[id] 今回のエンドポイントでは7テーブルを結合していたので、SELECT句に指定されたカラム数は130カラムほどありました。 例えばこれを全件取得する場合は総レコード数*130カラムとなり、取得するフィールド数が大変なことになる可能性があります。 実際には絞り込みがあるのでもっと少ないと思いますが、取得するデータ量が増えたのが原因の可能性もありそうなのがわかりました。 (実際に、SELECT句で指定するカラムを絞ったらスロークエリとならない場合があったが、一概にこれが直接的な原因とまでは言えない。複合的な要因でスロークエリになっていそう) 解決 とりあえず、eager_loadではスロークエリとなってしまっていたので、これを改善します。 修正前のコード(再掲) ItemDetail.joins(:item_detail_categories, :item) .eager_load(:item_detail_categories, item: [:hoge, :fuga, :item_category]) .where(detail_id: detail_ids, flag: true).merge(Item.where(is_hoge: true)) これを以下のように修正します。 修正後のコード ItemDetail.where(detail_id: detail_ids, flag: true) .joins(:item_detail_categories, :item) .merge(Item.where(is_hoge: true)) .distinct .preload(:item_detail_categories, item: [:hoge, :fuga, :item_category]) まず絞り込みを行いたい関連先のテーブルのみを結合し、絞り込む その後、キャッシュしたいテーブルをpreloadに指定する こうすることで、preloadは絞り込まれた左側のテーブルのidで関連先のテーブルに対してSELECTをかけるので、より小さいテーブル結合とクエリで実現することが可能となります。 また、途中でdistinctを挟んでいますが、これは必要な処理になります。(後述) なぜDISTNCT? eager_loadの仕様ではSQLの結果と違い、左側テーブルの各レコードは1件しか含まれません。 言葉ではよくわからないのでコードで。 以下のようなレコードがあったとき... ItemDetail id 1 2 ItemDetailCategory id item_detail_id 1 1 2 1 3 2 4 2 これを結合すると SELECT [ItemDetail].[id], [ItemDetailCategory].[id], FROM [ItemDetail] INNER JOIN [ItemDetailCategory] ON [ItemDetailCategory].[item_detail_id] = [ItemDetail].[id] SQLでは以下のような結果が返ってきます。 ItemDetail.id ItemDetailCategory.id 1 1 1 2 2 3 2 4 見て分かる通り、左側テーブル(ItemDetail)のIDレコードが4件になります。 モデル上でのeager_loadを用いた結合ではこうはならず、左側テーブルの各レコードは重複せず、それぞれ1件しか含まれません。 感覚としては以下のようになります。(ORM上で日頃使っていると思うので何を今更、という話ではありますが...) ItemDetail.id ItemDetailCategory.id 1 1 2 2 3 4 これはeager_loadを実行したとき、ActiveRecordがうまいことオブジェクトをマッピング(左側をDISTINCT)してくれる仕様になっているので起こる挙動になっています。 この挙動はeager_load + COUNTを実行すると実際に可視化されます。 ItemDetail.eager_load(:item_detail_categories).count #=> SELECT COUNT(DISTINCT [ItemDetail].[id]) FROM [ItemDetail] ... で、今回なぜDISTINCTを入れているのかについてですが、今回のように絞り込みを行うeager_loadから絞り込みを行うjoins + preloadに置き換えを行う場合のみ、この挙動にならず、自分でDISTINCTを挿入する必要があります。 ちゃんとまとめると... メソッド DISTINCTされるか 説明 joins × SQLと同じ挙動 eager_load ○ ARがうまいことマッピングしてくれる preload ○ クエリが分かれている(結合しない)のでそもそも重複しない joins + eager_load ○ eager_loadにjoinsを足す場合は、外部結合から内部結合に切り替わるだけ joins + preload × 最初にjoinsにて結合するため、重複が発生する という挙動になります。
- 投稿日:2022-01-19T21:50:22+09:00
【RuboCop】特定の「ディレクトリ」をチェック対象外とする
概要 bundle exec rubocopを実行した際、ルール違反のファイルを指摘してくれるが、 その中で特定のディレクトリをルール適用対象外とする方法についてメモ。 環境 ruby 3.0.2 rails 6.1.4 mysql 8.0.26 rubocop 1.23.0 rubocop-rails 2.12.4 ルール違反の検出 前提:多数のカラムを持つSampleモデルをrails generateで作成した直後とする。 terminal # チェックの実行 ❯❯❯ bundle exec rubocop # ルール違反箇所の検出 sample-app/.rubocop.yml: Warning: no department given for MethodLength. Inspecting 63 files .............................................................C. Offenses: db/migrate/20220119115229_create_sample.rb:2:3: C: Metrics/MethodLength: Method has too many lines. [11/10] def change ... ^^^^^^^^^^ 63 files inspected, 1 offense detected 検出した違反:Metrics/MethodLength これはメソッドが11行以上ある場合に違反とするもの。 参考 今後もmigrationでは11行以上のメソッドが必要になる可能性がありそうなので、ディレクトリ単位で除外しておきたい 対象外設定方法 .rubocop.ymlに例外として記述する。 .rubocop.yml # migrationファイルはメソッド行数が11行以上でも許可する MethodLength: Exclude: - 'db/migrate/*' # *でmigrate以下のファイルを全て指定する db/migrate以下のファイルを、デフォルトの10行制限の対象外とすることができた。 ※特定のファイルを対象外にしたい場合は、そのままファイル名まで指定すればOK 参考 最後に より良い方法や間違い等ありましたらご指摘いただけますと幸いです!
- 投稿日:2022-01-19T21:26:09+09:00
プログラミング言語を簡単にまとめる
Java JavaとはC言語ベースに開発された汎用性の高いプログラミング言語。求人需要が多い WEBサービスから業務システム、組み込み、など多岐に渡り開発現場で採用されている。 フレームワークはspring Framework Ruby Rubyとはオブジェクト指向のプログラミング言語。コードはシンプルに書きやすく読みやすい。 WEBサービスやWEBアプリケーションができる。 フレームワークのRuby on Rails。クックパッドや食べログに使用されている PHP PHPとは動的なWEBページを生成することができるサーバーサイドのスクリプト語。HTMLは静的WEBページ。 他のプログラミング言語と比べると文法が比較的容易なので取得しやすい。 フレームワークはLaravel C# マイクロソフト社が開発したオブジェクト指向のプログラミング言語。Javaと似ている。 GUIアプリケーション、WEBアプリケーション、ゲーム開発ができる フレームワークはASP.NET
- 投稿日:2022-01-19T20:40:26+09:00
RailsでMissing helper file helpers/〜〜というエラーが出た時の解決策
はじめに プログラミング初学者の@kat_logと申します。 Railsチュートリアル6版の学習中、14章の「14.2.4[Follow]ボタン(基本編)」の $ rails generate controller Relationships を実行後に Missing helper file helpers/relationships_helper.rb (AbstractController::Helpers::MissingHelperError) というエラーが出て戸惑ったのですが、解決したため共有です。 結論 $ spring stop コマンドで解決しました。 springとは 「開発効率を上げるためにバックグラウンドで動いてくれているやつ」らしいです! spring再起動方法 rails c等でspringも(再)起動されます。 その他解決策 bundle update 自分は同様の問題が13章「13.2.3プロフィール画面のマイクロポストをテストする」でも発生したのですが、その時は下記記事をもとに $ bundle updateで解決しました。 tmpデータ削除 $ bin/rails tmp:clearにてエラー解消された方もいらっしゃるようです。 (下記記事コメント欄より) おわりに お読みいただきありがとうございました。 自分と同じ状況で困った方の参考になれば嬉しいです。
- 投稿日:2022-01-19T18:39:28+09:00
【RSpec】変数名を付けながら大量のデータ作成したとき
instance_variable_set テストで変数名を付けながら大量のデータを作成したいときに便利。 (Date.new(2022, 1, 1)..Date.new(2022, 1, 31)).each_with_index do |date, i| instance_variable_set("@date#{i+1}", date) end @date1 #=> Sat, 01 Jan 2022 @date2 #=> Sun, 02 Jan 2022 @date3 #=> Mon, 03 Jan 2022 ... 参考
- 投稿日:2022-01-19T17:38:07+09:00
RailsのN + 1問題とは
N + 1問題 ループ処理の中でSQLを発行した際にSQLが大量に増えてしまうことです。 言い換えるとデータベースのアクセス数が増えすぎてしまうことです。 N + 1問題の具体例 野球選手検索アプリがあると想定します。 チーム(teams)とそれに所属する選手(players)を例に説明します。 teamsテーブル id name 1 巨人 2 阪神 3 広島 playersテーブル id team_id name 1 1 佐藤選手 2 1 田中選手 3 2 鈴木選手 4 2 渡辺選手 5 3 高橋選手 6 3 山田選手 1チームに複数の選手が所属しているので1対多の関係です。 app/models/team.rb class Team < ApplicationRecord has_many :players end app/models/player.rb class Team < ApplicationRecord end 各チームに所属する人選手の名前を出力します。 SQLのコメントも出しておきます。 #SELECT * FROM team; #全チームの選手のデータを取得 Team.all.each do |team| puts team.player #巨人 #阪神 #広島 #SELECT * FROM team WHERE team_id =1; #teamの巨人の選手 佐藤、田中選手を出力 #SELECT * FROM team WHERE team_id =2; #teamの阪神の選手 鈴木、渡辺選手を出力 #SELECT * FROM team WHERE team_id =3; #teamの広島の選手 高橋、山田選手を出力 #ループ処理でどんどんSQLが発行されています。 #チーム数が1000,2000チームになるとどんどんSQLは増えていってしまいます。 Team.players.each do |player| puts player.name #佐藤選手 #田中選手 #鈴木選手 #渡辺選手 #高橋選手 #山田選手 end end ループ処理でどんどんSQLが発行されています。 チーム数が1000,2000チームになるとどんどんSQLは増えていってしまいます。 N + 1問題の対策 ⚫︎JOIN SQLのjoinを使ってSQL1本で全てのデータを取ってくるイメージです。 #SELECT * FROM Team INNER JOIN player ON team.id = player.team_id; #teamテーブルにplayerテーブルをjoin(配列の要素を結合して一つの文字列にする) Team.join(:player).all.each do |team| Team.players.each do |player| end end ⚫︎EagerLoad(先にSELECT) SQL1本で全てのデータを取ってくるのではなく一気にSELECTすることです。 #SELECT FROM team; #SELECT * FROM team WHERE team_id IN(1,2,3); #INで1、2、3を1本にする Team.includes(:player).all.each do |team| Team.players.each do |player| end end ⚫︎補足 LazyLoad(遅延SELECT)と言うものもあります。 欠点としてはSQLが複数発行されてしまいます。 Team.all.each do |team| #SELECT * FROM team WHERE team_id =1; #SELECT * FROM team WHERE team_id =2; team.players.each do |player| end end ⚫︎注意点 上のやり方では2点注意があります。 ・メモリがオーバー(スワップ)する ・件数を区切りながら処理する ⚫︎参考資料 ⚫︎参考動画 https://m.youtube.com/watch?v=NkmWcmA69XU&list=PL-1KBX2gDRujQaRgEByueezHBiqHP8KDD&index=10
- 投稿日:2022-01-19T15:40:46+09:00
railsの環境構築
はじめに 備忘録 PCはMac M1チップ 参考記事を読んでから実装するとわかりやすいと思います。 参考にした記事 【Ruby】Rubyのインストール手順と動作確認 Ruby on Rails の環境構築構築 bundlerで非推奨になった --path --binstubs - Qiita bundle exec はもういやだ - Qiita 特質事項 非推奨の--path vendor/bundle --binstubs=vendor/binを使わない方法で実装する rails 環境構築部のbundle execを省略する設定を有効にするために .rbenvに設定を加える必要がある (rbenvをインストールした後実行してください) $ mkdir -p ~/.rbenv/plugins $ cd ~/.rbenv/plugins $ git clone https://github.com/ianheggie/rbenv-binstubs.git railsのインストール時にドキュメントをスキップする際は下記を設定 (Rubyをインストールした後実行してください) echo "gem: --no-document" >> ~/.gemrc Homebrewのインストール Homebrewのバージョン確認と更新 brew -v brew update インストールされていなければ実行. /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" コードが途中で切れてしまう場合、、公式からコピペ 1-1. Node環境の確認とインストール homebrewを使用してnodebrewのインストール brew install nodebrew nodeの安定バージョンをインストール nodebrew install stable nodebrewのバージョン一覧を確認 nodebrew ls インストールしたバージョンを指定して切り替える nodebrew use v14.15.3 nodeのパスを通す echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.zprofile' ターミナルの再起動 yarnのインストール npm install -g yarn nodeとnpmのバージョン確認 node -v npm -v バージョンを指定してnodeをインストール nodebrew install-binary <version> インストール可能なバージョンを確認 nodebrew ls-remote rbenvとrbenv-buildのインストール rbenvのインストール brew install rbenv rbenvのバージョン確認と更新 rbenv -v brew upgrade rbenv rbenv-buildのインストール brew install ruby-build rbenvのセットアップ (pathを通す) echo 'eval "$(rbenv init -)"' >> ~/.zshrc echo 'export PATH="/usr/local/sbin:$PATH"' >> ~/.zshrc 「~/.zshrc」を再読み込み source ~/.zshrc 正しくPATHが通っているか確認する echo $PATH /Users/username/.rbenv/shims:が追加されていればOK Rubyのインストール インストール可能なRubyのバージョンを確認 rbenv install -l 2.最新バージョンをインストール rbenv install version versionを数字に変更してinstall 標準で使うrubyのversionを指定 rbenv global version ディレクトリ毎に使用するversionを変更する方法は別途 標準で使うrubyを設定できたか確かめる ruby -v rubyが使えるかの動作確認 (動作確認は記事下部にて記載) インストール済みversion一覧 rbenv versions ruby on railseの環境構築 gemのインストールには、グローバルインストールとローカルインストールがある。今回は、ローカルにインストールして(ディレクトリ)ごとにgemやgemのバージョンを管理する構築方法 プロジェクトファイルを作成する mkdir sample ファイル内でbundleを初期化 bundle init Gemfileを編集する gemのインストール先をローカルに設定 bundle config set path 'vendor/bundle' bundle execを省略する設定 bundle binstubs --path=vendor/bin gemをインストール $ bundle install 新規Railsアプリケーションの作成 rails new . bundle configで確認 $ bundle config Settings are listed in order of priority. The top value will be used. path Set for the current user (/Users/hoge/.bundle/config): "vendor/bundle" bin Set for your local app (/Users/hoge/fuga/hege/.bundle/config): "vendor/bin" 補足事項 bundle execをつけることでディレクトリにインストールしたgemを利用できる bundle exec rails new sample → ローカルにインストールしたコマンド実行 rails new sample → グローバルインストールされているシステム共通のコマンド 今回紹介したrailsの構築ではbundle execを省略できる設定を付与しているので、bundle execはいらない Rubyの動作確認 ディレクトリを作成 ディレクトリ内でファイルを作成「拡張子はrb」 内容を記述 put "Hello World" ターミナルでrubyを実行 ruby test.rb
- 投稿日:2022-01-19T01:11:28+09:00
音声ファイルを保存、再生、ダウンロード機能を実装。【Ruby】
目的 音声ファイルをアップロードし保存ができ、再生やダウンロードができるように実装します。 以下の画像のようなオーディオプレーヤーが実装されるのがゴールです。 手順 オーディオファイルを保存したいモデルにカラムを追加。 ターミナル. % rails generate migration AddFileToモデル名 file:string ターミナル. % rails db:migrate ストロングパラメータの修正 songs_controller.rb # 省略 private def song_params params.require(:song).permit(:file, :title) # permit()の中に:fileを追加 end gemの追加 Gemfile. gem 'carrierwave' ターミナル. % bundle install アップローダーの作成 ターミナル. % rails generate uploader Audiofile モデルに追加 app/models/song.rb class Song < ApplicationRecord # ここから追記 mount_uploader :file, AudiofileUploader # ここまで追記 end viewに追加 app/views/songs/new.html.erb <%= form_with model: @song, class: 'registration-main', local: true do |f|%> # 省略 # ここから追記 <%= f.file_field :file , :size => 140 %> # ここまで追記 音声ファイルの再生とダウンロード機能のgem追加 Gemfile. gem 'audiojs-rails' ターミナル. % bundle install app/javascript/packs/application.js #省略 #ここから追記 //= require audiojs #ここまで追記 viewに追加 app/views/songs/show.html.erb # 省略 <div class="song_body"> # ここから追記 <audio src= "<%= "#{@song.file}" %>" controls=""> <a src="<%= "#{@song.file}" %>">ダウンロード</a> </audio> # ここまで追記 </div> 以上で完成です。 ぜひ試してみてください。