JavaRush /Java Blog /Random-JA /REST API とデータ検証
Денис
レベル 37
Киев

REST API とデータ検証

Random-JA グループに公開済み
最初の部分へのリンク: REST API と次のテスト タスク さて、アプリケーションは動作しており、アプリケーションから何らかの応答を得ることができますが、これで何が得られるでしょうか? それは何の有益な仕事もしません。言うまでもなく、便利なものを実装しましょう。まず最初に、build.gradle にいくつかの新しい依存関係を追加しましょう。これらは役に立つでしょう。
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.apache.commons:commons-collections4:4.4'
implementation 'org.springframework.boot:spring-boot-starter-validation'
そして、処理する必要がある実際のデータから始めます。永続化パッケージに戻って、エンティティの入力を開始しましょう。覚えているとおり、フィールドは 1 つだけ存在するようにしておき、自動は `@GeneratedValue(strategy = GenerationType.IDENTITY)` によって生成されます。最初の章の技術仕様を思い出してください。
{
  "firstName": String,
  "lastName": String,
  "department": String,
  "salary": String
  "hired": String //"yyyy-mm-dd"
  "tasks": [
  ]
}
初めて十分なフィールドが揃ったので、実装を開始しましょう。最初の 3 つのフィールドは問題を引き起こしません。これらは普通の行ですが、給与フィールドはすでに示唆に富んでいます。なぜ実際の行なのでしょうか?実際の仕事でも、これは起こります。顧客があなたのところに来て、「このペイロードを送信したい」と言い、あなたはそれを処理します。もちろん、肩をすくめてそうすることもできますし、合意に達して、必要な形式でデータを送信する方が良いと説明することもできます。賢いクライアントに出会って、数値を数値形式で送信する方が良いということに同意したとします。ここではお金について話しているので、これを Double にします。ペイロードの次のパラメータは雇用日です。クライアントはそれを合意された形式 yyyy-mm-dd で送信します。ここで、y は年、m は日数、d は期待日 - 2022- を表します。 08-12。現時点での最後のフィールドは、クライアントに割り当てられたタスクのリストになります。明らかに、Task はデータベース内の別のエンティティですが、それについてはまだよくわかっていないため、以前に Employee で行ったように、最も基本的なエンティティを作成します。現時点で想定できるのは、1 人の従業員に複数のタスクを割り当てることができるということだけなので、いわゆる 1 対多のアプローチ、つまり 1 対多の比率を適用します。より具体的には、employeeテーブルの 1 つのレコードは、 taskテーブルの複数のレコードに対応することができます。また、従業員を明確に区別できるように、uniqueNumber フィールドなどを追加することにしました。現時点では、Employee クラスは次のようになります。
@Entity
@Data
@Accessors(chain = true)
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @JsonIgnore
    Long id;

    @NotBlank
    @Column(unique = true)
    private String uniqueNumber;
    private String firstName;
    private String lastName;
    private String department;
    private Double salary;
    private LocalDate hired;

    @OneToMany
    @JoinColumn(name = "employee_id")
    List<Task> tasks = new ArrayList<>();
}
次のクラスが Task エンティティ用に作成されました。
@Entity
@Data
@Accessors(chain = true)
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long taskId;
}
先ほども言ったように、Task には新しいものは何も表示されません。このクラス用にも新しいリポジトリが作成されました。これは Employee のリポジトリのコピーです。私はそれを与えません。類推して自分で作成できます。しかし、Employee クラスについて話すのは理にかなっています。前述したように、いくつかのフィールドを追加しましたが、現在重要なのは最後のフィールドであるタスクだけです。これはList<Task> タスクであり、空の ArrayList ですぐに初期化され、いくつかの注釈が付けられます。1. @OneToMany先ほども言いましたが、これはタスクに対する従業員の比率になります。2. @JoinColumn - エンティティを結合する列。この場合、従業員の ID を指すemployee_id 列が Task テーブルに作成され、ForeignKey として機能します。名前の神聖さにもかかわらず、列には好きな名前を付けることができます。ID だけでなく、何らかの実際の列を使用する必要がある場合、状況はもう少し複雑になります。このトピックについては後ほど説明します。3. ID の上に新しい注釈 @JsonIgnore があることに気づいたかもしれません。ID は内部エンティティであるため、必ずしもクライアントに返す必要はありません。4. @NotBlank は検証用の特別なアノテーションで、フィールドが null または空の文字列であってはいけないことを示します。 5. @Column(unique = true) は、この列が一意の値を持つ必要があることを示します。したがって、すでに 2 つのエンティティがあり、それらは相互に接続されています。それらをプログラムに統合する時期が来ました。サービスとコントローラーを扱いましょう。まず最初に、 getAllEmployees() メソッドからスタブを削除し、実際に機能するものに変えてみましょう。
public List<Employee> getAllEmployees() {
       return employeeRepository.findAll();
   }
