学習記録

アウトプット用に作りました

N+1問題

N+1問題とは

SQLが必要以上に実行されてしまうとパフォーマンスが落ちるという問題です。
日常生活で例えるなら、お会計の時に品物をまとめて買うのではなく、
一つずつお金を払って、買っているような効率の悪いイメージです。


N+1問題の例

UserモデルとTweetモデルを例にして説明していきます。

ユーザーはたくさんのTweetをしているはずですよね。
なのでUserとTweetの関係は一対多の関係になっています。
それぞれのモデルを以下のように書き換えます。

# Userモデル
class User < ApplicationRecord
  has_many :tweets
end
# Tweetモデル
class Tweet < ApplicationRecord
  belongs_to :user
end


次にTweetをしたUserの名前を一覧画面に表示していこうと思います。

まず初めにTweetsコントローラを編集していきます。

def index
  @tweets = Tweet.all
end

そして一覧画面のビューを生成します。

<% @tweets.each do |tweet| %>
  <%= tweet.user.name %>
# Tweetモデルでアソシエーションを組んでいるのでuserをメソッドとして使える。
<% end %>

このコードだとまずはじめにTweetをすべて取得し、
その後各Tweetに対してusersテーブルから名前の情報を取得しています。

# Tweet.allの実行
SELECT 'tweets'.* FROM 'tweets'

# tweet.user.name を実行する際に走る
SELECT 'users'.* FROM 'users' WHERE 'users'.'tweet_id'=1
SELECT 'users'.* FROM 'users' WHERE 'users'.'tweet_id'=2
SELECT 'users'.* FROM 'users' WHERE 'users'.'tweet_id'=3
SELECT 'users'.* FROM 'users' WHERE 'users'.'tweet_id'=4

このようにtweetの数だけ
SELECT 'users'.* FROM 'users' WHERE 'users'.'tweet_id' = n
が発行されてしまいます。

N+1問題とは、はじめの1回のSQLTweetモデルを取得し、
そのモデルに対するデータの数(N回)のSQLが実行されてしまうことを言います。
順番から考えると、N+1問題よりも1+N問題と呼んだ方がイメージしやすいと思います。


解決方法

includesメソッドを使って定義します。

Tweetsコントローラを編集します。

def index
  @tweets = Tweet.all.includes(:user)
end
SELECT 'tweets'.* FROM 'tweets'

# tweetに関連するユーザーの情報をまとめて取得してくれます。
SELECT 'users'.* FROM 'users' WHERE 'users'.'tweet_id' IN (1, 2, 3, ......)

この上記の二つの SQL が発行されるだけになり、N+1 問題が解決します。

carrierwaveを使って画像アップロード機能を追加する

carrierwaveとは

Railsにおける画像アップロード用ライブラリです。
Uploaderクラスを別に持つから、
モデルが異なる場合でも都度記載する必要がありません。

gemfileにgem carrierwaveを追加し、bundle installを行います。


carrierwaveの使用方法

まずアップローダーを作成

$ rails g uploader HogeImage


生成されたアップローダーにデフォルトの画像ファイルと、
アップロード可能なファイル種別を指定します。
デフォルトで記述されているのでコメントタグを外します。

class HogeImageUploader < CarrierWave::Uploader::Base
  # 使用するストレージを指定。
  storage :file

  # carrierwaveを通じた画像のアップロード先をどこにするのか指定。  
     指定したディレクトリにアップロードされたファイルが保存されていく。
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # デフォルトの画像ファイルを指定。
  def default_url      
    'hoge_placeholder.png'
  end

  # アップロード可能なファイル種別を指定。
  def extension_whitelist    
    %w[jpg jpeg gif png]
  end
end


アップロード先のフォルダを.gitignoreに登録しておきます。
/config/database.ymlの下に記述しておきます。

/vendor
  /config/database.yml
  /public/uploads

.gitignoreはローカル環境でアップロードした画像ファイルを
アップロードしないようにするため。(gitの管理対象外にする)


モデルで使用するように宣言

Hogeモデルにhoge_imageを追加します。

$ rails g migration AddHogeImageToHoges hoge_image:string

Hogeモデルにアップローダーの仕様を宣言します。

mount_uploader :hoge_image, HogeImage Uploader


実際にアップロード作業を行う

コントローラのビューに画像ファイルのフィールドを追加します。

コントローラで画像ファイルの入力を受け付けます。

params.require(:hoge).permit(:title, :body, :hoge_image, :hoge_image_cache)

フォームに画像ファイルの入力フィールドを追加します。

<div class="form-group">
  <%= f.label :hoge_image %><br>
  <%= f.file_field :hoge_image, class: 'form-control mb-3', accept: 'image/*' %>
  <%= f.hidden_field :hoge_image_cache %>
</div>

:hoge_image_cacheはアップロードに失敗した際も
ファイルが消えないようにするために必要です。

accept属性でファイルの拡張子を制御できます。
accept: 'image/*'は画像ファイル accept: '.pdf'は拡張子pdf


