Rails+MongoDBにて
RailsからDBアクセスした際、DBにレコードがなかった時に
本来はエラーメッセージを画面内に設定するなど、
次のアクションを起こしたいところを
エラー画面DocumentNotFoundが出た時の対策です。
ローカル環境の例
ログインを行い、DBにレコードがあった場合、
次画面遷移を行う。なければエラーメッセージを出力し
再度ログイン画面を読み込む。
という処理を行いたい想定です。
def Login
@user = User.find_by(name: params[:name])
if @user && @user.authenticate(params[:password])
# 次画面遷移
redirect_to("/Hoge")
else
# エラーメッセージだす
@errorMessage = "ログインに失敗"
render("/top")
end
end
再度ログイン画面を読み込みたいところ、謎のDocumentNotFoudエラー発生です。。
原因
この例では、DBを検索して一件のみを取得する際に、find_byを使用して検索を行なっていますが、ここでエラーがおきています。
ググった結果、mongoDBのfind_byメソッドにエラーの設定がされています。
[MongoDB公式ドキュメントのfind_byのクエリメソッドの実装]
(https://github.com/mongodb/mongoid/blob/71c29a80599087008ecfbd057d3d049c2153c7ce/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
コメントをみると、
# @raise [ Errors::DocumentNotFound ] If no document found
# and Mongoid.raise_not_found_error is true.
とあります。レコードが存在しなくMongoid.ymlのraise_not_found_errorがtrueの場合はDocumentNotFoundが出力されます。
対策(ローカル環境編)
config/mongoid.yml
の設定を見てみる
development:
clients:
default:
・・・
options:
・・・
# Raise an error when performing a #find and the document is not found.
# (default: true)
#raise_not_found_error: true
デフォルトではraise_not_found_errorはtrueに設定されているため、DocumentNotFoundが出力される・・・
ここをfalseにします。
#raise_not_found_error: false
また、設定ファイルを反映するためには
ローカルのサーバを再起動させる必要があります。
$ rails s
動作確認(ローカル環境)
無事DocumentNotFoundにならなくなりました。
heroku環境の例と対策
次にローカル以外の環境の例です
ここではherokuでの例をあげます
↓存在しないユーザでログイン
ページが存在しないエラーになりました。
コマンドにてログを確認します。
(事前にherokuにログインが必要です$ heroku login
)
$ heroku logs -t
2018-12-15T08:50:55.791954+00:00 app[web.1]: F, [2018-12-15T08:50:55.791887 #4] FATAL -- : [a510ff0f-8ab4-4197-a560-8ef1fc93e3b5] Mongoid::Errors::DocumentNotFound (
2018-12-15T08:50:55.791957+00:00 app[web.1]: message:
2018-12-15T08:50:55.791960+00:00 app[web.1]: Document not found for class Like with attributes {:user_id=>BSON::ObjectId('hoge'), :post_id=>BSON::ObjectId('piyo')}.
2018-12-15T08:50:55.791961+00:00 app[web.1]: summary:
2018-12-15T08:50:55.791963+00:00 app[web.1]: When calling Like.find_by with a hash of attributes, all attributes provided must match a document in the database or this error will be raised.
2018-12-15T08:50:55.791965+00:00 app[web.1]: resolution:
2018-12-15T08:50:55.791967+00:00 app[web.1]: Search for attributes that are in the database or set the Mongoid.raise_not_found_error configuration option to false, which will cause a nil to be returned instead of raising this error.):
この例では次画面遷移時にLikeというコレクションを読んでいますが、その際にMongoid::Errors::DocumentNotFoundが発生しています。原因がLike.find_byを実行時にレコードがなかったためにエラってます。対策としてデータを用意するか、mongoid.ymlのraise_not_found_errorオプションをfalseにすると良い。と書いています。
ここではデータがない場合も想定しているのでMongoid.ymlのraise_not_found_error:falseにします。
デフォルトではoptions自体がないので追記します。
(optionsの階層に注意してください。
productionの直下です。clientsの直下ではないです。)
production:
clients:
default:
uri: mongodb://heroku_9v5bk0vx:fuga@hage.mlab.com:37977/heroku_9v5bk0vx
options:
connect_timeout: 15
options:
raise_not_found_error: false
mongoid.ymlを設定後にherokuにデプロイ
動作確認(heroku環境)
herokuでも無事DocumentNotFoundにならなくなりました。
環境
(今回のエラーはvarsionは関係ないですが載せておきます)
Rails 5.2.1
ruby 2.3.7p456
MongoDB v4.0.2
参照
Mongoidの隠れクエリメソッドfind_by!とraise_not_found_errorオプションの関係
ありがとうございます。