【Ruby】「next」がうまく動かずハマったときの話
こんにちは!サーバーサイドエンジニアのメグミです!
みなさんご存知、Rubyの制御構造のnext
!
今回は、next
がうまく動かず悩んだときの話をしたいと思います。
next とは
Ruby リファレンスマニュアルには以下のように説明されています。
# 空行を捨てるcat
ARGF.each_line do |line|
next if line.strip.empty?
print line
end
引用: https://docs.ruby-lang.org/ja/latest/doc/spec=2fcontrol.html#next
nextはもっとも内側のループの次の繰り返しにジャンプします。イテレータでは、yield 呼び出しの脱出になります。
next により抜けた yield 式は nil を返します。ただし、引数を指定した場合、yield 式の戻り値はその引数になります。
やりたかったこと
あるServiceクラスのループ処理の中で、特定の条件の場合にループの次の繰り返しにジャンプしたい。
具体的な処理の流れ
users レコードには、 memo レコードが紐づくとします。
- CSVファイルの内容を処理する
- 入力値に不備があれば次の繰り返しにジャンプ
- user の memo がある かつ メモを削除したい場合はmemoレコードを取得
- 非公開のメモは削除せずに次の繰り返しへジャンプ
- 公開済みのメモは削除
- CSVの内容をもとに user のレコードを更新
- 更新に成功したらカウントを増やす
- 更新したレコードの件数を返す
問題のコード
やりたいことをふまえ、私は以下のようなコードを書きました。
class TestNextService
def call
success_count = 0
CSV.foreach(file.path) do |row|
if 入力値に不備がある場合
puts "〇〇が未入力です"
next
end
user = User.find_by!(name: row['氏名'])
update_params = {
# 更新したい内容たち...
}
begin
ActiveRecord::Base.transaction do
if user.memo.present? && row['メモを削除'] == 'はい'
# true の場合はレコードを取得し以下どちらかの処理をしたい
memo = user.memo
if !memo.is_published
puts '非公開のメモは削除できません'
# 以降の処理は行わず、次の繰り返しにジャンプ!
next
else
# レコードを削除
memo.destroy!
puts 'メモを削除しました!'
end
end
end
# レコードを更新する
user.update!(update_params)
success_count += 1
puts '処理が成功しました!'
rescue StandardError => e
# 例外処理
end
end
puts "#{success_count}件処理しました!"
end
end
上記のコードで、next
を実行しているのは以下の2箇所です。
- 入力値に不備がある場合
- ユーザーのメモを削除したい かつ メモが非公開だった場合
それぞれどうなった?
1:期待通り次の繰り返しにジャンプ
2:ジャンプせず、更新処理とsuccess_countの追加が実行された。
何が違うのか?
この2箇所で違う点は、transaction のブロック内で実行しているか否かです。
どうして?
ではなぜ transaction のブロック内でnext
を実行すると
次の繰り返しにジャンプせず、以降の処理が実行されてしまったのでしょうか。
next
は、今いるブロックに対して有効なため、
transaction のブロックに対して作用してしまったことが原因だったんですね。
どうすればよかったのか?
2の条件判定をtransactionの外に出し, transacitonのブロック範囲を小さくすることで、
期待する挙動を実現することができました。
class TestNextService
def call
success_count = 0
CSV.foreach(file.path) do |row|
if 入力値に不備がある場合
puts "〇〇が未入力です"
next
end
user = User.find_by!(name: row['氏名'])
update_params = {
# 更新したい内容たち...
}
begin
memo = if user.memo.present? && row['メモを削除'] == 'はい'
user.memo
end
next unless memo.is_published
ActiveRecord::Base.transaction do
# レコードを削除
memo.destroy! if memo.present?
puts 'メモを削除しました!'
# レコードを更新する
user.update!(update_params)
end
success_count += 1
puts '処理が成功しました!'
rescue StandardError => e
# 例外処理
end
end
puts "#{success_count}件処理しました!"
end
end
複雑な条件がある場合には、このような点も注意しなければいけないと感じました。
さいごに
以上、Ruby のnext
を実行してうまくいかなかったことについて調べてみました!
細かい部分ですが、結構ハマってしまい調査に時間がかかってしまいました。
どなたかの参考になれば幸いです!
おわり