2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Ruby/Rails】クイズで学ぶ📚️ネスト構造へのアクセス方法

Last updated at Posted at 2025-10-17

本記事は筆者の体験をもとに生成 AI を活用して作成しています

失敗談

Rails アプリケーションにおいて機能実装をしていたときのことです。

以下は例ですが、Settings を使った定数取得の処理を追加しました。

API_KEY = Settings.external.service.api_key

今回、設定ファイル(config/settings/development.yml)に新たに追加したのは .service.api_key の部分でした。

開発環境ではうまく動作していたので、そのままコミット。ところが CI でのテストでは失敗...

NoMethodError: undefined method `api_key' for nil

原因は単純でした。test環境の設定ファイル(config/settings/test.yml)には .service.api_key を追加していなかったのです。

この経験から、ネストされた設定値へのアクセスでは「存在しないかもしれない」という前提を持つことの重要性を学びました。

(補足) Settings とは

本記事で扱うSettingsは、Rails アプリケーションで YAML 設定ファイルを簡単に扱うための Gem(configrails-settings-cachedなど)を指します。

以下の YAML ファイルの場合、よくある方法として Settings.external.service.api_key のようにドット記法(メソッドチェーン)で値にアクセスできる便利な仕組みです。

# config/settings.yml
external:
  service:
    api_key: "your_api_key_here"

クイズで学ぶ:各パターンの挙動

Settings で定義された変数値のようにネストされた値へのアクセス方法や、それらの挙動についてクイズ形式で学んでいきましょう。

🕵🏻‍♂️ Quiz 1: メソッドチェーンは安全?

次のコードはどうなるでしょう?

前提:

# config/settings.yml
external:
  service:
    api_key: "your_api_key_here"
    # timeout: 30 # これが定義されていない場合

コード:

Settings.external.service.timeout
💡 正解を見る

答え: nilが返されます

解説: config gem は存在しないキーに対しても安全にnilを返します。メソッドチェーンの最後のキーが存在しない場合、エラーにはなりません。

Settings.external.service.timeout
# => nil (NoMethodErrorは発生しない)

🕵🏻‍♂️ Quiz 2: エラーが出ないのはどれ?

次の 6 つのコードを実行した場合、エラーが発生しないものはどれでしょう?

前提:

# config/settings.yml
external:
  service:
    api_key: "your_api_key_here"
    timeout: 30
# internal: # これが定義されていない場合

コード:

# A. メソッドチェーン
Settings.internal.service.api_key

# B. Safe Navigation Operator
Settings.internal&.service&.api_key

# C. []チェーン
Settings[:internal][:service][:api_key]

# D. tryチェーン
Settings.try(:internal).try(:service).try(:api_key)

# E. dig
Settings.dig(:internal, :service, :api_key)

# F. fetch
Settings.fetch(:internal, :service, :api_key)
💡 正解を見る

答え: B. Safe Navigation、D. try チェーン、E. dig がエラーを発生させません

解説: 今回の場合、メソッドチェーンはNoMethodErrorを発生させます。Quiz 1 では末端キーの未定義でnilが返されましたが、今回は途中の階層(internal)自体が存在しないため、この違いが生まれます。

digfetch は使い方は似ていますが、エラー処理のアプローチが根本的に異なります。

# A. メソッドチェーン
Settings.internal.service.api_key
# => NoMethodError: undefined method `service' for nil:NilClass ❌

# B. Safe Navigation Operator
Settings.internal&.service&.api_key
# => nil ✅

# C. []チェーン
Settings[:internal][:service][:api_key]
# => NoMethodError: undefined method `[]' for nil:NilClass ❌

# D. tryチェーン
Settings.try(:internal).try(:service).try(:api_key)
# => nil ✅

# E. dig
Settings.dig(:internal, :service, :api_key)
# => nil ✅

# F. fetch
Settings.fetch(:internal, :service, :api_key)
# => KeyError: key not found: :internal ❌

🕵🏻‍♂️ Quiz 3: 空文字列の扱い

次のコードの返り値は何でしょう?

前提:

# config/settings.yml
external:
  service:
    api_key: "your_api_key_here"
    timeout: 30
    description: "" # 空文字列

コード:

Settings.dig(:external, :service, :description)
💡 正解を見る