したがって、私たちのリポジトリは、データベースから利用可能なものをすべて集めて、それを私たちに提供します。タスクリストも選択されることは注目に値します。しかし、それをかき集めるのは確かに興味深いですが、そこに何もないのにそれをかき集めるとは何でしょうか?そうです、つまり、そこに何かを配置する方法を考える必要があるということです。まず、コントローラーに新しいメソッドを作成しましょう。
@PostMapping("${application.endpoint.employee}")
    public ResponseEntity<?> createOrUpdateEmployee(@RequestBody final Employee employee) {
        try {
            employeeService.save(employee);
        } catch (ValidationException e) {
            return ResponseEntity.badRequest().body(e.getViolations());
        }
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
これは @PostMapping です。従業員のエンドポイントに届く POST リクエストを処理します。一般に、このコントローラーへのすべてのリクエストは 1 つのエンドポイントに届くため、これを少し単純化しましょうと考えました。application.yml の素晴らしい設定を覚えていますか? それらを修正しましょう。アプリケーションセクションは次のようになります。
application:
  endpoint:
    root: api/v1
    employee: ${application.endpoint.root}/employees
    task: ${application.endpoint.root}/tasks
これは私たちに何をもたらすのでしょうか?コントローラー内で特定のメソッドごとのマッピングを削除でき、エンドポイントが@RequestMapping("${application.endpoint.employee}")アノテーションのクラス レベルで設定されるという事実。 これが今の美しさです。私たちのコントローラー:
@RestController
@RequestMapping("${application.endpoint.employee}")
@RequiredArgsConstructor
public class EmployeeController {

    private final EmployeeService employeeService;

    @GetMapping
    public ResponseEntity<List<Employee>> getEmployees() {
        return ResponseEntity.ok().body(employeeService.getAllEmployees());
    }

    @PostMapping
    public ResponseEntity<?> createOrUpdateEmployee(@RequestBody final Employee employee) {
        try {
            employeeService.save(employee);
        } catch (ValidationException e) {
            return ResponseEntity.badRequest().body(e.getViolations());
        }
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
}
ただし、先に進みましょう。createOrUpdateEmployee メソッドでは具体的に何が起こるのでしょうか? 明らかに、employeeService には save メソッドがあり、これがすべての保存作業を担当します。このメソッドがわかりやすい名前の例外をスローできることも明らかです。それらの。何らかの検証が行われています。そして答えは、検証結果が 201 Created になるか、問題のリストを含む 400 badRequest になるかに直接依存します。今後については、これは新しい検証サービスであり、受信データに必須フィールド (@NotBlank を覚えていますか?) が存在するかどうかをチェックし、その情報が私たちに適しているかどうかを判断します。保存方法に進む前に、このサービスを検証して実装しましょう。これを行うには、サービスを入れる別の検証パッケージを作成することを提案します。
import com.example.demo.exception.ValidationException;
import com.example.demo.persistence.entity.Employee;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validator;

@Service
@RequiredArgsConstructor
public class ValidationService {

    private final Validator validator;

    public boolean isValidEmployee(Employee employee) throws ValidationException {
        Set<Constraintviolation<Employee>> constraintViolations = validator.validate(employee);

        if (CollectionUtils.isNotEmpty(constraintViolations)) {
            throw new ValidationException(buildViolationsList(constraintViolations));
        }
        return true;
    }

    private <T> List<Violation> buildViolationsList(Set<Constraintviolation<T>> constraintViolations) {
        return constraintViolations.stream()
                                   .map(violation -> new Violation(
                                                   violation.getPropertyPath().toString(),
                                                   violation.getMessage()
                                           )
                                   )
                                   .toList();
    }
}
クラスが大きすぎることが判明しましたが、パニックにならないでください。すぐに解決します:) ここでは、既製の検証ライブラリ javax.validation のツールを使用します。このライブラリは、私たちが作成した新しい依存関係から来まし 。 build.gradle 実装「org.springframework.boot:spring-boot-starter -validation」 に追加されました。私たちの古い友人である Service と RequiredArgsConstructor は、このクラスについて知っておくべきことをすべてすでに教えてくれています。また、プライベートの Final Validator フィールドもあります。彼は魔法をかけてくれるでしょう。isValidEmployee メソッドを作成し、それに Employee エンティティを渡すことができます。このメソッドは ValidationException をスローしますが、これについては後ほど説明します。はい、これは私たちのニーズに対するカスタム例外になります。validator.validate(employee) メソッドを使用して、ConstraintViolation オブジェクトのリストを取得します。これは、前に追加した検証アノテーションとの不一致すべてです。さらなるロジックは単純です。このリストが空でない場合 (つまり、違反がある場合)、例外をスローして違反のリストを作成します (buildViolationsList メソッド)。これは汎用メソッドであることに注意してください。さまざまなオブジェクトの違反リストを操作できます。将来、何か他のものを検証する場合に役立つ可能性があります。このメソッドは実際に何をするのでしょうか? ストリーム API を使用して、違反のリストを調べます。Map メソッド内の各違反を独自の違反オブジェクトに変換し、結果として得られるすべてのオブジェクトをリストに収集します。私たちは彼を返します。私たち自身の違反の対象は他に何があるでしょうか?簡単な記録です
public record Violation(String property, String message) {}
レコードは、ロジックなどを必要とせずにデータを含むオブジェクトが必要な場合に備えて、Java における特別なイノベーションです。なぜこのようなことになったのかは私自身もまだ理解できていませんが、便利なこともあります。通常のクラスと同様に、別のファイルに作成する必要があります。カスタム ValidationException に戻ると、次のようになります。
@RequiredArgsConstructor
public class ValidationException extends Exception {

    @Getter
    private final List<Violation> violations;
}
すべての違反のリスト、Lombok アノテーション - Getter がリストに添付され、別の Lombok アノテーションを通じて必要なコンストラクターを「実装」しました:) ここで注意すべき点は、isValid の動作を完全に正しく実装していないことです。 ... メソッドを使用すると、true または例外のいずれかを返しますが、通常の False に限定する価値があります。例外アプローチが行われるのは、このエラーをクライアントに返したいためです。つまり、ブール メソッドから true または false 以外のものを返す必要があります。純粋に内部的な検証メソッドの場合、例外をスローする必要はなく、ここではログが必要になります。ただし、EmployeeService に戻りましょう。まだオブジェクトの保存を開始する必要があります :) このクラスがどのようになるかを見てみましょう。
@Service
@RequiredArgsConstructor
public class EmployeeService {

