【自戒】継承関係を把握しようという話。
はじめに
こんにちは!大ちゃんの駆け出し技術ブログです。
2月の毎日投稿達成まで本記事を合わせて残すところ2つとなりました。
正直に言うと最近はPF課題で詰まってましたので、新しい知識が学べない状況でして、そろそろネタ切れ感がうっすらと伝わってないか心配ですw
今回の記事は「継承関係はしっかり意識しよう」です。これは自戒の意味を込めて記事にしようと思い至りました。継承関係なんて超初学者向けと思われますが、当然自分も継承関係自体は理解していました。しかし、継承関係は視野が狭いととても見えづらいものになります。
この記事を書こうと思った発端はまさにこの継承関係を把握してなかったことから起こりました。継承関係を把握しておらず簡単な実装ができない理由を探すのに1日以上詰まってしまったお話を自戒の念を込めてさせていただきます!
またAPIについてよく理解していない方はまずはAPIについて勉強してください!API知らないと理解できないので。。。
どのような実装をしたいのか
本記事ではRUNTEQの課題のコードを使用しますので、コード自体はあまり見せられませんがご容赦ください。テストする内容はユーザーの新規登録機能のAPIです。登録されたユーザーの情報をAPIとして取得します。
{ "data": { "id": "1", "type": "user", "attributes": { "name": "sample_name", "email": "sample@example.com" } } }
上記のJSONを取得するためにPostmanで送るパラメータは以下の画像です。
しかし、nameのパラメータを消して、emailを同じままで送ると下記のようなエラーメッセージのJSONが返ってきます。
{ "message": "Bad Request", "errors": [ "Name can't be blank", "Email has already been taken" ] }
これはUserモデルにてバリデーションが設定されているからです。
# app/model/user.rb class User < ApplicationRecord ・ validates :name, presence: true validates :email, presence: true, uniqueness: true ・ end
今回実装方法に苦闘したのはまさにこの部分です。自分が実装したかった内容は、「JSONのerrorsにバリデーションエラーを表示させる」ことです。
現状のエラーハンドリング
実際のところ、パラメータが誤っている時下記JSONを出力するところまでは実装できました。
{ "message": "Bad Request", "errors": [ "ActionController::BadRequest" ] }
このエラーはどのように表示しているのかというと、まず間違ったパラメータが渡った時にraise
メソッドで例外を明示的に起こすようにしました。@user.save
がfalse
であれば例外が発生します。
# app/controllers/api/v1/registrations_controller.rb raise ActionController::BadRequest unless @user.save
ActionController::BadRequest
は別ファイルでrescue_from
でエラーハンドリングをしています。
これにより、ActionController::BadRequest
のエラーが発生した時に行う処理をメソッドで設定することができます。今回のファイルでいえば、rescue400
がActionController::BadRequest
発生時に行う処理です。
module ApiErrorHandle extend ActiveSupport::Concern included do rescue_from ActionController::BadRequest, with: :rescue400 end ・ ・ ・ def rescue400(exception = nil, messages = nil) render_error(400, 'Bad Request', exception&.message, *messages) end private def render_error(code, message, *error_messages) response = { message: message, errors: error_messages.compact } render json: response, status: code end
rescue400
のメソッドは基本的にprivate
メソッドであるrender_error
とほぼ同じです。しかし、ActionController::BadRequest
が発生しているので、引数にHTTPステータスコードである400と、エラーメッセージ'Bad Request'をそれぞれ指定しています。
第三引数について、exception
の中身は#<ActionController::BadRequest: ActionController::BadRequest>となっておりnil
ではないのでexception&.message
でエラーは起きず"ActionController::BadRequest"を返しています。第四引数のmessages
はrescue400
メソッドの第二引数デフォルト値nil
を返しています。
[1] pry(#<Api::V1::RegistrationController>)> exception => #<ActionController::BadRequest: ActionController::BadRequest> [2] pry(#<Api::V1::RegistrationController>)> exception&.message => "ActionController::BadRequest" [3] pry(#<Api::V1::RegistrationController>)> messages => nil
そして、render400
メソッドの中で実行されるrender_error
メソッドですが、上述した通り、第一引数は400で第二引数は'Bad Request'です。第三引数である*error_messages
はexception&.message
で出力された"ActionController::BadRequest"のみですので、要素数が一つの配列で["ActionController::BadRequest"]と出力されます。
[1] pry(#<Api::V1::RegistrationController>)> code => 400 [2] pry(#<Api::V1::RegistrationController>)> message => "Bad Request" [3] pry(#<Api::V1::RegistrationController>)> error_messages => ["ActionController::BadRequest"]
よって変数responseの中身は以下のようになります。
[1] pry(#<Api::V1::RegistrationController>)> response => {:message=>"Bad Request", :errors=>["ActionController::BadRequest"]}
render json: response, status: code
でresponse
がレンダーされるわけですので、現状のJSONファイルがレンダリングされるわけです。
[再掲]
{ "message": "Bad Request", "errors": [ "ActionController::BadRequest" ] }
この時の自分は完全にこのエラーハンドリングのロジックを理解していました。そこまで難しいわけでもなかったですし、このエラーハンドリングの実装自体は1つ、2つ前の課題でも実装済みです。
実際、何が問題かは把握していました。現在取得しているエラーメッセージは引数で指定したものですが、これをモデルのバリデーションエラーのエラーメッセージで指定できれば、バリデーションエラーをJSON形式で表示できるはずです。
{ "message": "Bad Request", "errors": [ "Name can't be blank", "Email has already been taken" ] }
しかしながら、それの実装方法がわからなかったのです。バリデーションエラーをどう取得していいのかがわからなかったのです。
エラーハンドリングしているモジュールはユーザー登録用のコントローラーと違うファイルにあるので、モジュール内で@user.errors.full_messages
は取得できません。ですので、コントローラー内でエラーハンドリングをする必要があるのですが、エラーハンドリングようのモジュールを設けているのでコントローラーにも別でエラーハンドリングのロジックを記述するのは汚いコードを書くことになるのでできれば避けるべきです。
この部分で完全に自分は詰まってしまいました。
継承関係を理解する
仕方なく質問したところ講師から以下の返答をもらいました。
rescue400
のメソッドはこのエラーハンドラーのモジュール内だけでしか利用しない想定ですかね?直接このメソッドを利用したりできそうな気がしませんか?
ぴかーーーーーーーーーーーーーーーん!!!!!
脳に電光石火が走るぐらいピカんとしました笑
rescue400
メソッドはモジュール内でしか利用できないと思い込んでいました。しかし、APIのエラーハンドリングのモジュールはBaseControllerでincluede
してました。
module Api module V1 class BaseController < ApplicationController include ApiErrorHandle
そして、ユーザー登録用のコントローラーはBaseControllerを継承していることがわかりました。
class RegistrationsController < BaseController
つまり、ユーザー登録用のRegistrationsControllerでもAPIのエラーハンドリングのモジュールで定義されているメソッドを使用可能ということです!!!!これがわかった時本当に気持ちよかったです!
下記のように定義していたcreateアクションを変更しました。
def create @user = User.new(user_params) raise ActionController::BadRequest unless @user.save json_string = UserSerializer.new(@user).serialized_json render json: json_string end
↓
def create @user = User.new(user_params) if @user.save json_string = UserSerializer.new(@user).serialized_json render json: json_string else rescue400(nil, @user.errors.full_messages) end end
これで再度誤ったパラメータ(nameのパラメータなし、emailを登録されたものにする)を送信すると、期待した通りのJSONが返却されました!!
{ "message": "Bad Request", "errors": [ "Name can't be blank", "Email can't be blank" ] }
なんで解決できないかわからなかったのですが、本当に簡単に解決できましたね。
めちゃくちゃスッキリしました笑
終わりに
今回なぜこのように詰まったのかというと、親クラスがmodule
を継承していたことを完全に失念していたことにあります。本当に深く反省しています。そのためこの記事を書きましたし。
自戒の念もしっかり込めたのでこれぐらいで勘弁していただければと思いますww
さてさて、明日はいよいよ28日目の投稿!長かった毎日投稿も明日で最後です!
明日の記事ですが、技術ブログではなくブログの続け方について書いていこうと思います!
以上、大ちゃんの駆け出し技術ブログでした!