アップロードした画像のURLを指定するとき、
アップローダーファイルでdefault_urlを設定しているから、
もし画像をアップロードする処理をしていなくても
elseの処理を記載する必要はありません。
設定しているデフォルトの画像が使われます。

Fakerを使ってダミーデータを作る

Fakerとは

ダミーデータを自動生成してくれるgemです。
gemfileにgem 'faker'を記載してbundle installを行い使えるようになります。


使い方

seeds.rbにUser(10件)とTask(20件)のダミーデータを用意します。

(db/seeds.rb)
10.times do
    User.create!(last_name: Faker::Name.last_name,
                 first_name: Faker::Name.first_name,
                 email: Faker::Internet.email,
                 password: 'password',
                 password_confirmation: 'password')
end

20.times do |n|
    Task.create!(title: "タイトル#{n}",
                  body: "本文#{n}",
                  user: User.offset(rand(User.count)).first)
end

Fakerを使ってダミーデータを作るときには、
名前やemailなどはFakerが用意しているメソッドを使います。
Faker: :Name.last_name、email: Faker::Internet.emailなど...


taskのダミーデータを作るとき、
user: User.offset(rand(user.count)).firstを使ってユーザー(user_id)を指定します。
Userテーブルのユーザーの数をランダムでレコードを取得する。その最初のレコードを使う、という意味です。

  • .offset : レコードを指定した場所から取得する。

  • user.count : countメソッド。ユーザーの数。

  • first : 最初に取得したレコード。


ダミーデータをデータベースに投入するときは、
$ rails db:seedを使います。


データベースをリセットしたいとき

rails db:resetとrails db:migrate:resetの2つがあります。
両者の違いは、
rails db:reset

  • マイグレーションファイルを編集しても、その内容を反映されません。

  • db/schema.rbを元にデータベースを作成します。

rails db:migrate:reset

  • マイグレーションファイルを直接使用するので、変更が反映されます。
    データベースを削除した後にdb/migrate/を古い順から実行します。

  • migrateファイルを修正したときに、一度データベースを削除した上で、
    新たにマイグレーションを適用したい場合などに使います。

データベースのロールバック

直前に行ったマイグレーションロールバックしたいときに使います。
$ rails db:rollback

マイグレーションを複数ロールバックしたいときに使います。
$ rails db:rollback STEP=3

ロールバックと再マイグレーションを一度に実行できるショートカットです。
$ rails db:migrate:redo STEP=3

アソシエーション

アソシエーション

Railsで作成するモデルの間に関係性を定義したい場合、
アソシエーションという機能を使うことができます。

userモデルでは、

has_many :tasks, dependent: :destroy

taskモデルでは、

belongs_to :user

が定義されていないとアソシエーションは使えません。


また2つのモデルを紐づけるために外部キーを定義します。
taskモデルに外部キーを付与したいとき、

$ rails g model task user:references

このようにuser:referencesでカラムを作るとuser_idという外部キーを作ることができます。
外部キーのカラムを作ったときは、foreign_key: trueをマイグレーションファイルに追加する必要があります。

taskモデルのレコードから紐づくuserモデルのレコードを取得したいとき、
@user = @task.userと書き、関連レコードの呼び出しを行うことができます。


dependent:オプション

オーナーオブジェクトがdestroyされたとき、
関連づけられたオブジェクトをどうするのか制御するものです。

  • :destroy 関連づけられたオブジェクトも同時にdestroyします。

  • :delete_all 関連づけられたオブジェクトは全てデータベースから直接削除されます。このときコールバックは実行されません。

  • :nullify 外部キーはNULLに設定されます。コールバックは実行されません。

  • restrict_with_exception 関連づけられたレコードが一つでもある場合は例外が発生します。

  • :restrict_with_error 関連づけられたオブジェクトが一つでもある場合にエラーがオーナーオブジェクトに追加されます。


Railsで使うアソシエーション

  • belongs_to 「1対1」
    関連づけで指定するモデル名を単数形にします。

  • has_one 「1対1」
    他方のモデルのインスタンスを丸ごと含んでいる、所有している関係です。

※ belongs_toと has_oneの違いは、、、 belongs_toは一人の著者が何冊かの本を書いている状態で1冊の書籍に1人の著者。
has_oneはユーザー1人につき1つのアカウントを持っている。

  • has_many 「1対多」 反対側のモデルではbelongs_toが使われています。
    相手のモデル名を複数形にします。

  • has_many :through 「多対多」 2つのモデルの間に第3のモデルが介在しているときに使います。

  • has_one :through 「1対1」 例えばユーザー1人につき1つのアカウント、そこに1つのアカウント履歴が関連づいているときに使います。

  • has_one_belongs_tomany 「多対多」 たくさんのユーザーとたくさんの本、それを結ぶ中間テーブルがあるときに使います。

デコレーターの導入

デコレーターとは

既存のクラスに機能を付け加えたい場合や、
既存機能に少しだけ修飾を加えたい場合に使われるパターンです。

