(個人メモ) Rails5モデルの関連付けるとき 気をつけましょう


前提

Railsでモデルを更新するようなコードを書こうとして、思いの外ハマって変更しない関連させていたモデルもロードさせる

テーブル構造

以下簡単なテーブルを作ってみます。

とりあえず書いてみる

ひとまず自分の思うがままにコードを書いていく。

# app/models/product.rb

class Product < ApplicationRecord
  belongs_to :category
  belongs_to :main_image, class_name: 'PublicImage', foreign_key: :main_image_id

  validates :name, uniqueness: true
end

いい感じにコードを書いているが。。。

しかし、問題が起きる…

既存レコードを更新してみるとログで無駄なqueryが出ているな

Product.first.update(name: "test")

  Product Load (0.7ms)  SELECT  `products`.* FROM `products` ORDER BY `products`.`id` ASC LIMIT 1
   (0.1ms)  BEGIN
  Category Load (0.6ms)  SELECT  `categories`.* FROM `categories` WHERE `categories`.`id` = 1 LIMIT 1
  PublicImage Load (0.2ms)  SELECT  `public_images`.* FROM `public_images` WHERE `public_images`.`id` = 1 LIMIT 1
  Product Exists (0.8ms)  SELECT  1 AS one FROM `products` WHERE `products`.`name` = BINARY 'black-ball' AND `products`.`id` != 1 LIMIT 1
   (0.2ms)  COMMIT
=> true

なぜかProductの更新だけのにCategoryとPublicImageもロードさせいるか
調べてみると「Rails5からbelongs_to関連はデフォルトでrequired: trueになる」ということがわかりました。

改善

required: falseにしたい時はoptional: trueと書けるようになる。
実感required: trueにしたいですが、そのまま書いてると無駄なqueryが発生されていたまま気持ち悪いです。

じゃ、以下の書き方で解決しましょう。
単純に外部キーを更新するとき、バリデーションかける。

# app/models/product.rb

class Product < ApplicationRecord
  belongs_to :category, optional: true
  belongs_to :main_image, class_name: 'PublicImage', foreign_key: :main_image_id, optional: true

  validates :name, uniqueness: true

  validates :category, presence: true, if: :validate_category_presence?
  validates :main_image, presence: true, if: :validate_main_image_presence?

  private

  def validate_category_presence?
    new_record? || category_id_changed?
  end

  def validate_main_image_presence?
    new_record? || main_image_id_changed?
  end
end

結果

簡単な改善ですが、スピードの効果はすごいです。
以下は簡単に検証して観ます

a = Time.now
1000.times{Product.first.update(name: "test")}
b = Time.now
b - a

修正前: 15.837107
修正後: 5.362079

以上