zenet_logo

-株式会社ゼネット技術ブログ-

Ruby / Rails 開発でよくある落とし穴(実務でハマりがちなポイント)

Ruby/Rails開発の落とし穴と解決法

ゼネットの廣瀬です。
Ruby / Rails は「書きやすい言語・フレームワーク」として有名です。

しかし実務では

  • なぜか NoMethodErrorが出る
  • updateが失敗しているのに処理が進む
  • includesを使ったのにクエリが増える

といった 「仕様を知らないとハマるポイント」 が数多くあります。

実際の開発でも、こうした挙動が原因で
バグ調査に時間を取られるケースは少なくありません。

この記事では、Ruby / Rails 開発でよくある落とし穴を取り上げ、

  • なぜ問題になるのか
  • どう書けば安全なのか

を実例とともに整理します。

1. find_by はヒットしないと nil を返す

■問題コード

user = User.find_by(id: params[:id])
user.update(name: "foo")

■なぜ問題?
find_by がレコードを見つけられなかった場合は nil を返します。その nil に対してメソッドを呼び出すと、次のエラーが発生します。

NoMethodError: undefined method `update' for nil:NilClass

「必ず存在するはず」という前提で書くと見落としがちです。
Rails には似たメソッドがあります。

メソッド 見つからない場合
find 例外
find_by nil
find_by! 例外

■ 安全な書き方
存在するかもしれない場合

user = User.find_by(id: params[:id])
user&.update(name: "foo")

&. を使うことで
nil の場合でも安全に処理できます。

必ず存在する前提の場合

user = User.find_by!(id: params[:id])
user.update(name: "foo")

find_by! はレコードが無い場合、例外を発生させます。
コントローラーでは 404 エラーとして処理されるため安全です。

2.blank? は false を「空」とみなす

Rails には便利なメソッド blank? / present? があります。
ただしこれは Rubyの真偽値とは別の「空」の概念 です。

0.present?      # => true
" ".present?    # => false(空白は空扱い)
[].present?     # => false
nil.present?    # => false
false.present?  # => false

■問題コード

params[:enabled].blank?

■なぜ問題?
false も blank? では 空扱い になります。
つまり次のようなケースです。

params[:enabled] = false
params[:enabled].blank?
# => true

本来は「値が設定されている (false)」にも関わらず、
blank? では 「未設定」と判定されてしまいます。

例えば次のコードは誤判定になります。

unless params[:enabled].blank?
  enable_feature
end

params[:enabled] = false の場合でも
blank? => true になるため 意図しない動作になります。

■安全な書き方
boolean の未設定チェックなら

params[:enabled].nil?

blank? は文字列や配列の空チェックには便利ですが、boolean 値の判定には不向きです。nil チェックには nil? を使うようにしましょう。

3. update は失敗しても例外を出さない

■問題コード

user.update(name: nil)

update は バリデーションエラーでも例外を出さず false を返します。

■ なぜ問題?
次の問題が起きます。

  • update は失敗しても処理が継続するため、データ不整合が発生しやすい
  • バッチ処理や複数レコード更新で、一部だけ更新されてしまう事故が起きる
  • ログに何も残らず、原因調査が困難になる

■ 安全な書き方
失敗時のログを残す

unless user.update(name: "foo")
  Rails.logger.warn(user.errors.full_messages.join(", "))
end

トランザクションでは update!
update! を使うと例外が発生し、トランザクション全体がロールバックされます。

ActiveRecord::Base.transaction do
  user.update!(name: nil)  # 例外発生 → ロールバック
  log.update!(message: "updated")
end

なお、update! を使う場合は ActiveRecord::RecordInvalid 例外が発生するため、必要に応じて rescue で捕捉することも検討してください。

4. each と map の役割を混同する

■問題コード

names = []
users.each { |u| names << u.name }

■なぜ問題?
Ruby の each は「繰り返すだけ」で、戻り値は元の配列です。

result = users.each { |u| u.name }
# => users(意図と違う)

つまり each は 配列を生成するメソッドではありません。
そのため次のようなコードを書くと、
意図しない戻り値になることがあります。

names = users.each { |u| u.name }
names
# => users

names に ["Taro", "Hanako"] のような配列が入ることを期待しますが、実際には users がそのまま返ります。
このような 戻り値の誤解によるバグが発生する可能性があります。

■ 安全な書き方
新しい配列を作る場合は map を使います。

names = users.map { |u| u.name }

Rubyでは次のように書くことも多いです。

names = users.map(&:name)

一方、副作用だけが目的の場合は each を使います。

users.each { |u| u.notify }

5. select と pluck の違い

pluck と select は似ていますが大きく違います。

メソッド 返り値 特徴
pluck Ruby配列 高速
select ActiveRecord モデル生成

■ 問題コード

ids = User.select(:id).map(&:id)

このコードは動作しますが、パフォーマンス上の問題があります。
不要な ActiveRecord オブジェクトが生成されます。

■なぜ問題?
select は ActiveRecordオブジェクトを生成します。
つまり

  • オブジェクト生成
  • メモリ使用
  • GC負荷

が発生します。

■ 安全な書き方
値だけ欲しい場合は pluck を使う方が高速です。

ids = User.pluck(:id)

モデルとして扱いたいなら select

users = User.select(:id, :name)
users.each { |u| puts u.name } # =>モデルの特定の値を使える

6. eager_load / preload / includes の違い

Rails では N+1 問題を解決するために

  • includes
  • preload
  • eager_load

というメソッドがあります。
includes は条件の内容によって内部で preload または eager_load に自動切り替えされるため、意図しないクエリが発行される場合があります。
■問題コード

users = User.includes(:posts)

users.each do |user|
  user.posts.each do |post|
    puts post.title
  end
end

一見すると N+1 問題を防げているように見えますが、
クエリ条件によっては JOIN が発生しないケース があります。
例えば次のようなコードです。

User.includes(:posts).where(posts: { published: true })

■なぜ問題?
includes は 状況によって preload または eager_load に切り替わります。

メソッド 動作
preload 別クエリで関連を取得
eager_load LEFT OUTER JOIN で取得

例えば

User.includes(:posts).where(active: true)

この場合は 関連テーブルを条件に使っていないため preload になります。
一方で

User.includes(:posts).where(posts: { published: true })

この場合は eager_load(JOIN)に変わります。
つまり includes は クエリ内容によって内部挙動が変わるメソッド です。

■ 安全な書き方
実務ではまずincludesを使うケースが多いです。
ただし、次のようなケースでは挙動を明示した方が安全です。

  • SQL を最適化したい
  • JOIN を確実に使いたい
  • パフォーマンス調査中

その場合はpreload/eager_loadを明示的に使うと
クエリの挙動をコントロールできます。

includes は referencesを組み合わせると JOIN を明示できます

User.includes(:posts).where(posts: { published: true }).references(:posts)

必ず別クエリにしたい場合

User.preload(:posts)

関連テーブルで絞り込みたい場合

User.eager_load(:posts).where(posts: { published: true })

7. ||= は false も上書きする

||= は「左辺が falsy(false または nil)のとき、右辺を代入する」という Ruby の構文です。
false は「値が設定されている」ケースでも普通に使われるため、
||= を使うと 意図しない上書き が起きます。
■問題コード

@flag ||= false

■なぜ問題?
||= はnil または falseのとき再代入します。
例えば次のようなケースです。

@flag = false
@flag ||= true

@flag
# => true

false が設定されているにも関わらず、
true に上書きされてしまいます。
つまり、falseが保持できず正しい結果をキャッシュできません。

■ 安全な書き方
boolean キャッシュではこちらの方が安全です。

@flag = compute_flag if @flag.nil?

まとめ

Ruby はとても書きやすい言語であり、Rails はその特性を活かした開発しやすいフレームワークです。しかしその分、「気づきにくい落とし穴」も多く存在します。
今回紹介したポイントは、実務でも頻繁に見かけるものばかりです。

  • 仕様を正しく理解する
  • 戻り値の型を意識する
  • 例外を活用して失敗に気づく

こうした基本を押さえておくことで、バグを未然に防ぎ、開発のスピードと品質を大きく向上できます。