N+1問題について
N+1問題って?
ループ処理の中で、都度SQLを発行して、大量のSQLが発行されてパフォーマンスが低下してしまう問題のこと。
初めの一回のSQLでModelを取得して、そのModelに対するデータ数分(N回)のSQLが実行されてしまう。
データ量(N)+1回文のSQLが実行されて、パフォーマンスの低下につながる問題。
N+1問題の対策として、preload
やeager_load
を使用する
例:スーパーで1点ずつお会計するような感じで、圧倒的に非効率。
ショップ(shops)とそこに所属している人(users)を例にしてみる。
shopsテーブル
id name
1 Aショップ
2 Bショップ
3 Cショップ
usersテーブル
id shop_id name
1 1 伊藤さん
2 1 八田さん
3 1 木村さん
4 2 松田さん
5 2 斉藤さん
6 3 高橋さん
7 3 橘さん
一つショップーに複数人の従業員が所属しているという関係なので、
ショップ 1: 従業員 多 となる。
それぞれのモデルは以下のようになる。
app/models/shop.rb
class Shop < ApplicationRecord has_many :users end
app/models/user.r
class User < ApplicationRecord end
各ショップに所属している人の名前を出力するプログラムを作成。
Shop.all.each do |s| # usersテーブルから名前を取得し、カンマ区切りで結合する user_names = s.users.pluck(:name).join(",") p "#{s.name}に所属する人は#{user_names}です" end
上記のコードはまずショップを全て取得してくる。
その後、それぞれのショップに対してusersテーブルから名前の情報を取得してくる。
実行すると下記のような感じで出力される。
Shop Load (0.3ms) SELECT `shops`.* FROM `shops` (0.3ms) SELECT `users`.`name` FROM `users` WHERE `users`.`shop_id` = 1 "Aショップに所属する人は伊藤さん,八田さん,木村さんです" (0.3ms) SELECT `users`.`name` FROM `users` WHERE `users`.`shop_id` = 2 "Bショップに所属する人は松田さん,斉藤さんです" (0.3ms) SELECT `users`.`name` FROM `users` WHERE `users`.`shop_id` = 3 "Cショップに所属する人は高橋さん,橘さんです"
上記のログから、まず最初のSQLで全てのショップの情報を取得している。
その後、ショップの数分の人の情報を取得するSQLが実行されていることがわかる。
今回の場合だと、数が少ないからそこまで問題なさそうだが、
データ数と実行する回数が同じになるので、データの数が増えたときに処理に時間がかかってしまう。
N+1問題の対策について
each よりも前にuserの情報を取得すればN+1問題ができるかもしれないと推測。
上記のように関連しているテーブルの情報を先に読み込んでおく方法として、
ActiveRecordのpreload
メソッドeager_load
メソッドを使う。
preloadを使用
# すべてのショップに対し、それぞれ所属する人の名前を出力する Shop.preload(:users).all.each do |s| # usersテーブルから名前を取得し、カンマ区切りで結合する user_names = s.users.pluck(:name).join(",") p "#{s.name}に所属する人は#{user_names}です" end
shopの後にpreload(:users)
を追加してみる。
これで何が起こるか。。。
Shop Load (0.3ms) SELECT `shops`.* FROM `shops` User Load (0.5ms) SELECT `users`.* FROM `users` WHERE `users`.`shop_id` IN (1, 2, 3) "Aショップに所属する人は伊藤さん,八田さん,木村さんです" "Bショップに所属する人は松田さん,斉藤さんです" "Cショップに所属する人は高橋さん,橘さんです"
N+1問題が解決されました。
先ほど4回実行されていたSQLが2回に減っていることがわかる。
ショップのデータを全て取得してから、ユーザーの情報をまとめて取得している。
eager_loadを使用
# すべてのショップに対し、それぞれ所属する人の名前を出力する Shop.eager_load(:users).all.each do |s| # usersテーブルから名前を取得し、カンマ区切りで結合する user_names = s.users.pluck(:name).join(",") p "#{s.name}に所属する人は#{user_names}です" end
実行結果を確認
SQL (0.4ms) SELECT `shops`.`id` AS t0_r0, `shops`.`name` AS t0_r1, `shops`.`created_at` AS t0_r2, `shops`.`updated_at` AS t0_r3, `users`.`id` AS t1_r0, `users`.`name` AS t1_r1, `users`.`shop_id` AS t1_r2, `users`.`created_at` AS t1_r3, `users`.`updated_at` AS t1_r4 FROM `shops` LEFT OUTER JOIN `users` ON `users`.`shop_id` = `shops`.`id`
eager_load
の場合はLEFT OUTER JOIN
というのが使われて、
1回のSQLで全てのデータを取得している。
Vue.jsの基礎テンプレート構文
テンプレートについて
●HTML
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script> <div id="app"> <p>{{message}}</p> <button @click="reverseMessage">メッセージ反転</button> </div>
●JS
new Vue({ el: '#app', data: { message: 'HelloWorld!' }, methods: { reverseMessage: function() { this.message = this.message.split('').reverse().join('') } } })
{{ message }}
の中身はJSの中のdata
の中で定義している。
今回の場合、message: 'HelloWorld'と書いてるので、それがブラウザに表示される。
ディレクティブについて
Vue.jsにおける特別な属性のこと。(Vue.js専用のもの)
v-text, v-bind, v-htmlなど
v-once・・・一度も出力される値を変えたくないときに使用する。
v-bind・・・属性に対してそのデータを表示させる(URLなど)
双方向データバインディング
v-model を使用する
ブラウザinputタグの中身を入力しなくてもdataの中の記述を読み取って表示する
また、ブラウザ上で違うことを入力すると、テンプレートからデータ側(モデル)を変更することができる
算出プロパティ(computed)
動的な処理をしたいときに使用する
Vueインスタンスプロパティ
el
Vueインスタンスを結びつけるHTML要素を指定。
このプロパティで指定した要素の配下のみVue.jsが有効になる。
data
Vue.jsで扱うデータを入れておく場所。
このデータをHTMLファイルから読み込み画面に表示することができる。
methods
Vue内で扱えるメソッド(機能)を定義する場所。
他の関数やHTMLテンプレート内から呼び出して使うことができる。
computed
算出プロパティと呼ばれる関数を定義する場所。
computedは、dataプロパティと同様に、データの変化を画面に即時反映するが、computedはデータに何らかの加工をしてから反映させる場合に使う。
template
コンポーネントを定するときに出力されるHTMLテンプレート。
JavaScriptでコンポーネントを作成する際によく使われる。
components
外部モジュールとして取り込んだコンポーネントを登録し、HTMLテンプレートとして扱えるようにする。
Ajaxを使ってRailsサーバと通信
AjaxはJavaScriptで表現したいことで、サーバ側からデータを新たに取得したり、データを裏側で更新したい時などに使用する。
Ajaxとは
Webブラウザ上で非同期通信を行い、ページの再読み込みなしに、ページを更新するためのJavaScriptのプログラミング手法。
Ajaxを使うことで、ページの一部だけをサーバから取得して、更新することが可能になる。
しかもこの処理は非同期に、バックグラウンドで行われるため、ユーザはページに比べて遷移待ちのストレスが発生しないので、スムーズに操作できる。
Ajaxを使ってタスクを削除したい場合
app/controllers/tasks_controller.rb
def destroy @task.destroy redirect_to tasks_url, notice: "タスク「#{@task.name}」を削除しました。" end
削除機能としてやりたいことは以下の2つ
1. タスクを削除する
2. タスクが削除されたことを反映した一覧画面を表示する(削除した)
上記の実装は、ブラウザからPOSTリクエストを送ることで実現している。
これを以下のように変更する。
1. タスクの削除は、サーバサイドでやる必要があるものの、今表示しているタスク一覧画面をそのまま表示し続けていたいため、Ajaxでサーバにリクエストを飛ばす。
2. 次に削除されたタスクの非表示は、クライアントサイドにて、JavaScriptで行う。一つの処理が無事に行われたら非表示になるようにする。
やりたいこととしては、ページを遷移させるのではなく、Ajaxリクエストを発生させる。
app/views/tasks/index.html.slim
= link_to '削除', task, nethod: :delete, data: { confirm: "タスク「#{@task.name}」を削除します。よろしいですか?" }, class: 'btn btn-danger'
mehod: :deleteを指定してリンクを出力する方法はJavaScriptを介しているが、HTMLのformを使ってリクエストを飛ばしていることと同じなので、ブラウザからrequestを発生させて、ページ遷移している。
Ajaxで削除アクションへリクエストを飛ばすにはremote: true
を追記する。
app/views/tasks/index.html.slim
= link_to '削除', task, nethod: :delete, remote: true, data: { confirm: "タスク「#{@task.name}」を削除します。よろしいですか?" }, class: 'btn btn-danger'
RailsのAjax機能はこのremote: true
属性を見て処理を実行するようになっている。
form_withメソッドはデフォルトでAjax機能を利用していて、無効にする場合はremote: true
オプションを追記する。
次にタスクはサーバサイドで削除して、一覧画面の再表示は必要ないので、
redirect_toの行を削除して、代わりにheadメソッドを用いてレスポンスボディなしでHTTPステータスとして204(成功と判定されること)が返るようにしておく。
app/controllers/tasks_controller.rb
def destroy @task.destroy head :no_content end
この状態で削除を行なっても、画面はそのままになってしまう。
なので削除したら、削除したタスクを非表示する処理をJavaScriptを実装する。
Railsはremote: true
をつけたa要素に対して、Ajax通信が成功したときにajax:successというイベントを発行してくれる。
そこでこれに対応するイベントハンドラを記述する。
対象となるa要素を簡単に特定できるようにしたいので、まずは削除リンクのa要素に「delete」というCSSクラスを目印として付与する。
app/views/tasks/index.html.slim
= link_to '削除', task, nethod: :delete, remote: true, data: { confirm: "タスク「#{@task.name}」を削除します。よろしいですか?" }, class: 'btn btn-danger delete'
次に、app/assets/javascripts/task.jsでこの目印を利用して、削除リンク(a要素)にイベントハンドラを設定する。
参考
アセットパイプラインについて
アセットパイプラインとは
JavaScriptaやCSS、画像などのリソース(アセット)を効率的に扱うための仕組みのことをいう。
sprockets-rails gem
にて提供されるSproketsの機能で、デフォルトで有効になっている。
アセットパイプラインって何をしてくれるの
開発者が書いたJavaScriptやCSSを最終的にアプリを使う上で都合の良い状態ににするためのパイプライン処理を行う。
都合の良い状態=ブラウザが読み取れる形式で、実行速度が早く、ブラウザキャッシュに対して最適化される状態。
パイプラインの処理の順番は以下の感じ。
1. 高級言語のコンパイル
CoffeScript, SCSS, ERB, Slimなどで書かれたコードをコンパイルして、ブラウザが認識できるJavaScript, CSSファイルとして扱う。
2. アセットの連結
複数のJavaScript, CSSファイルを一つのファイルに連結することで読み込みに必要となるリクエスト数を減らし、すべての読み込みが終わるまで時間の短縮をする。
3. アセットの最小化
スペース、改行、コメントを削除してファイルを最小化して、通信量を節約する。
4. ダイジェストの付与
コードの内容からハッシュ値を算出してファイル名の末尾に付与する。
このようにすると、コードが変更されればファイル名が変更されるので、ブラウザのキャッシュの影響で修正が反映されないという問題を防げる。
マニフェストファイルの記述
Railsのアプリケーションを作成した時点で以下のファイルが作成される。
app/assets/javascripts/application.js app/assets/styleseets/application.css(scss)
JavaScriptのマニフェストファイルを見てみると以下のようになっている。
//= require rails-ujs //= require activestorage //= require turbolinks //= require_tree .
JavaScriptのマニフェストファイルでは「//=」から始まる行を、アセットパイプラインに指示を伝えるための特別な行として扱う。
require・・・指定したJavaScriptファイルの内容を、記述した位置に取り込む。上記だとrails-ujs
やturbolinks
といったJavaScriptを指定している。
require_tree・・・指定されたディレクトリは以下の全ファイルを結合して、記述した位置に取り込む。上記の場合、「.」としているので、application.jsが配置されているディレクトリ配下が対象になる。
Sassで書くように置き換えている場合、Sassの@importを使ってディレクティブを記述する。
app/assets/stylesheets/application.scss
@import "bootstrap"; @import "tasks";
参考
・https://railsguides.jp/asset_pipeline.html
・アセットパイプラインについて | Railsドキュメント
モンキーパッチとは
モンキーパッチとは
オリジナルのソースコードを変更することなく、動的言語(JavaScriptやRailsなどのような)のコードを拡張したり、変更したりする方法のこと。
参考
404エラー、500エラーについて
404(Not Found)とは
ルーティングエラーのこと。
ルーティングで行き先となるActionが見つからない状態。
サーバー側から「このURLは存在しない」というエラーの応答を意味している。
500(Internal Server Error)とは
サーバー内でエラーが起こっている場合に発生するエラー
システム全般エラー