答え: ""(空文字列)

解説: digは値をそのまま返すため、空文字列の場合は""が返ります。空文字列も有効な値として扱われます。

また、Quiz 2 で登場した他のパターンでの空文字列が返る結果も確認してみましょう。

# 前提: Settings.external.service.description = ""

# A. メソッドチェーン
Settings.external.service.description
# => "" (空文字列をそのまま返す)

# B. Safe Navigation Operator
Settings.external&.service&.description
# => "" (空文字列をそのまま返す)

# C. []チェーン
Settings[:external][:service][:description]
# => "" (空文字列をそのまま返す)

# D. tryチェーン
Settings.try(:external).try(:service).try(:description)
# => "" (空文字列をそのまま返す)

# E. dig
Settings.dig(:external, :service, :description)
# => "" (空文字列をそのまま返す)

# F. fetch
Settings.fetch(:external, :service, :description)
# => "" (空文字列をそのまま返す)

🕵🏻‍♂️ Quiz 4: ||.presence ||の違い

次の 2 つのコードの違いは何でしょう?

前提:

# config/settings.yml
external:
  service:
    api_key: "your_api_key_here"
    timeout: 30
    description: "" # 空文字列

コード:

# パターンA
Settings.dig(:external, :service, :description) || 'default_key'

# パターンB
Settings.dig(:external, :service, :description).presence || 'default_key'

それぞれ何を返すでしょう?

💡 正解を見る

答え:

  • パターン A: ""(空文字列)
  • パターン B: 'default_key'

解説: ||演算子はnilfalseのみを falsy 値として扱うため、空文字列はそのまま返ります。一方、.presenceは空文字列や空白文字列をnilに変換するため、デフォルト値が使われます。

パフォーマンス比較

👥 比較対象

前述のクイズで登場した 6 つのアクセスパターンの速度を比較してみました。

方法 記法例
A. Method chain Settings.external.service.api_key
B. Safe Navigation Operator Settings.external&.service&.api_key
C. [] チェーン Settings[:external][:service][:api_key]
D. try チェーン Settings.try(:external).try(:service).try(:api_key)
E. dig Settings.dig(:external, :service, :api_key)
F. fetch Settings.fetch(:external, :service, :api_key)

📃 ベンチマークスクリプト

📊 ベンチマークスクリプト(クリックして展開)
docker run --rm ruby:3.4.1 bash -c "
gem install benchmark-ips activesupport --no-document > /dev/null 2>&1

ruby << 'EOF'
require 'benchmark/ips'
require 'ostruct'
require 'active_support/core_ext/object/try'

# Hashでのテスト
data = {
  external: {
    service: {
      timeout: 30
    }
  }
}

# OpenStructでのテスト(メソッドチェーン用)
def create_nested_struct(hash)
  if hash.is_a?(Hash)
    struct = OpenStruct.new
    hash.each do |key, value|
      struct[key] = create_nested_struct(value)
    end
    struct
  else
    hash
  end
end

settings_struct = create_nested_struct(data)

puts \"Ruby #{RUBY_VERSION} + Hash/OpenStruct での実測値(Docker環境):\"
puts

# 存在するキーでのベンチマーク
puts \"🔍 存在するキーでのベンチマーク結果\"
Benchmark.ips do |x|
  x.config(time: 3, warmup: 1)
  x.report('A. method chain (exists):    ') { settings_struct.external.service.timeout }
  x.report('B. safe navigation (exists):') { settings_struct&.external&.service&.timeout }
  x.report('C. [] chain (exists):       ') { data[:external][:service][:timeout] }
  x.report('D. try chain (exists):      ') { settings_struct.try(:external).try(:service).try(:timeout) }
  x.report('E. dig (exists):            ') { data.dig(:external, :service, :timeout) }
  x.report('F. fetch (exists):          ') { data.fetch(:external).fetch(:service).fetch(:timeout) }

  x.compare!
end

puts
puts \"🔍 存在しないキーでのベンチマーク結果\"
Benchmark.ips do |x|
  x.config(time: 3, warmup: 1)
  x.report('B. safe navigation (missing):') { settings_struct&.nonexistent&.missing_key }
  x.report('C. [] chain (missing):       ') { data[:nonexistent][:missing_key] rescue nil }
  x.report('D. try chain (missing):      ') { settings_struct.try(:nonexistent).try(:missing_key) }
  x.report('E. dig (missing):            ') { data.dig(:nonexistent, :missing_key) }

  x.compare!
end

puts
puts \"⚠️ エラーが発生するパターン\"
puts \"A. method chain (missing):     NoMethodError発生\"
puts \"F. fetch (missing):            KeyError発生\"
EOF
"

📑 ベンチマーク実行結果

📊 ベンチマーク実行結果(クリックして展開)
Ruby 3.4.1 + Hash/OpenStruct での実測値(Docker環境):

🔍 存在するキーでのベンチマーク結果
ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +PRISM [aarch64-linux]
Warming up --------------------------------------
A. method chain (exists):
                       520.387k i/100ms
B. safe navigation (exists):
                       664.181k i/100ms
C. [] chain (exists):
                         1.442M i/100ms
D. try chain (exists):
                       141.067k i/100ms
E. dig (exists):
                         1.178M i/100ms
F. fetch (exists):
                         1.020M i/100ms
Calculating -------------------------------------
A. method chain (exists):
                          7.488M (± 1.3%) i/s  (133.55 ns/i) -     22.897M in   3.058495s
B. safe navigation (exists):
                          6.718M (± 1.0%) i/s  (148.84 ns/i) -     20.590M in   3.064937s
C. [] chain (exists):
                         14.665M (± 1.1%) i/s   (68.19 ns/i) -     44.694M in   3.048061s
D. try chain (exists):
                          1.369M (± 9.9%) i/s  (730.48 ns/i) -      4.091M in   3.043173s
E. dig (exists):
                         11.862M (± 1.0%) i/s   (84.30 ns/i) -     36.519M in   3.078955s
F. fetch (exists):
                         10.276M (± 0.8%) i/s   (97.32 ns/i) -     31.617M in   3.077042s

Comparison:
C. [] chain (exists):       : 14664885.9 i/s
E. dig (exists):            : 11861907.2 i/s - 1.24x  slower
F. fetch (exists):          : 10275628.1 i/s - 1.43x  slower
A. method chain (exists):    :  7487593.1 i/s - 1.96x  slower
B. safe navigation (exists)::  6718468.0 i/s - 2.18x  slower
D. try chain (exists):      :  1368963.5 i/s - 10.71x  slower


🔍 存在しないキーでのベンチマーク結果
ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +PRISM [aarch64-linux]
Warming up --------------------------------------
B. safe navigation (missing):
                       216.335k i/100ms
C. [] chain (missing):
                       108.695k i/100ms
D. try chain (missing):
                       418.918k i/100ms
E. dig (missing):
                         1.493M i/100ms
Calculating -------------------------------------
B. safe navigation (missing):
                          2.953M (± 2.9%) i/s  (338.66 ns/i) -      8.870M in   3.006661s
C. [] chain (missing):
                          1.112M (± 2.1%) i/s  (898.94 ns/i) -      3.370M in   3.030419s
D. try chain (missing):
                          4.119M (± 2.0%) i/s  (242.76 ns/i) -     12.568M in   3.052107s
E. dig (missing):
                         14.862M (± 1.3%) i/s   (67.29 ns/i) -     44.792M in   3.014377s

Comparison:
E. dig (missing):            : 14862041.8 i/s
D. try chain (missing):      :  4119317.5 i/s - 3.61x  slower
B. safe navigation (missing)::  2952780.8 i/s - 5.03x  slower
C. [] chain (missing):       :  1112422.2 i/s - 13.36x  slower


⚠️ エラーが発生するパターン
A. method chain (missing):     NoMethodError発生
F. fetch (missing):            KeyError発生

📊 実測結果サマリ

存在するキーでの性能ランキング

  • 1 位: C. [] chain - 14.67M i/s(最速)
  • 2 位: E. dig - 11.86M i/s(1.24 倍遅い)
  • 3 位: F. fetch - 10.28M i/s(1.43 倍遅い)
  • 4 位: A. method chain - 7.49M i/s(1.96 倍遅い)
  • 5 位: B. safe navigation - 6.72M i/s(2.18 倍遅い)
  • 6 位: D. try chain - 1.37M i/s(10.71 倍遅い)

