Railsを使ったソースコードを眺めていたところ、モデルのメソッドにて
def foo !!bar&.baz end
というコードがありました。
どのような挙動になるのか気になったので、試してみた時のメモを残します。
目次
環境
- Rails 7.0.4.2
モデル構造
今回は3つのモデル
- Market
- Area
- Apple
があるとします。
また、それぞれの関連は
- Market : Area = n : 1
- Area : Apple = 1: n
となっています。
各モデルの定義は以下の通りであり、今回挙動を知りたいメソッド sell_apple?
が Market
モデルにあるとします。
なお、今回は各関連先が無いケースについても動作確認をするため、 belongs_to
に optional: true
をつけています。
4.1.2.11 :optional | Active Record の関連付け - Railsガイド
# Market class Market < ApplicationRecord # optional: trueにすることで #<ActiveRecord::RecordInvalid "Validation failed: Area must exist"> を防ぐ # 今回は area が無い場合の動作確認もしているので、optional: true の設定が必要 belongs_to :area, optional: true def sell_apple? !!area&.apples end end # Area class Area < ApplicationRecord has_many :apples end # Apple class Apple < ApplicationRecord belongs_to :area, optional: true end
ER図的にはこんな感じです。
挙動確認
sell_apple?
メソッドの挙動を確認する前に、気になる部分の挙動を確認していきます。
ちなみに、今回はテストコードを書いて挙動を確認してみます。
なお、記事では必要な部分だけ抜粋しています。ソースコード全体は以下で確認できます。
https://github.com/thinkAmi-sandbox/rails_7_0_minimal_app/pull/6
否定(!)や二重否定(!!)の確認
まずは否定(!
)や二重否定(!!
)の確認をします。
- BasicObject#! (Ruby 3.2 リファレンスマニュアル)
- Rubyの否定演算子2つ重ね「!!」(double-bang)でtrue/falseを返す|TechRacho by BPS株式会社
動作を確認してみると、インスタンスの値をbooleanに変換しています。
context '!や!!の確認' do let!(:market) { create(:market, area: nil) } context '取得しただけ' do # itの中身がわかりやすいので、itの説明は省略(以降同様) it do actual = Market.find(market.id) expect(actual).to eq(market) end end context '!(否定)' do it do actual = Market.find(market.id) expect(!actual).to eq(false) end end context '!!(二重否定)' do it do actual = Market.find(market.id) expect(!!actual).to eq(true) end end
ぼっち演算子(&.)と組み合わせた、!や!!の確認
続いて、ぼっち演算子と組み合わせた時の挙動を確認します。
多対一での確認(market : area = n : 1)
まずは、多対一の確認をします。
一側(area)が無い場合
一側が無い場合、ぼっち演算子を使っているので nil
になります。
Rubyで使われる記号の意味(正規表現の複雑な記号は除く) (Ruby 3.2 リファレンスマニュアル)
context 'marketのみあり' do let!(:market) { create(:market, area: nil) } context '&.' do it do actual = Market.find(market.id) expect(actual&.area).to eq(nil) end end
そのため、 nil
の否定では true
、二重否定では false
になります。
context '!&.' do it do actual = Market.find(market.id) expect(!actual&.area).to eq(true) end end context '!!&.' do it do actual = Market.find(market.id) expect(!!actual&.area).to eq(false) end end
一側(area)がある場合
一側がある場合は、一側が無いときと逆の結果になります。
context 'marketとareaともにあり' do let!(:area) { create(:area) } let!(:market) { create(:market, area: area) } context '&.' do it do actual = Market.find(market.id) expect(actual&.area).to eq(area) end end context '!&.' do it do actual = Market.find(market.id) expect(!actual&.area).to eq(false) end end context '!!&.' do it do actual = Market.find(market.id) expect(!!actual&.area).to eq(true) end end end
一対多での確認(area : apple = 1 : n)
続いて、一対多の確認をします。
多側(apple)が無い場合
多側がない場合、 actual&.apples
の結果は #<ActiveRecord::Relation []>
になります。
let!(:area) { create(:area) } context '&.' do it do actual = Area.find(area.id) # 左辺: #<ActiveRecord::Relation []> expect(actual&.apples).to eq([]) end end
続いて否定(!
)を見てみます。
この場合、 []
の否定形のbooleanを取得することになるので、 false
になります。
これは、Rubyの false
が
false は FalseClass クラスの唯一のインスタンスです。 false は nil オブジェクトとともに偽を表し、その他の全てのオブジェクトは真です。
なことから、 []
は真であり、 ![]
は偽であるためです。
context '!&.' do it do actual = Area.find(area.id) expect(!actual&.apples).to eq(false) end end
二重否定(!!)の場合は、否定の更に逆なので、元に戻って真になります。
context '!!&.' do it do actual = Area.find(area.id) expect(!!actual&.apples).to eq(true) end end
多側(apple)がある場合
多側がない場合の逆の結果となります。
let!(:area) { create(:area) } let!(:apple) { create(:apple, area: area)} context '&.' do it do actual = Area.find(area.id) expect(actual&.apples).to eq([apple]) end end context '!&.' do it do actual = Area.find(area.id) expect(!actual&.apples).to eq(false) end end context '!!&.' do it do actual = Area.find(area.id) expect(!!actual&.apples).to eq(true) end end
元々のメソッドの動作確認
気になる動作
改めて、今回のきっかけとなったメソッドと同じ作りのメソッド sell_apple?
を見てみます。
class Market < ApplicationRecord def sell_apple? !!area&.apples end end
このメソッドは、関連先の関連先(apple
)があれば true
を返してほしいように見えます。
ただ、ここまで見てきた通り、多側である apple
が存在しないときにも true
を返してしまい、意図しない挙動となっています。
context 'Marketのみあり' do let!(:actual) { create(:market, area: nil) } it do expect(actual.sell_apple?).to eq(false) end end context 'MarketとAreaあり' do context 'area without apple' do let!(:area) { create(:area) } let!(:actual) { create(:market, area: area) } it do expect(actual.sell_apple?).to eq(true) # << ここ end end end context 'MarketとAreaとAppleあり' do let!(:area) { create(:area) } let!(:apple) { create(:apple, area: area)} let!(:actual) { create(:market, area: area) } it do expect(actual.sell_apple?).to eq(true) end end
present?を使って解消
多側が存在しないときも false
を返してほしい場合は、RailsではActiveSupportの present?
メソッドが使えそうです。
2.1 blank?とpresent? | Active Support コア拡張機能 - Railsガイド
そこで、 sell_apple_with_present?
メソッドを定義し、 sell_apple?
メソッドと挙動を比較してみます。
class Market < ApplicationRecord belongs_to :area, optional: true def sell_apple? !!area&.apples end # 追加 def sell_apple_with_present? area&.apples.present? end end
結果を確認すると、 sell_apple_with_present?
メソッドは、多側が無い場合でも false
を返すようになりました。良さそうな挙動です。
context 'Marketのみあり' do let!(:actual) { create(:market, area: nil) } it do expect(actual.sell_apple?).to eq(false) expect(actual.sell_apple_with_present?).to eq(false) end end context 'MarketとAreaあり' do context 'area without apple' do let!(:area) { create(:area) } let!(:actual) { create(:market, area: area) } it do # ここだけ結果が違う expect(actual.sell_apple?).to eq(true) expect(actual.sell_apple_with_present?).to eq(false) end end end context 'MarketとAreaとAppleあり' do let!(:area) { create(:area) } let!(:apple) { create(:apple, area: area)} let!(:actual) { create(:market, area: area) } it do expect(actual.sell_apple?).to eq(true) expect(actual.sell_apple_with_present?).to eq(true) end end
ソースコード
Githubに上げました。
https://github.com/thinkAmi-sandbox/rails_7_0_minimal_app
今回のプルリクはこちらです。
https://github.com/thinkAmi-sandbox/rails_7_0_minimal_app/pull/6