もこたんぺ Learning Note

自分が勉強する上でわからなかったことを理解を深めていくためにまとめていく。

N+1問題について

N+1問題って?

ループ処理の中で、都度SQLを発行して、大量のSQLが発行されてパフォーマンスが低下してしまう問題のこと。
初めの一回のSQLでModelを取得して、そのModelに対するデータ数分(N回)のSQLが実行されてしまう。
データ量(N)+1回文のSQLが実行されて、パフォーマンスの低下につながる問題。
N+1問題の対策として、preloadeager_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問題ができるかもしれないと推測。
上記のように関連しているテーブルの情報を先に読み込んでおく方法として、
ActiveRecordpreloadメソッド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サーバと通信

AjaxJavaScriptで表現したいことで、サーバ側からデータを新たに取得したり、データを裏側で更新したい時などに使用する。

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'

RailsAjax機能はこの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を実装する。

Railsremote: 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要素)にイベントハンドラを設定する。


参考

Rails で JavaScript を使用する - Railsガイド

Ruby on Rails - RailsでJavaScriptを使う - このガイドでは、Railsに組み込まれたAjax/JavaScriptの機能(およびそれ以上の機能)をカバーしていますので、リッチでダイナミックなAjaxアプリ - 日本語

アセットパイプラインについて

アセットパイプラインとは

JavaScriptaやCSS、画像などのリソース(アセット)を効率的に扱うための仕組みのことをいう。

sprockets-rails gem にて提供されるSproketsの機能で、デフォルトで有効になっている。

アセットパイプラインって何をしてくれるの

開発者が書いたJavaScriptCSSを最終的にアプリを使う上で都合の良い状態ににするためのパイプライン処理を行う。

都合の良い状態=ブラウザが読み取れる形式で、実行速度が早く、ブラウザキャッシュに対して最適化される状態。

パイプラインの処理の順番は以下の感じ。

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-ujsturbolinksといったJavaScriptを指定している。

require_tree・・・指定されたディレクトリは以下の全ファイルを結合して、記述した位置に取り込む。上記の場合、「.」としているので、application.jsが配置されているディレクトリ配下が対象になる。

Sassで書くように置き換えている場合、Sassの@importを使ってディレクティブを記述する。

app/assets/stylesheets/application.scss

@import "bootstrap";
@import "tasks";


参考

https://railsguides.jp/asset_pipeline.html
アセットパイプラインについて | Railsドキュメント

モンキーパッチとは

モンキーパッチとは

オリジナルのソースコードを変更することなく、動的言語JavaScriptRailsなどのような)のコードを拡張したり、変更したりする方法のこと。

参考

Ruby on Rails アプリケーションにおけるモンキーパッチの当て方 - クックパッド開発者ブログ

Rails における gem へのモンキーパッチの方法をいくつか試した話 - R-Hack(楽天グループ株式会社)

404エラー、500エラーについて

404(Not Found)とは

ルーティングエラーのこと。
ルーティングで行き先となるActionが見つからない状態。
サーバー側から「このURLは存在しない」というエラーの応答を意味している。

500(Internal Server Error)とは

サーバー内でエラーが起こっている場合に発生するエラー
システム全般エラー