MongoDB/Mongoid の exists を整理する

# MongoDB の `$exists` の挙動

以下のようなデータがあるとする。

```js
> db.items.find()
{ "_id" : ObjectId("5de50feab2e406088d483c44"), "name" : "item01", "color" : "red", "size" : "M" }
{ "_id" : ObjectId("5de51001b2e406088d483c45"), "name" : "item02", "color" : "blue", "size" : "S", "memo" : null }
{ "_id" : ObjectId("5de51012b2e406088d483c46"), "name" : "item03", "color" : "yellow", "size" : "L", "memo" : "" }
{ "_id" : ObjectId("5de51029b2e406088d483c47"), "name" : "item04", "color" : "green", "size" : "M", "memo" : "hoge" }
```

このとき `$exists` を使って memo があるデータだけを取ろうとすると以下のようになる。

```js
> db.items.find({memo: {$exists: true}})
{ "_id" : ObjectId("5de51001b2e406088d483c45"), "name" : "item02", "color" : "blue", "size" : "S", "memo" : null }
{ "_id" : ObjectId("5de51012b2e406088d483c46"), "name" : "item03", "color" : "yellow", "size" : "L", "memo" : "" }
{ "_id" : ObjectId("5de51029b2e406088d483c47"), "name" : "item04", "color" : "green", "size" : "M", "memo" : "hoge" }
```

`$exists` は対象のフィールドが存在するかどうかのみを評価している。
したがって、 memo フィールドの値が null でも空でも、 `$exists` では true として扱われる。

ここまでは MongoDB の `$exists` の話であり、何も難しいことはない。

# Mongoid の `exists` の挙動

Mongoid は Ruby 上で MongoDB を扱うための ODM だ。
当然 MongoDB の `$exists` に相当するものが存在する。

まず、以下のようなクラスを定義しておく。

```rb:app/models/item.rb
class Item
  include Mongoid::Document

  field :name
  field :color
  field :size
  field :memo
end
```

以下の1行を実行すれば全件取得できる。
（ここでは rails console を利用している）

```rb
irb(main):001:0> Item.each { |i| p "#{i._id}, #{i.name}, #{i.color}, #{i.size}, #{i.memo}" }; nil
MONGODB | [1] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{}, "lsid"=>{"id"=><BSON::Binary:0x70346640265920 type=uuid data=0x672fb96b60a9434f...>}}
MONGODB | [1] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
"5de50feab2e406088d483c44, item01, red, M, "
"5de51001b2e406088d483c45, item02, blue, S, "
"5de51012b2e406088d483c46, item03, yellow, L, "
"5de51029b2e406088d483c47, item04, green, M, hoge"
=> nil
```

`exists` を使うと以下のようになる。

```rb
irb(main):002:0> Item.exists(memo: true).each { |i| p "#{i._id}, #{i.name}, #{i.color}, #{i.size}, #{i.memo}" }; nil
MONGODB | [2] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"memo"=>{"$exists"=>true}}, "lsid"=>{"id"=><BSON::Binary:0x70346640265920 type=uuid data=0x672fb96b60a9434f...>}}
MONGODB | [2] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
"5de51001b2e406088d483c45, item02, blue, S, "
"5de51012b2e406088d483c46, item03, yellow, L, "
"5de51029b2e406088d483c47, item04, green, M, hoge"
=> nil
```

`Item.exists(memo: true)` の代わりに `Item.where(:memo.exists => true)` と書くこともできる。

ここまでは MongoDB の `$exists` と同じ挙動になる。

# Mongoid で default 値が設定されている場合の挙動

memo フィールドにモデル側で default を設定することができる。

```rb:app/models/Item.rb
class Item
  include Mongoid::Document

  field :name
  field :color
  field :size
  field :memo, default: 'empty'
end
```

rails console でモデルの変更を読み込むためリロードを行う。

```rb
irb(main):003:0> reload!
Reloading...
=> true
```

全件取得すると以下のようになる。

```rb
irb(main):004:0> Item.each { |i| p "#{i._id}, #{i.name}, #{i.color}, #{i.size}, #{i.memo}" }; nil
MONGODB | [3] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{}, "lsid"=>{"id"=><BSON::Binary:0x70346640265920 type=uuid data=0x672fb96b60a9434f...>}}
MONGODB | [3] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
"5de50feab2e406088d483c44, item01, red, M, empty"
"5de51001b2e406088d483c45, item02, blue, S, "
"5de51012b2e406088d483c46, item03, yellow, L, "
"5de51029b2e406088d483c47, item04, green, M, hoge"
=> nil
```

item01 の memo に default 値が設定されていることがわかる。

この状況で先ほどと同様に `exists` で絞り込むと以下のようになる。

```rb
irb(main):005:0> Item.exists(memo: true).each { |i| p "#{i._id}, #{i.name}, #{i.color}, #{i.size}, #{i.memo}" }; nil
MONGODB | [4] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"memo"=>{"$exists"=>true}}, "lsid"=>{"id"=><BSON::Binary:0x70346640265920 type=uuid data=0x672fb96b60a9434f...>}}
MONGODB | [4] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
"5de51001b2e406088d483c45, item02, blue, S, "
"5de51012b2e406088d483c46, item03, yellow, L, "
"5de51029b2e406088d483c47, item04, green, M, hoge"
=> nil
```

item01 が出力されない。
つまり default 値の有無は `exists` の結果に影響しない。
default はあくまでそのフィールドが存在しない時にアプリケーション側で扱いたい値をセットしておくためのものである。
これを利用して、アプリケーション側でその値が default 値なのかセットされた値なのかを区別することができる。

items に1件追加する。

```js
> db.items.insert({name: 'item05', color: 'orange', size: 'S', memo: 'empty'})
WriteResult({ "nInserted" : 1 })
```

rails console で全件取得すると以下のようになる。

```rb
irb(main):006:0> Item.each { |i| p "#{i._id}, #{i.name}, #{i.color}, #{i.size}, #{i.memo}" }; nil
MONGODB | [5] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{}, "lsid"=>{"id"=><BSON::Binary:0x70346640265920 type=uuid data=0x672fb96b60a9434f...>}}
MONGODB | [5] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
"5de50feab2e406088d483c44, item01, red, M, empty"
"5de51001b2e406088d483c45, item02, blue, S, "
"5de51012b2e406088d483c46, item03, yellow, L, "
"5de51029b2e406088d483c47, item04, green, M, hoge"
"5de51b39b2e406088d483c4a, item05, orange, S, empty"
=> nil
```

この結果から item01 の memo と item05 の memo が同一かどうかは区別がつかない。

```rb
irb(main):007:0> Item.find_by(name: 'item01').memo == Item.find_by(name: 'item05').memo
MONGODB | [6] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"name"=>"item01"}, "lsid"=>{"id"=><BSON::Binary:0x70346640265920 type=uuid data=0x672fb96b60a9434f...>}}
MONGODB | [6] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
MONGODB | [7] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"name"=>"item05"}, "lsid"=>{"id"=><BSON::Binary:0x70346640265920 type=uuid data=0x672fb96b60a9434f...>}}
MONGODB | [7] localhost:28001 | sandbox.find | SUCCEEDED | 0.001s
=> true
```

`exists` を使えば区別できる。

```rb
irb(main):008:0> Item.exists(memo: true).each { |i| p "#{i._id}, #{i.name}, #{i.color}, #{i.size}, #{i.memo}" }; nil
MONGODB | [8] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"memo"=>{"$exists"=>true}}, "lsid"=>{"id"=><BSON::Binary:0x70346640265920 type=uuid data=0x672fb96b60a9434f...>}}
MONGODB | [8] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
"5de51001b2e406088d483c45, item02, blue, S, "
"5de51012b2e406088d483c46, item03, yellow, L, "
"5de51029b2e406088d483c47, item04, green, M, hoge"
"5de51b39b2e406088d483c4a, item05, orange, S, empty"
=> nil
```

逆を取りたい場合は以下。

```rb
irb(main):009:0> Item.exists(memo: false).each { |i| p "#{i._id}, #{i.name}, #{i.color}, #{i.size}, #{i.memo}" }; nil
MONGODB | [9] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"memo"=>{"$exists"=>false}}, "lsid"=>{"id"=><BSON::Binary:0x70346640265920 type=uuid data=0x672fb96b60a9434f...>}}
MONGODB | [9] localhost:28001 | sandbox.find | SUCCEEDED | 0.003s
"5de50feab2e406088d483c44, item01, red, M, empty"
=> nil
```

あるいは、直接 `where(memo: 'empty')` してしまうことでも item05 のみを取ることができる。

```rb
irb(main):010:0> Item.where(memo: 'empty').each { |i| p "#{i._id}, #{i.name}, #{i.color}, #{i.size}, #{i.memo}" }; nil
MONGODB | [10] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"memo"=>"empty"}, "lsid"=>{"id"=><BSON::Binary:0x70346640265920 type=uuid data=0x672fb96b60a9434f...>}}
MONGODB | [10] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
"5de51b39b2e406088d483c4a, item05, orange, S, empty"
=> nil
```