Rails これどう違うんだっけ 存在確認編

普段RubyとRuby on Railsで開発していると「これどう違うんだっけ、どっち使うべきなんだっけ?」と思うことがそこそこあります。内容的には初心者向けですが備忘録がてら書き記しておくことにしました。

今回は値やレコードがある場合/ない場合で分岐するときに使うメソッドです。

使いどころ

あればこうする、なければどうする、みたいな分岐はよくあると思います。が、その前にまずtruefalseの判定についておさらいしましょう。

Rubyでは、falseと見なされるはいわゆるfalsyなものはnilfalseのみです。
例えばStringが空文字""や、空の配列[]'、空hash{}`は全てtruethyとして扱われます。

たとえば変数fooがあるとしたら、

foo = ''
if foo
  p "あるよ"
else
  p "ないよ"
end

# => あるよ

ですね。

普段書いていて迷ったら公式ドキュメントを見つつサッとirbで試すのが早くていいですね。

nil?empty?blank?present?

普段Railsしか触ってないと忘れがちですが、nil?, empty?はRubyのメソッド、blank?, present?はRailsのメソッドということは覚えておきましょう。

nil?

nil?メソッドは値がnilの時にtrueを返します。

値がfalseなのかnilなのか、厳密に判定して分岐したいようなときが使いどころです。
逆にいえばnilfalseを区別する必要なく、truethyのものとの分岐したい場は

if foo
  do_something
else
  do_nothing
end

で十分です。

empty?

empty?メソッドは中身が空のものかを判定します。

レシーバが"", [], {} の場合、trueになります。

blank?

Railsではレシーバが空(empty? => trueのもの)かfalse, nilをtrueと判定するblank?メソッドがあります。便利ですね。

つまり中身が空、もしくは入れもの自体がnilかfalseの場合にtrueを返します。

present?

こちらもRailsのメソッド。blank?の反転です。つまりnil, falseもしくは値が空文字や空配列でもない場合、trueを返します。

ちなみにレシーバがpresent?なときレシーバの値を返し、falseの時はnilを返すpresenceメソッドもあります。

ここまでまとめ

つまり一般的には値がある場合、のようなときは

foo = "値がなにかしらあるよ"
do_something if foo.present?
# => do_somethingメソッドが呼ばれる

bar = [""]
do_something if bar.present?
# => falseなのでなにもしない

を使っておけばOKで、blank?との使いわけはどちらがifに相応わしいかで考えるのが良いですね。

# BAD
do_something unless foo.blank?

# GOOD
do_something if foo.present?

好みやコーディングルールに寄りますが、BADのほうは「存在しないのでないならば」という二重否定になっているので可読性から避けたほうが無難ですのでif present?「存在するならば」を使いましょう。

また、二重否定ではないですが無理にunlessを使うことも避け、
unless present?ならif blank?のほうが良いでしょう。

それ以外のケースで、入れ物がある前提で、中身が空であることをを限定で確かめたいときはempty?、nilであることを限定で確かめたいときはnil?を使うのが良さそうです。

メソッドを使わないパターン

Rubyではifの後はtruethy/falsyを判定するので、
if @somethingみたいにもかけます。つまり値がfalse,nilの時に分岐させたいのであれば特にメソッドは必要ありません。

これはRubyの場合、インスタンス変数は値が入ってない場合nilになることと合わせると使い勝手良いので一般的には必要なければわざわざif @something.present?と書かずにif @somethingで済ませるほうが多いでしょう。

exists?

これはRailsのActiveRecord用のメソッドです。
User.exists?(id: 1)User.where(id: 1).exists?のように使えます。

やっていることはSQLをLIMIT 1で発行して空でなければtrueが返ります。

レコードが存在するか否かを確かめるときだけ使うのが良いでしょう。
例えば

# GOOD
if User.where(role: "admin").esixts?
  p "adminが既に存在します"
else
  p User.create(role: "admin")
end

のように使います。レコードをガバッととってからpresent?で確かめるより優しいですね。

逆に

# BAD 2回SQL発行することになる
if User.where(role: "admin").esixts?
  p "管理者は#{User.where(role: "admin").pluck  (:name)}さんです"
else
  p "adminは既に存在しません"
end

# GOOD SQLは一回で済む
admins = User.where(role: "admin")
if admins.present?
  p "管理者は#{admins}さんです"
else
  p "adminは既に存在しません"
end

という例もあります。exists?はその確認のためにSQLを発行するので、存在した場合、そのデータを使うなら何かするような場合はexists?を使わないほうが良いこともあります。

実コードではもっと複雑なことがほとんどなので実際はパフォーマンスを総合的に見る必要がありますがexists?レコードが存在するか否かをSQLを使って判断するというのは覚えておきましょう。

try系

あったら特定のメソッドを実行しようとするが、そもそもレシーバにメソッドがなければnilを返したいようなそんな時に使います。たとえばDog#say => 'bow' が実装されているとして愚直にやると

if dog # dogはDog classのインスタンスとして
  dog.say
else
  nil
end

みたいなとき

参考演算子だとdog.present? ? dog.say : nilのようにワンラインですね。

この場合は通称ぼっち演算子とよばれる&.を使います。
つまりdog&.sayでOKです。Railsではdog.try(:say)でも可能ですが、パフォーマンス挙動ともに&.のほうが有用なので&.を使うほうが良いでしょう。

ちなみにぼっち演算子という通称は&.をAAとして見たときに体育座りのように見えるかららしいです。

値がnilになる場合のメモ化とデフォルト値

例えば何か値を扱うようなメソッドを使うとき、特に外部のAPIから値を取得したりそこそこ大変な処理をしたりする場ははメモ化が有効です。値がすでにあればそれを返し、無い場合のみ取得する、みたいなコードですね。

まず愚直に書くと

def foo
  if @foo.present?
    @foo
  else
    getSomethingFromApi.call # 例えば外部通信とか
  end
end

これは

def foo
  @foo ||= getSomethingFromApi.call
end

というイディオムで書くことができます。||=は左辺を見て、falthyな場合に右辺を実行して左辺に代入しつつ値をreturnするやつですね。
こうすることで2回目移行は外部通信や計算することなく、一度取得した結果を返すテクニックです。

これはインスタンス変数を明示的に初期化しなくても、nilを返す仕様との相乗効果でとても使いやすくなっています。

ちなみに処理が複数行にわたるような複雑なものになる場合はbegin,endを使う方法があります。

まず愚直にやると

def foo
  return @foo if @foo

  response = getSomethingFromApi.call
  @foo = response.nanka_sugoi_method
end

みたいな。実際にはもっと複雑な感じだと思ますが。こういう場合は

def foo
  @foo ||= begin
    response = getSomethingFromApi.call
    response.nanka_sugoi_method
  end
end

という感じで書けますね。

まとめ

今回はRailsでの存在確認、判定、それにまつわる書き方をまとめました。わりとよく使うのでこのへんは備忘録にするまでもないですが、今までブログで本業のRubyやRailsの話を書いてこなかったのでまずはこのあたりから。

普段業務とは別に自分でやったことを書きとめることが多いですが、それ以外にも日々仕事中に蓄積される知見も忘れずに書きとめていきたい所存。

Rails これどう違うんだっけ DBセットアップコマンド毒を食らわば皿まで、AZIK拡張日本語入力に入門しました