Hatena::ブログ(Diary)

Alone Like a Rhinoceros Horn

2011-12-15

Mongoidノート - validates_uniqueness_of で :case_sensitive => false を指定すると……

Mongoid の validates_uniqueness_of で :case_sensitive => false を指定するとインデックスが使われないような予感がしたので、確認のためソースを追ってみた。

# File: mongoid-2.3.4/lib/mongoid/validations.rb

      def validates_uniqueness_of(*args)
        validates_with(UniquenessValidator, _merge_attributes(args))
      end

UniquenessValidator の方を見てみる。

# File: mongoid-2.3.4/lib/mongoid/validations/uniqueness.rb

      def validate_each(document, attribute, value)
        if document.embedded?
          # snip	
        else
          criteria = klass.where(criterion(document, attribute, value))
          criteria = scope(criteria, document, attribute)
          document.errors.add(attribute, :taken) if criteria.exists?
        end
      end

if criteria.exists? ならエラーと。


# File: mongoid-2.3.4/lib/mongoid/validations/uniqueness.rb

      def criterion(document, attribute, value)
        { attribute => filter(value) }.tap do |selector|
          if document.persisted? ||
            (document.embedded? && (document.primary_key != Array.wrap(attribute)))
            selector.merge!(:_id => { "$ne" => document.id })
          end
        end
      end

criterion は where に渡すハッシュを返すメソッド

{ :name => "David_Thomas" } 

みたいなのを返す。

で、:case_sensitive => false ならどうなるんだ?

# File: mongoid-2.3.4/lib/mongoid/validations/uniqueness.rb

      def filter(value)
        !case_sensitive? && value ? /^#{Regexp.escape(value.to_s)}$/i : value
      end

わわわ。正規表現になるのかよ。これだとインデックスが使えないはず。


Advanced Queries - MongoDB


For simple prefix queries (also called rooted regexps) like /^prefix/, the database will use an index when available and appropriate (much like most SQL databases that use indexes for a LIKE 'prefix%' expression). This only works if you don't have i (case-insensitivity) in the flags.

とあり、simple prefix queries (also called rooted regexps) like /^prefix/ のような場合を除きインデックスは使われない(使えない)ので、:case_sensitive => false を指定した場合、

validates_uniqueness_of は validation に際して、フィールドの値がユニークであることを保証するために、$regex を用いたクエリでコレクションの全ドキュメント(またはそのインデックス上のエントリ)をスキャンすることになる。*1


ということで注意が必要というか、validates_uniqueness_of を使うなら最初から大文字の使用を禁じるとか、大文字は小文字に変換した上で格納するなどして、インデックスを効かせられるようにしておくべき。

*1対象フィールドが主キーの場合は確かインデックス上のエントリのスキャンだけで済むはずだ条件によってはインデックス上のエントリのスキャンだけで済むこともあった気がするが、ドキュメントの数だけ正規表現マッチが必要になる点は変わらない。