【Ruby】「next」がうまく動かずハマったときの話 | Dev Driven 開発・デザインチーム 【Ruby】「next」がうまく動かずハマったときの話 | 働くひとと組織の健康を創る iCARE

BLOG

【Ruby】「next」がうまく動かずハマったときの話

メグミ
2021/02/26

こんにちは!サーバーサイドエンジニアのメグミです!

みなさんご存知、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 レコードが紐づくとします。

  1. CSVファイルの内容を処理する
  2. 入力値に不備があれば次の繰り返しにジャンプ
  3. user の memo がある かつ メモを削除したい場合はmemoレコードを取得
    • 非公開のメモは削除せずに次の繰り返しへジャンプ
    • 公開済みのメモは削除
  4. CSVの内容をもとに user のレコードを更新
  5. 更新に成功したらカウントを増やす
  6. 更新したレコードの件数を返す

問題のコード

やりたいことをふまえ、私は以下のようなコードを書きました。

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. ユーザーのメモを削除したい かつ メモが非公開だった場合

それぞれどうなった?

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を実行してうまくいかなかったことについて調べてみました!

細かい部分ですが、結構ハマってしまい調査に時間がかかってしまいました。
どなたかの参考になれば幸いです!

おわり