JavaRush /Java Blog /Random-TW /REST API 和資料驗證
Денис
等級 37
Киев

REST API 和資料驗證

在 Random-TW 群組發布
連結到第一部分: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'
我們將從必須處理的實際數據開始。讓我們返回持久性包並開始填充實體。正如你所記得的,我們讓它只存在一個字段,然後透過`@GenerateValue(strategy = GenerationType.IDENTITY)`產生自動讓我們記住第一章中的技術規格:
{
  "firstName": String,
  "lastName": String,
  "department": String,
  "salary": String
  "hired": String //"yyyy-mm-dd"
  "tasks": [
  ]
}
我們第一次有足夠的字段,所以讓我們開始實現它。前三個欄位不會引起問題 - 這些是普通行,但薪資欄位已經具有暗示性。為什麼是實際線路?在實際工作中,這種情況也會發生,客戶來找你並說 - 我想向你發送這個有效負載,然後你處理它。當然,您可以聳聳肩並這樣做,您可以嘗試達成協議並解釋說最好以所需的格式傳輸資料。讓我們想像一下,我們遇到了一個聰明的客戶端,並同意最好以數字格式傳輸數字,由於我們正在談論金錢,因此將其設定為 Double。我們有效負載的下一個參數將是僱用日期,客戶將以約定的格式將其發送給我們:yyyy-mm-dd,其中 y 負責年份,m 負責天數,d 預計負責天數 - 2022- 08-12。目前的最後一個欄位將是分配給客戶端的任務清單。顯然,Task 是我們資料庫中的另一個實體,但我們對它還不太了解,所以我們將像之前對 Employee 所做的那樣創建最基本的實體。我們現在唯一可以假設的是,可以將多個任務分配給一名員工,因此我們將應用所謂的一對多方法,即一對多的比例。詳細來說,員工表中的一筆記錄可以對應任務表中的多筆記錄。我還決定添加一個 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<>();
}
為任務實體建立了以下類別:
@Entity
@Data
@Accessors(chain = true)
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long taskId;
}
正如我所說,我們不會在Task 中看到任何新內容,還為該類創建了一個新的存儲庫,它是Employee 的存儲庫的副本- 我不會給出它,您可以自行創建它以此類推。但談論 Employee 類別是有意義的。正如我所說,我們添加了幾個字段,但現在只有最後一個字段令人感興趣 - 任務。這是一個 List<Task>tasks,它立即用一個空的 ArrayList 初始化並用幾個註解標記。1. @OneToMany正如我所說,這將是我們的員工與任務的比率。2. @JoinColumn - 將連接實體的欄位。在這種情況下,將在任務表中建立一個employee_id列,指向我們員工的id;它將作為ForeighnKey為我們服務,儘管這個名字看起來很神聖,但您可以將該列命名為您喜歡的任何名稱。如果您不僅需要使用 ID,還需要使用某種真實的列,情況會稍微複雜一些;我們稍後會討論這個主題。3. 您可能也注意到 id 上方有一個新註解 - @JsonIgnore。由於 id 是我們的內部實體,因此我們不一定需要將其傳回給客戶端。4. @NotBlank 是用於驗證的特殊註解,表示該欄位不能為 null 或空字串 5. @Column(unique = true) 表示該列必須具有唯一值。所以,我們已經有了兩個實體,它們甚至是相互連結。是時候將它們整合到我們的程式中了 - 讓我們來處理服務和控制器。首先,讓我們從 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 請求。一般來說,我認為由於對該控制器的所有請求都會到達一個端點,所以讓我們稍微簡化一下。還記得我們在 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.graddle 實現'org.springframework. boot:spring-boot-starter -validation' 我們的老朋友 Service 和 requiredArgsConstructor 已經告訴我們關於這個類別需要知道的一切,還有一個私有的最終驗證器欄位。他會施展魔法。我們建立了 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());
    }
}
注意新的最終屬性 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 兩種方法。順便說一句,關於端點的一些有趣的資訊。例如,為什麼我們不為 GET 和 POST 分配單獨的端點,例如 getAllEmployees 或 createEmployees?一切都非常簡單——用一個點來處理所有請求會更加方便。路由基於 HTTP 方法進行,而且很直觀,無需記住 getAllEmployees、getEmployeeByName、get...update...create...delete... 的所有變體,讓我們測試一下我們得到了什麼。我已經在上一篇文章中寫道,我們需要 Postman,現在是時候安裝它了。在程式介面中,我們建立一個新的POST請求 REST API 和資料驗證 - 2 並嘗試發送它。如果一切順利,我們將在螢幕右側看到 Status 201。但例如,發送了相同的內容但沒有唯一的號碼(我們對此進行了驗證),我得到了不同的答案:好吧,讓我們檢查一下我們的完整選擇是如何工作的- 我們為同一端點 REST API 和資料驗證 - 3 創建一個GET 方法並發送它。 REST API 和資料驗證 - 4 我真誠地希望您一切順利,就像我一樣,我們下一篇文章再見。
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION