結論から言うと Ruby でメモ化するときはアーリーリターンしてはいけないという話です(恐らく Ruby に限らず)。
- NG
def user
@user ||= begin
user = User.find_or_initialize_by(id: 1)
return user if user.persisted? # インスタンス変数定義中に return してはいけない
user.save
user
end
end
- OK
def user
@user ||= begin
user = User.find_or_initialize_by(id: 1)
if user.persisted?
user
else
user.save
user
end
end
end
# もしくは
def user
@user ||= begin
user = User.find_or_initialize_by(id: 1)
user.save if user.new_record?
user
end
end
Ruby(Rails)でメモ化を使う際、以下のように書くことがあると思います。
def user
@user ||= User.first
end
複数行であれば begin 〜 end で囲みます。
def user
@user ||= begin
user = user.find_or_initialize_by(id: 1)
if user.persisted?
user
else
user.save
user
end
end
end
ここでよくあるリファクタリング手法であるアーリーリターンを使ってみます。
def user
@user ||= begin
user = User.find_or_initialize_by(id: 1)
return user if user.persisted?
user.save
user
end
end
一見するといい感じに見えますが、このメソッドを rails console で実行してみると以下のようになります。
[1] pry(main)> def user
[1] pry(main)* @user ||= begin
[1] pry(main)* user = User.find_or_initialize_by(id: 1)
[1] pry(main)* return user if user.persisted?
[1] pry(main)*
[1] pry(main)* user.save
[1] pry(main)* user
[1] pry(main)* end
[1] pry(main)* end
=> :user
[2] pry(main)> user
User Load (3.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> #<User:0x00007ff0b4d2e338
id: 1,
created_at: Tue, 09 Jun 2020 15:04:04 JST +09:00,
updated_at: Tue, 09 Jun 2020 15:04:04 JST +09:00>
[3] pry(main)> user
User Load (1.8ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> #<User:0x00007ff0ad554740
id: 1,
created_at: Tue, 09 Jun 2020 15:04:04 JST +09:00,
updated_at: Tue, 09 Jun 2020 15:04:04 JST +09:00>
メモ化が行われておらず毎回クエリが発行されています。リファレンスを改めて確認してみると、
Ruby 2.7.0 リファレンスマニュアル
https://docs.ruby-lang.org/ja/latest/doc/spec=2fcontrol.html#return
return
式の値を戻り値としてメソッドの実行を終了します。
とありますので return した段階で user メソッドが終了し、@user のインスタンス変数が nil のままなので毎回処理を実行してしまっているということでした。
考えてみれば当然ですが、クセで早期リターンをよく使っているとつい書いてしまってハマってしまうこともあるので気をつけましょうということで。