22Inc. サービス開発日誌

スタンプスのサービス開発チームが日々の業務で得たノウハウ、経験の共有ブログです。

意外と知らない便利なRuby/Railsメソッド5選

こんにちは、22Inc.山崎です。

この記事では、便利ではあるけども意外と知らないRuby(またはRails)のメソッドを紹介します。

以下に紹介するような便利メソッドを用いて短く書かなくても、機能開発に必要なコードを書くことはできます。 しかし、短く書く方法を知らないと、書くコード量が多くなり抜け漏れも発生しがちになります。

保守性に優れたコードを書いて 一歩上のエンジニアになるために、以下に紹介するようなコードを積極的に書いていきましょう!

Enumerable#detect

使用場面

ArrayやHashの要素の中から、任意の条件にマッチする最初の要素を取り出したい。

ary = [1, 7, 55, ... , 9893211, 9953441, 10000000 ]
# ↑のように、昇順に不規則な値が並ぶ要素を持つ配列の中から、6の倍数である値のうち、一番最初にマッチする値を取り出したい

良くない書き方

ary.select {|e| e % 6 == 0}.first

selectメソッドを用いると、配列の要素を全てチェックすることになり、配列の要素数が多くなればなるほど実行完了までの時間がかかることになります。 条件にマッチする最初の要素を見つけた時点で、要素の探索を終了するほうがパフォーマンス上望ましいですよね。

キレイな書き方

ary.detect {|e| e % 6 == 0}

detectメソッドはブロック内の条件にマッチする要素を探索するという点ではselectメソッドと同じですが、1つ目の要素にマッチした時点で探索を終了します。

find, detect (Enumerable) - Rubyリファレンス

Hash#fetch_values

使用場面

複数の要素を持つハッシュの中から、指定した複数keyに対応するvalueのみを抽出したい場合。以下のような例が挙げられます。

params = { name: "James", age: 25, gender: "man", phone_number: "333-2323", prefecture_id: 1, prefecture_name: "北海道", segment: "free" }
#↑のハッシュのkeyに対応するvalueのうちいくつかを、nilまたは空白でないことをチェックしたい。

良くない書き方

params = { name: "James", age: nil, gender: "", phone_number: "333-2323", prefecture_id: 1, prefecture_name: "北海道", segment: "free" }

# nameとage、gender、segmentのキーには値が入ってなければならない。
if params[:name].present? && params[:age].present? && params[:gender].present? && params[:segment].present?
  puts "問題ありません"
else
  puts "必須パラメータが不足しています"
end

if文の条件に params が4回も重複して登場しており、DRYでないことが一目で分かります。今後、新たなパラメータを追加したり既存のパラメータを削除したりした際は、if文の条件部分の修正も行わなければならなくなり、ヒューマンエラーによるバグの温床となってしまいます。

キレイな書き方

params = { name: "James", age: nil, gender: nil, phone_number: "333-2323", prefecture_id: 1, prefecture_name: "北海道", segment: "free" }

# nameとage、gender、segmentのキーには値が入ってなければならない。
necessary_params = [:name, :age, :gender, :segment]
if params.fetch_values(*necessary_params).all? {|e| e.present?}
  puts "問題ありません"
else
  puts "必須パラメータが不足しています"
end

fetch_valuesメソッドは、引数に渡したkeyにマッチする要素のvalueを配列で返します。今回は以下のような結果を返します。

params.fetch_values(*necessary_params)
#=> ["James", nil, nil, "free"]

コードの重複が削減され、パラメータの追加や削除などの変更に強くなりました。

docs.ruby-lang.org

Object#in?

使用場面

あるオブジェクトが、配列の要素として存在するかを確認したいとき。 Array#include? メソッドを使うことでも確認できますが、代わりにObject#in?を使用することでコードの可読性を改善できます。

良くない書き方

#ID1番のユーザーが、いくつかある契約プランのうちどれかに加入していることを確認したい。
plan = User.first.plan
contract_term = User.first.contract_term

if %w(1week 2week 1month).include?(contract_term)
  puts "短期間向けプラン加入済"