    private final EmployeeRepository employeeRepository;
    private final ValidationService validationService;

    public List<Employee> getAllEmployees() {
        return employeeRepository.findAll();
    }

    @Transactional
    public void save(Employee employee) throws ValidationException {
        if (validationService.isValidEmployee(employee)) {
            Employee existingEmployee = employeeRepository.findByUniqueNumber(employee.getUniqueNumber());
            if (existingEmployee == null) {
                employeeRepository.save(employee);
            } else {
                existingEmployee = updateFields(existingEmployee, employee);
                employeeRepository.save(existingEmployee);
            }
        }
    }

    private Employee updateFields(Employee existingEmployee, Employee updatedEmployee) {
        return existingEmployee.setDepartment(updatedEmployee.getDepartment())
                               .setSalary(updatedEmployee.getSalary())
            				 //TODO implement tasks merging instead of replacement
                               .setTasks(updatedEmployee.getTasks());
    }
}
新しい Final プロパティ private Final ValidationService validationService に注目してください。save メソッド自体には @Transactional アノテーションが付けられているため、RuntimeException を受信した場合に変更がロールバックされます。まず最初に、先ほど作成したサービスを使用して受信データを検証します。すべてが順調に進んだ場合は、データベース内に既存の従業員が存在するかどうかを確認します (一意の番号を使用)。そうでない場合は新しいものを保存し、存在する場合はクラス内のフィールドを更新します。そうそう、実際にはどうやって確認すればいいのでしょうか?はい、とても簡単です。新しいメソッドを Employee リポジトリに追加しました。
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    Employee findByUniqueNumber(String uniqueNumber);
}
注目すべき点は何ですか? ロジックや SQL クエリは作成しませんでしたが、ここから入手できます。Spring は、メソッドの名前を読み取るだけで、必要なものを決定します。つまり、ByUniqueNumber を見つけて、対応する文字列をメソッドに渡します。フィールドの更新に戻ります。ここでは常識を働かせて、部門、給与、およびタスクのみを更新することにしました。これは、名前の変更は許容されることですが、まだあまり一般的ではないためです。そして、採用日の変更は一般的に物議を醸す問題です。ここで何をしたら良いでしょうか?タスク リストを結合しますが、まだタスクがなく、タスクを区別する方法がわからないため、TODO のままにします。フランケンシュタインを起動してみましょう。何も記述し忘れていなければ、うまくいくはずですが、まず、取得したクラス ツリーは次のとおりです。 REST API とデータ検証 - 1 変更されたクラスは青でハイライトされ、新しいクラスは緑でハイライトされています。作業を行うと、このような表示が得られます。 git リポジトリを使用しますが、git はこの記事のトピックではないため、これについては詳しく説明しません。したがって、現時点では GET と POST という 2 つのメソッドをサポートする 1 つのエンドポイントがあります。ところで、エンドポイントに関する興味深い情報がいくつかあります。たとえば、getAllEmployees や createEmployees など、GET と POST に別個のエンドポイントを割り当てなかったのはなぜでしょうか。すべてが非常にシンプルです。すべてのリクエストに対して単一のポイントがある方がはるかに便利です。ルーティングは HTTP メソッドに基づいて行われ、直感的であるため、getAllEmployees、getEmployeeByName、get... update... create... delete... のすべてのバリエーションを覚える必要はありません。取得した内容をテストしてみましょう。前回の記事で Postman が必要になると書きましたが、今度は Postman をインストールします。プログラム インターフェイスで、新しい POST リクエストを作成し REST API とデータ検証 - 2 、送信しようとします。すべてがうまくいけば、画面の右側にステータス 201 が表示されます。しかし、たとえば、同じものを送信したものの、(検証済みの)一意の番号がない場合は、別の答えが得られます。では、完全な REST API とデータ検証 - 3 選択がどのように機能するかを確認しましょう。同じエンドポイントに対して GET メソッドを作成し、それを送信します。 。 REST API とデータ検証 - 4 私と同じように、あなたにとってもすべてがうまくいくことを心から願っています。また次の記事でお会いしましょう。
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION