8
4

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.

Mongoidの隠れクエリメソッドfind_by!とraise_not_found_errorオプションの関係

Posted at

Mongoidには実はfind_by!というクエリメソッドがあるのだが、公式ドキュメントのクエリセクションにはこの説明が載っていない。
かつ、mongoid.ymlで指定できるraise_not_found_errorオプションと、find, find_by, find_by!の関係にちょっと癖があったので、それらをまとめるためにこの記事を残す。

find_by!

find_by!の存在を知るためには、まずソースコードを読む必要がある。

lib/mongoid/findable.rb#L134にその記述がある

    # Find the first +Document+ given the conditions, or raises
    # Mongoid::Errors::DocumentNotFound
    #
    # @example Find the document by attribute other than id
    #   Person.find_by(:username => "superuser")
    #
    # @param [ Hash ] attrs The attributes to check.
    #
    # @raise [ Errors::DocumentNotFound ] If no document found.
    #
    # @return [ Document ] A matching document.
    #
    def find_by!(attrs = {})
      result = where(attrs).find_first
      raise(Errors::DocumentNotFound.new(self, attrs)) unless result
      yield(result) if result && block_given?
      result
    end

メソッドのなかではMongoid.raise_not_found_errorをチェックする条件分岐がないので、その設定にかかわらずMongoid::Errors::DocumentNotFoundを投げることが分かる。

find_by

find_byの実装は同ファイル中のlib/mongoid/findable.rb#L113にある。

    # Find the first +Document+ given the conditions.
    # If a matching Document is not found and
    # Mongoid.raise_not_found_error is true it raises
    # Mongoid::Errors::DocumentNotFound, return null nil elsewise.
    #
    # @example Find the document by attribute other than id
    #   Person.find_by(:username => "superuser")
    #
    # @param [ Hash ] attrs The attributes to check.
    #
    # @raise [ Errors::DocumentNotFound ] If no document found
    # and Mongoid.raise_not_found_error is true.
    #
    # @return [ Document, nil ] A matching document.
    #
    # @since 3.0.0
    def find_by(attrs = {})
      result = where(attrs).find_first
      if result.nil? && Mongoid.raise_not_found_error
        raise(Errors::DocumentNotFound.new(self, attrs))
      end
      yield(result) if result && block_given?
      result
    end

find_byの挙動はドキュメントによると

Criteria#find_by

Find a document by the provided attributes, and if not found raise an error or return nil *depending on the * raise_not_found_error configuration option.

とのことだが、なるほど確かに実装をみると条件分岐中でMongoid.raise_not_found_errorの値をチェックしているのが分かる。

find

ここまではよろしかったが、このfindが問題である。findは実装上複数のメソッドに分けて隠蔽されていて、少し細かくみていかねばならない。

find自体の実装はまずlib/mongoid/criteria/findable.rb#L37に記述されている

      # Find the matchind document(s) in the criteria for the provided ids.
      #
      # @example Find by an id.
      #   criteria.find(BSON::ObjectId.new)
      #
      # @example Find by multiple ids.
      #   criteria.find([ BSON::ObjectId.new, BSON::ObjectId.new ])
      #
      # @param [ Array<BSON::ObjectId> ] args The ids to search for.
      #
      # @return [ Array<Document>, Document ] The matching document(s).
      #
      # @since 1.0.0
      def find(*args)
        ids = args.__find_args__
        raise_invalid if ids.any?(&:nil?)
        for_ids(ids).execute_or_raise(ids, args.multi_arged?)
      end

処理の中心になるexecute_or_raiseはその上の行で定義されている。

      # Execute the criteria or raise an error if no documents found.
      #
      # @example Execute or raise
      #   criteria.execute_or_raise(id)
      #
      # @param [ Object ] args The arguments passed.
      #
      # @raise [ Errors::DocumentNotFound ] If nothing returned.
      #
      # @return [ Document, Array<Document> ] The document(s).
      #
      # @since 2.0.0
      def execute_or_raise(ids, multi)
        result = multiple_from_db(ids)
        check_for_missing_documents!(result, ids)
        multi ? result : result.first
      end

さらにcheck_for_missing_documents!が出てくる。
定義先はlib/mongoid/criteria.rb#L455らしい。

    # Are documents in the query missing, and are we configured to raise an
    # error?
    #
    # @api private
    #
    # @example Check for missing documents.
    #   criteria.check_for_missing_documents!([], [ 1 ])
    #
    # @param [ Array<Document> ] result The result.
    # @param [ Array<Object> ] ids The ids.
    #
    # @raise [ Errors::DocumentNotFound ] If none are found and raising an
    #   error.
    #
    # @since 3.0.0
    def check_for_missing_documents!(result, ids)
      if (result.size < ids.size) && Mongoid.raise_not_found_error
        raise Errors::DocumentNotFound.new(klass, ids, ids - result.map(&:_id))
      end
    end

result配列が空なら例外を上げるという簡単な挙動であることが分かる。
しかし、ここでfindメソッドに関するドキュメントを見てみたい。

Criteria#find

Find a document or multiple documents by their ids. Will raise an error by default if any of the ids do not match.

つまり、findメソッドも「デフォルトでは例外を上げる」という説明があるのだが、はたしてその挙動がraise_not_found_errorオプションの影響を受けるということが書いていないので、注意が必要である。

まとめ

例外を上げる 例外をあげない
raise_not_found_error: true find, find_by, find_by!, (なし)
raise_not_found_error: false find_by! find, find_by
ActiveRecordと違って、raise_not_found_errorというオプションがあるので注意して使いたい:fist:
8
4
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
8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?