準備
例として items コレクションの memo
フィールドに unique index を設定する。
> 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
にデフォルト値を設定する。
class Item
include Mongoid::Document
field :name
field :memo, default: 'default value'
end
本題
Mongoid で default を設定されたフィールドは、 new
でオブジェクトを生成したときにフィールドにセットされる。
irb(main):001:0> item01 = Item.new({name: 'item01'})
=> #<Item _id: 5de6602c21b620c0c9f2ff03, name: "item01", memo: "default value">
memo
に Item クラスで指定されているデフォルト値が入っていることが確認できる。
この時点では MongoDB には保存されていないので、改めて保存を行う。
irb(main):002:0> item01.save
=> true
次に2つ目の item を生成する。
irb(main):003:0> item02 = Item.new({name: 'item02'})
=> #<Item _id: 5de660bf21b620c0c9f2ff04, name: "item02", memo: "default value">
もうおわかりですね?
unique index の指定がされている memo
に、既に保存済みの item01 と同じ値が入っている。
これを保存しようとすると以下のようになる。
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 属性をつけることができる。
> 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 の対象外とする。
class Item
include Mongoid::Document
field :name
field :memo
end
上記のように default 値を設定しないでおけば、以下のように問題なく2つ目のオブジェクトが save できる。
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 の際にエラーになる。
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'}
と設定するのはどうだろうか。
> 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 値をセットするべきではない。