0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

MongoDBAdvent Calendar 2019

Day 5

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

Last updated at Posted at 2019-12-04

準備

例として 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 にデフォルト値を設定する。

app/models/item.rb
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 の対象外とする。

app/models/item.rb
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 値をセットするべきではない。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?