elsif %w(3month 6month a_year).include?(contract_term)
  puts "長期間向けプラン加入済"
elsif %w(free trial beginner).include?(plan)
  puts "初心者向けプラン加入済"
elsif %w(standard select medium).include?(plan)
  puts "中級者向けプラン加入済"
elsif %w( high pro super ultra great).include?(plan)
  puts "上級者向けプラン加入済"
else
  puts "プラン加入なし"
end

配列の要素数が増えれば増えるほど、include?メソッドのレシーバは巨大になるので、いったいどのオブジェクトを確認の対象としたいかが分かりにくくなってしまいます。 上記のように、配列の要素数が多かったり組み合わせパターンが多い場合には、配列を変数に格納することで可読性を上げるためには、パターンの数だけ変数を宣言することになりかえって冗長になります。

キレイな書き方

#ID1番のユーザーが、いくつかある契約プランのうちどれかに加入していることを確認したい。
plan = User.first.plan
contract_term = User.first.contract_term

if contract_term.in? %w(1week 2week 1month)
  puts "短期間向けプラン加入済"
elsif contract_term.in? %w(3month 6month a_year)
  puts "長期間向けプラン加入済"
elsif plan.in? %w(free trial beginner)
  puts "初心者向けプラン加入済"
elsif plan.in? %w(standard select medium)
  puts "中級者向けプラン加入済"
elsif plan.in? %w( high pro super ultra great)
  puts "上級者向けプラン加入済"
else
  puts "プラン加入なし"
end

in?メソッド を用いることで、レシーバと引数をinclude?の場合と逆転させることができました。(実際、in?メソッドは内部でinclude?を呼んでいます) まだこのif文は改善の余地がありますが、ひとまずどのオブジェクトが条件として用いられるかは分かりやすくなりました。

Object

ActiveRecord_Relation#find_or_create_by

使用場面

一定期間分のデータの集計をバッチで行い、その結果をデータベースに保存するという業務ロジックを組むのはよくありますよね。

集計ロジックでまず必要になるのは、データベースに保存していない時期のデータを集計して保存を行う処理でしょう(例: 毎月1日に前月の売上データを集計する) また、集計を行うタイミングによっては、過去に集計したデータと異なる数値が得られることもあり、その場合は既存データを上書きしなければならないときもあります。(例: 毎月1日に、過去1年分の月次データを集計するバッチ処理)

ここで必要になってくるのが、「データの集計をした上で過去データと値を比較し、既存データがない場合は新規レコードを作成し、既存データがある場合は取得データによる既存データの上書きを行う」という処理です。

このfind_or_create_byメソッドは、その名の通りの働きをします。 つまり、 「引数に指定した条件にマッチするレコードが存在すればそのオブジェクトを取得し、存在しなければ引数に指定した条件に従うレコードを新規作成する」 という挙動になります。

なお、データベースに保存する処理はせず、インスタンスの生成までに留めるfind_or_initialize_byというメソッドもあるので、場面によって使い分けることができます。

ActiveRecord::Relation

ActiveRecord_Relation#size

使用場面

ActiveRecord_Relation#sizeは基本的にコレクションの要素数を返しますが、ActiveRecord_Relation#groupの戻り値に対して呼んだ場合、以下のようにグルーピングした状態でハッシュを返します。

#ID1〜100番のユーザーのうち、契約プランごとにグルーピングした上で各プランの人数を抽出したい

User.where(id: 1...100).group(:plan).size
#=> {"free"=>25, "standard"=>50, "pro"=>20, "super"=>5}

このとき内部ではGROUP BYとCOUNT(*)を用いたSQLが1回投げられています。そのためcount メソッドを用いても同じくハッシュの戻り値になりますが、 lengthメソッドを用いると 要素の種類数 が戻り値となります。

User.where(id: 1...100).group(:plan).count
#=> {"free"=>25, "standard"=>50, "pro"=>20, "super"=>5}

User.where(id: 1...100).group(:plan).length
#=> 4

以上、意外と知らない便利メソッド5つを紹介しました。 複雑な処理をついつい力技で実装しそうになりますが、「もっとスマートな方法はないか?」を常に考えながらコードを書いていきましょう!