色々なモデルで同じようなメソッドを使いたいときに、
userモデルでもcommentモデルでも定義していたら手間です。

またモデルで定義することもできますが、
このメソッドはビューの表示部分のロジックを担っているので、
その責務をモデルには持たせたくありません。

なのでこのようなときにデコレータを使用します。
デコレーターは対応したモデルのビューへの表示フォーマットのためのロジックを請け負ってくれます。


draperを導入

Railsでデコレーター層を導入したい場合、Draper もしくはActive_Decoratorを使用するのが一般的です。
gem draperをgemfileに追加後、bundle installを行います。


$ rails g draper:install

これを使ってapp/decorators/application_decorator.rbが生成されます。


$ rails g decorator user

これでapp/decorators/user_decorator.rbが生成されます。
指定したuserモデルに対応したデコレーターファイルを作ることができました。


user_decorator.rbファイルの中身を編集していきます。
ユーザーのフルネームを定義するメソッドを追加します。

class UserDecorator < Draper::Decorator
  delegate_all

  def full_name
    "#{object.last_name} #{object.first_name}"
  end
end

delegate_allはデコレーターファイルを生成すると最初から書かれていました。
この記述があることで、デコレーターに存在しないメソッドがコールされたときは、
モデルの処理が実行されるようになります。

object.はデコレートしているモデルを参照するメソッドです。
delegate_allの記載があれば、object.は記述しなくても大丈夫です。


ビューファイルで呼び出すときは、

<%= current_user.decorate.full_name %>

このように書くとデコレーターで定義したメソッドを使うことができます。

flashメッセージを表示する

flashメッセージとは

Webサービスにて何かの処理を行なった際、
それが正しく行われたかをユーザー側で確認できるよう、
メッセージで表示させるための機能です。
例えば、ログインが成功しましたよ、失敗しましたよ、
などのメッセージを表示することでユーザーが状況を知ることができます。


使い方

Railsのデフォルトでは、alertとnoticeのスタイルが用意されていますが、
Bootstrapを導入している場合は、flashメッセージで
success, info, warning, dangerのスタイルが用意されています。
この4種類を使うために、application_controller.rbにflashの定義を行います。

 add_flash_types :success, :info, :warning, :danger


ログインが成功したときのflashメッセージを出力したいときは、

(user_sessions_controller.rb)
redirect_to root_path, success: 'ログインに成功しました'

このようにapplication_controller.rbで定義したので、
flash: { success: 'ログインに成功しました' }と記載しなくても良いです。


ビューファイルにコントローラで定義したflashメッセージを出力します。

<% flash.each do |message_type, message| %>
  <div class="alert alert-<%= message_type %>"><%= message %></div>
<% end %>

パーシャルテンプレートとして用意しておくと、
色々なページでエラーメッセージを使用できる汎用的な作りにすることができます。

パーシャルテンプレートを呼び出すときはrenderを使います。

i18nによる日本語化対応

i18nとは

Railsではさまざまな言語に対応するために、このgemがデフォルトで導入されています。
gemを導入することで、gem内部の一般的なメッセージについての日本語での国際化設定ファイルを参照するようになります。
なのでconfig/locals/ja.ymlというファイルを作成する必要なありません。

i18n国際化の使い方

config/application.rbに以下のコードを追加します。

config.i18n.default_locale = :ja
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s]

私は二つ目のコードを使ったときに、
Please use Rails.root.join('path/to') instead.と言われてしまいました。

config.i18n.load_path += Dir[Rails.root.join('config/locales/**/*.{rb,yml}').to_s]  

これに書き換えたら大丈夫でした。

この設定をすることで、読み込む対象のファイルを増やし、
定義ファイルを分けることができます。


モデルとビューで翻訳ファイルを分けて管理していきます。
Viewの表示のみの内容を記載するconfig/locals/views/ja.ymlと、
モデルに関連する内容のみを記載するconfig/locals/activerecord/ja.ymlというファイルを作成します。

config/locales/views/ja.yml

ja:
 defaults:    # すべてのコントローラーで使用
   login: 'ログイン'
   register: '登録'
   logout: 'ログアウト'
 users:    # ユーザーコントローラで使用  
   new:    
     title: 'ユーザー登録'
     to_login_page: 'ログインページへ'
 user_sessions:    # セッションコントローラで使用
   new:
     title: 'ログイン'
     to_register_page: '登録ページへ'

config/locales/activerecord/ja.yml

ja:
 activerecord:
   models:
     user: 'ユーザー'
   attributes:
     user:
       last_name: '姓'
       first_name: '名'
       email: 'メールアドレス'
       password: 'パスワード'
       password_confirmation: 'パスワード確認'

モデルは全てactiverecordの下から記述します。
これによってUser.model_name.humanで使用することができます。
User.model_name.humanは、t('activerecord.models.user')と同じものを出力します。


ビューやコントローラで出力するときは、
lazy lookupを使用します。<%= t'.title %>や<%= t('.title) %>