Closed26

バリデーションはどこで行うべきか?

mikannmikann

バリデーションの実装箇所について一度まとめてみる
SPA+RestAPIのWEBアプリを想定

mikannmikann

調査前予想

  • フロント
    • UXを考えるとフロント側でのバリデーションは必要(DBアクセスが必要ないものに限る)
  • API
    • DBアクセスが必要ないもの
      • controllerのFormバリデーション
    • DBアクセスが必要なもの
      • Service
mikannmikann

調査後結論

  • フロント
    • フロント側でのバリデーションは必要
    • 基本的にDBアクセスが必要ないものに限る
  • API
    • DBアクセスが必要ないもの
      • ControllerのFormバリデーション
    • DBアクセスが必要なもの
      • Service
      • フィールドごとにメッセージを出力したい場合は、Serviceに実装し、Formのカスタムバリデーションで呼び出す
mikannmikann

値オブジェクトで実装する方が、入力(controller)で実装するよりも安全性が高いっていうのは確かにそうだわ

mikannmikann

現在、この世で動いているシステムのほとんどは、一つの入力フォームに対して、フロントとサーバ両方でバリデーションをかけていて、その用法が主流のようです。
どういうことかというと、一つの入力データに対して、フロント、そしてサーバと2つの側面からチェックを働かせているのです。

そうなのね
まあSPA+APIならどちらもやるしかあるまいて

https://qiita.com/isaaac/items/4ca28057a45dddb14a64

mikannmikann

みんなの回答まとめ

  • サーババリデーションは必須
    • これがないとシステムがバグる
  • クライアントバリデーションは必須ではないがあったほうがよい
    • クライアントバリデーションも入れることでUXを最大化する
    • サーバサイドに合わせる
    • 理想はサーバサイドと同等、そうでない場合はサーバサイドより常に軽めに

https://jp.quora.com/クライエント側のバリデーションとサーバーサイド

mikannmikann

次はフロントではDBアクセスが必要なバリデーションは行わない(仮説)について調べるか
経験則に過ぎないが、

  • 実装が面倒(バリデーションごとにAPI作成?)
  • 例えばユーザーメールアドレスは一意であるべきというのをフロントが管理しているのは少々ファットに思える
  • バリデーション実行時とDBへの登録時のタイムラグを考えると、フロントで通ってもサーバ側で通るとは限らない
mikannmikann

データベースの状態を用いた入力値のチェックをしたい場合があります。 データベースの状態チェックはトランザクション配下で行ったほうが良いケースがあるため、Bean Validationを使用するのではなくServiceなどでバリデーションを行うことをおすすめします。

まあそうだよね

https://fintan-contents.github.io/spring-crib-notes/latest/html/web/validation/database-validation.html

mikannmikann

ちょっと調べた感じ最初の認識で問題なさそう
ちらほらFormでDBアクセスしているような記事もあったけど…思想の問題か?

それにしても今回もTransactionか
そろそろ調べんとな

mikannmikann

問題はserviceでDBアクセスが必要なバリデーション→エラーがあった場合にどうフロントで表示するかなんだよなあ

直近ではserviceで例外発生→controllerでキャッチ&エラーメッセージで判別→フロントに文字列として返すってやったけど流石にもっといい方法あるだろ

mikannmikann

DBアクセスが必要なバリデーションをフロントでも行う場合があるみたい。
例)インスタのアカウント登録画面

mikannmikann

※正確には、登録処理とは別にバリデーション用のAPIを呼び出している

mikannmikann

お!DBアクセスが必要ならドメイン層でやるって書いてある!

以下引用

ユーザーが入力した値が不正かどうかを検証することは必須である。 入力値の検証は大きく分けて、

1.長さや形式など、文脈によらず入力値だけを見て、それが妥当かどうかを判定できる検証
2.システムの状態によって入力値が妥当かどうかが変わる検証
がある。

1.の例としては必須チェックや、桁数チェックがあり、2.の例としては 登録済みのE-mailかどうかのチェックや、注文数が在庫数以内であるかどうかのチェックが挙げられる。

本節では、基本的には前者のことを説明し、このチェックのことを「入力チェック」を呼ぶ。 後者のチェックは「業務ロジックチェック」と呼ぶ。業務ロジックチェックについては ドメイン層の実装を参照されたい。

本ガイドラインでは、基本的に入力チェックをアプリケーション層で行い、 業務ロジックチェックは、ドメイン層で行うことをポリシーとする。

Webアプリケーションの入力チェックには、サーバサイドで行うチェックと、クライアントサイド(JavaScript)で行うチェックがある。 サーバーサイドのチェックは必須であるが、クライアントサイドでも同じチェックを実施すると、 サーバー通信なしでチェック結果が分かるため、ユーザビリティが向上する。

http://terasolunaorg.github.io/guideline/current/ja/ArchitectureInDetail/WebApplicationDetail/Validation.html#overview

mikannmikann

後日追記

  • ビジネスルールのエラーをフィールド毎に出力する必要がある場合、Controller側の仕組みを利用する
  • チェックロジック自体はServiceとして実装し、Bean ValidationからServiceのメソッドを呼び出す方式で実現することを推奨する

とのこと。つまり、フィールドごとにメッセージを表示したい場合は、FormでDBアクセスも許容するということ。

https://terasolunaorg.github.io/guideline/current/ja/ImplementationAtEachLayer/DomainLayer.html#tips-business-error-label

mikannmikann

このサイトも参考になりそう
今まではSPAを前提としたサイトじゃなかったし

https://fintan-contents.github.io/spa-restapi-guide/

mikannmikann

正にこれ
以下引用

フロントエンドのUIで入力された値のバリデーションは、フロントエンド・バックエンドの両方で行います。

バリデーションは次の2つのパターンがあります。

  1. フロントエンドでのバリデーションに加え、バックエンドでも同様のバリデーションを行う
  2. フロントエンドではバリデーションが行えず、バックエンドでのバリデーションのみを行う
mikannmikann

これ以上詳しいことは書いてない?
↓見る限りは、serviceで例外起こして、エラーメッセージいい感じに詰めて返せばいいのかな?それが難しい!どうやってやるんや

mikannmikann

↓これ見る感じ、エラーメッセージをレスポンスに詰めて返せばよさそう
ただ例外を一回controllerで受け取るのか、serviceで出しっぱなしにするのかはわからん

https://qiita.com/suin/items/f7ac4de914e9f3f35884

mikannmikann

最終的なコード

@Slf4j
@RestControllerAdvice
public class ApiControllerAdvice {

    @ExceptionHandler(InvalidArgumentException.class)
    public ResponseEntity<ValidationErrorResponse> handleInvalidArgument(InvalidArgumentException ex) {

        log.warn(ex.getMessage(), ex);

        ValidationErrorResponse re = new ValidationErrorResponse(
                BAD_REQUEST.value(),
                BAD_REQUEST.getReasonPhrase(),
                ex.getErrors().stream().map(error ->
                        new ValidationErrorResponse.Error(error.getMessage(), error.getField())).toList());

        return new ResponseEntity<>(re, BAD_REQUEST);
    }

}
このスクラップは2023/01/15にクローズされました