存在しないキーでの性能ランキング

  • 1 位: E. dig - 14.86M i/s(最速)
  • 2 位: D. try chain - 4.12M i/s(3.61 倍遅い)
  • 3 位: B. safe navigation - 2.95M i/s(5.03 倍遅い)
  • 4 位: C. [] chain - 1.11M i/s(13.36 倍遅い)

重要なポイント

  • Ruby 3.4.1 では[] chainが存在するキーで最速
  • digは存在しないキーで圧倒的に高速
  • try chainは常に低速(特に存在するキーで 10 倍以上遅い)
  • method chainfetchは存在しないキーでエラー発生

ユースケース別の推奨方法

ケース 1: シンプルなデフォルト値設定

Settings.dig(:database, :timeout) || 30

使うべき場面:

  • 設定値が存在しない場合のみデフォルト値を使いたい
  • 空文字列も有効な値として扱いたい

||演算子の動作:

||演算子はnilfalseのみを falsy 値として扱います。

nil     || 'DEFAULT'     # => 'DEFAULT'
false   || 'DEFAULT'     # => 'DEFAULT'
""      || 'DEFAULT'     # => ""  (空文字列はそのまま)
"  "    || 'DEFAULT'     # => "  "  (空白もそのまま)
"VALUE" || 'DEFAULT'     # => "VALUE"

ケース 2: 空文字列も無効値として扱う

Settings.dig(:external, :service, :api_key).presence || 'default_key'

使うべき場面:

  • ユーザー入力や外部設定ファイルの値を扱う
  • 空文字列や空白文字列を無効な値として扱いたい
  • より厳密なバリデーションが必要

.presence ||の動作:

nil.presence     || 'DEFAULT'     # => 'DEFAULT'
false.presence   || 'DEFAULT'     # => 'DEFAULT'
"".presence      || 'DEFAULT'     # => 'DEFAULT' (空文字列も除外)
"  ".presence    || 'DEFAULT'     # => 'DEFAULT' (空白も除外)
"VALUE".presence || 'DEFAULT'     # => "VALUE"

ケース 3: 環境ごとに異なるデフォルト値

Settings.dig(:external, :service, :endpoint).presence ||
  case Rails.env
    when 'production' then 'https://api.example.com'
    when 'staging'    then 'https://staging-api.example.com'
    else 'http://localhost:3000'
  end

使うべき場面:

  • 本番、ステージング、開発で異なる設定が必要
  • 環境依存の値を管理したい

まとめ

本記事では、Ruby でネストした設定値に安全にアクセスする 6 つの方法を、クイズ形式での学習からパフォーマンス測定まで包括的に検証しました。

実測結果から見えた真実

Ruby 3.4.1 + Hash/OpenStruct での実測により、以下のことが明らかになりました:

存在するキーの場合

  • [] chainが最速(14.67M i/s)だが、存在しないキーでエラーリスク
  • digが 2 位(11.86M i/s)で安全性とのバランスが優秀
  • try chainが最遅(1.37M i/s)で 10 倍以上の性能差

存在しないキーの場合

  • digが圧倒的に最速(14.86M i/s)
  • [] chainが最遅(1.11M i/s)で 13 倍以上の性能差
  • エラー発生パターンmethod chainfetch)は論外

結論:状況に応じた適切な選択を

基本方針:dig + .presence

# ✅ 推奨パターン(基本)
api_key = Settings.dig(:external, :service, :api_key)

# ✅ 空文字列も無効にしたい場合
api_key = Settings.dig(:external, :service, :api_key).presence || 'default_key'

なぜdigなのか?

観点 評価 理由
安全性 ⭐⭐⭐⭐⭐ 途中のキーが存在しなくてもnilを返す(エラーにならない)
パフォーマンス ⭐⭐⭐⭐⭐ 存在しないキーで最速、存在するキーでも 2 位の高速性
読みやすさ ⭐⭐⭐⭐⭐ 意図が明確で、誰が見ても理解しやすい
汎用性 ⭐⭐⭐⭐⭐ ハッシュ、配列、複雑なデータ構造すべてに対応

避けるべきパターン

パターン 問題点 実測結果
tryチェーン
Settings.try(:external).try(:service)...
常に低速(特に存在するキーで 10 倍遅い) 1.37M i/s / 4.12M i/s
Safe Navigation
Settings&.external&.service&.api_key
存在するキーで 2 倍遅い 6.72M i/s / 2.95M i/s
[]チェーン
Settings[:external][:service][:api_key]
存在しないキーで 13 倍遅い、エラーリスク 14.67M i/s / 1.11M i/s

重要な例外:メソッドチェーンの戦略的活用

とはいえ、メソッドチェーンにも利点はありますNoMethodErrorが発生することで、設定値が未定義であることを確実に検知できます。特に本番環境で必須の設定値の場合、起動時にエラーを出して早期に問題を発見できるのは有益です。状況に応じて、意図的にメソッドチェーンを選択する判断も有効でしょう。

# 必須設定値の場合:意図的にエラーを発生させる
API_KEY = Settings.external.service.api_key  # 未定義なら即座にエラー

# オプショナル設定値の場合:安全にアクセス
timeout = Settings.dig(:external, :service, :timeout) || 30

開発環境では動いていても、test 環境や本番環境でNoMethodErrorが発生すると大きな問題になります。設定値の性質(必須 vs オプショナル)とチームの方針を考慮し、適切な手法を選択することが最も重要です。

Tips: さらに深掘り

💡 Tip 1: dig の汎用性の高さ

digメソッドは非常に汎用性が高く、様々なデータ構造で安全にアクセスできます。

ハッシュでの使用

Ruby のHashでもdigは使えます。

config = { external: { service: { api_key: 'secret' } } }

# dig を使う
config.dig(:external, :service, :api_key)
# => "secret"

# 存在しないキー
config.dig(:external, :database, :host)
# => nil

配列とハッシュが混在する場合

digは配列インデックスも扱えるため、複雑なデータ構造でも安全にアクセスできます。

data = {
  servers: [
    { name: 'server1', ip: '192.168.1.1' },
    { name: 'server2', ip: '192.168.1.2' }
  ]
}

# 配列の最初の要素のnameを取得
data.dig(:servers, 0, :name)
# => "server1"

# 存在しないインデックス
data.dig(:servers, 5, :name)
# => nil

# ネストした配列とハッシュの組み合わせ
complex_data = {
  environments: [
    {
      name: 'production',
      databases: [
        { host: 'prod-db1.example.com', port: 5432 },
        { host: 'prod-db2.example.com', port: 5432 }
      ]
    }
  ]
}

# 深くネストしたデータも安全にアクセス
complex_data.dig(:environments, 0, :databases, 1, :host)
# => "prod-db2.example.com"

# 存在しないパスでもエラーにならない
complex_data.dig(:environments, 5, :databases, 0, :host)
# => nil

このようにdigは、ハッシュ、配列、そしてそれらが混在する複雑なデータ構造でも一貫した方法で安全にアクセスできる優れたメソッドです。

💡 Tip 2: .presenceの内部実装

参考までに、.presenceメソッドの実装を理解しておくと便利です。

# ActiveSupport::CoreExtensions::Object::Blank
def presence
  self if present?
end

def present?
  !blank?
end

def blank?
  respond_to?(:empty?) ? !!empty? : !self
end

blank?の判定基準:

blank? 説明
nil true nil は常に blank
false true false も blank 扱い
"" true 空文字列
" " true 空白のみの文字列
[] true 空の配列
{} true 空のハッシュ
0 false ⚠️ 数値は blank ではない
"0" false ⚠️ 文字列の"0"も blank ではない

この実装を知ることで、.presenceがどのような値をnilに変換するのかが明確になります。

さいごに

本記事ではネストされた設定値へのアクセス方法の紹介とそれらのパフォーマンス比較を行いました。

「どの方法が絶対的に正しい」ということではありません。重要なのは、設定値の性質やチームの開発方針を踏まえ、適切な実装手段を選択することだと思います。

  • 必須の設定値であれば、fetchやメソッドチェーンで意図的にエラーを発生させる
  • オプショナルな値であれば、dig.presenceを使用して安全にアクセスする
  • チームの既存のコーディング規約やパターンを尊重する

状況に応じた適切な選択ができるよう、この記事が誰かの参考になれば幸いです。

最後までご覧いただき、ありがとうございました 🙏✨

2
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?