前提
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
以上