Mongoid で unique index を指定したフィールドに default 値をセットしてはならない

# 準備

例として items コレクションの `memo` フィールドに unique index を設定する。

```js
> db.items.getIndexes()
[
        {
                "v" : 2,
                "key" : {
                        "_id" : 1
                },
                "name" : "_id_",
                "ns" : "sandbox.items"
        },
        {
                "v" : 2,
                "unique" : true,
                "key" : {
                        "memo" : 1
                },
                "name" : "memo_1",
                "ns" : "sandbox.items"
        }
]
```

そして Item モデルで `memo` にデフォルト値を設定する。

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

  field :name
  field :memo, default: 'default value'
end
```

# 本題

Mongoid で default を設定されたフィールドは、 `new` でオブジェクトを生成したときにフィールドにセットされる。

```rb
irb(main):001:0> item01 = Item.new({name: 'item01'})
=> #<Item _id: 5de6602c21b620c0c9f2ff03, name: "item01", memo: "default value">
```

`memo` に Item クラスで指定されているデフォルト値が入っていることが確認できる。
この時点では MongoDB には保存されていないので、改めて保存を行う。

```rb
irb(main):002:0> item01.save
=> true
```

次に2つ目の item を生成する。

```rb
irb(main):003:0> item02 = Item.new({name: 'item02'})
=> #<Item _id: 5de660bf21b620c0c9f2ff04, name: "item02", memo: "default value">
```

**もうおわかりですね？**
unique index の指定がされている `memo` に、既に保存済みの item01 と同じ値が入っている。
これを保存しようとすると以下のようになる。

```rb
irb(main):004:0> item02.save
Traceback (most recent call last):
        1: from (irb):5
Mongo::Error::OperationFailure (E11000 duplicate key error collection: sandbox.items index: memo_1 dup key: { memo: "default value" } (11000) (on localhost:28001, attempt 2))
```

当然の結果です。

実際にはこの問題は default 値の有無は関係なく、 default 値が設定されておらずオブジェクト生成時に値をセットされることもなかったフィールドはドキュメント上は存在しない。
そして unique index が設定されているフィールドが存在しないドキュメントは、最大で1つしか存在できない。
つまり default 値を設定しなくても同様の問題が発生する。

# ではどうするか

default 値がなければ sparse indexes を使うことで回避できる。

## sparse indexes

MongoDB では index を設定する際に sparse 属性をつけることができる。

```js
> db.items.getIndexes()
[
        {
                "v" : 2,
                "key" : {
                        "_id" : 1
                },
                "name" : "_id_",
                "ns" : "sandbox.items"
        },
        {
                "v" : 2,
                "unique" : true,
                "key" : {
                        "memo" : 1
                },
                "name" : "memo_1",
                "ns" : "sandbox.items",
                "sparse" : true
        }
]
```

`sparse: true` なインデックスは、対象のフィールドが存在しないドキュメントを indexing の対象外とする。

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

  field :name
  field :memo
end
```

上記のように default 値を設定しないでおけば、以下のように問題なく2つ目のオブジェクトが save できる。

```rb
irb(main):001:0> item01 = Item.new({name: 'item01'})
=> #<Item _id: 5de6641221b620f2bbf2ff03, name: "item01", memo: nil>
irb(main):002:0> item01.save
=> true
irb(main):003:0> item02 = Item.new({name: 'item02'})
=> #<Item _id: 5de6641e21b620f2bbf2ff04, name: "item02", memo: nil>
irb(main):004:0> item02.save
=> true
```

ちなみに、対象のフィールドが null のドキュメントは当然 sparse index の indexing 対象なので以下のようにオブジェクトに `memo: nil` を設定してあげると、2件目の save の際にエラーになる。


```rb
irb(main):005:0> item03 = Item.new({name: 'item03', memo: nil})
=> #<Item _id: 5de6652d21b620ffc0f2ff03, name: "item03", memo: nil>
irb(main):006:0> item03.save
=> true
irb(main):007:0> item04 = Item.new({name: 'item04', memo: nil})
=> #<Item _id: 5de6653721b620ffc0f2ff04, name: "item04", memo: nil>
irb(main):008:0> item04.save
Traceback (most recent call last):
        1: from (irb):4
Mongo::Error::OperationFailure (E11000 duplicate key error collection: sandbox.items index: memo_1 dup key: { memo: null } (11000) (on localhost:28001, attempt 2))
```

## 同様の除外を default で設定された値をもつドキュメントに対してできないか

結論だけを述べると現時点ではおそらくできない。

MongoDB には partial indexes が用意されており、これによって **条件を満たすドキュメントのみを indexing する** ことができる。
これを使ってたとえば `memo: {$ne: 'default value'}` と設定するのはどうだろうか。

```js
> db.items.createIndex({memo: 1}, {unique: true, partialFilterExpression: {memo: {$ne: 'default value'}}})
{
        "ok" : 0,
        "errmsg" : "unsupported expression in partial index: $not\n    memo $eq \"default value\"\n",
        "code" : 67,
        "codeName" : "CannotCreateIndex"
}
```

エラーになった。
なぜなら partial indexes はその条件指定の際に `$ne` を使うことを許容していないためだ。
partial indexes がサポートするオペレータは限られており、それらのみを使って今回目的としている default 値の除外を実現するのはおそらく不可能と思われる。

したがって unique index を指定したフィールドに Mongoid で default 値をセットするべきではない。