【冷啟動#2】實用的springboot tutorial入門demo

dayceng發表於2024-07-04

跟著官方文件熟悉一遍建立spring工程的步驟

https://spring.io/guides/gs/spring-boot

https://juejin.cn/post/7077958723829760008

demo簡介

整個demo的預期目標是:
管理一堆玩家的資料,資料庫使用的是現成的我們虛擬機器上安裝的MySQL

專案結構參考

主要工作:

  • 建立並熟悉springboot工程
  • 基於Java提供的MySQL資料庫操控方法,封裝幾個能夠操作玩家資料的API介面

透過springboot來完成這樣一個專案,目的是熟悉其整套使用流程
專案地址:有點懶,還沒傳,過兩天先(新電腦沒環境)

玩家資料管理demo

專案需求拆解

這是一個用於實驗在springboot框架下資料庫互動的專案

專案的前端部分為瀏覽器

API層負責處理GET\POST\PUT\DELETE請求

Service層負責具體業務(對應這裡就是在springboot下與MySQL的相關互動)

DataAccess層負責對接業務與資料庫

整個系統的主要功能是對player類的屬性進行CURD

新建一個springboot專案

多版本Java共存

參考

為什麼要多版本呢?因為springboot3.x僅支援JDK17以上的Java了,但是我又不想放棄JDK8

先去這裡下載JDK安裝包

JDK8安裝

先把JDK8安裝了,其實沒什麼特殊的操作,就是設定好路徑就行

比如這裡我是在D:\coding_environment\Java路徑下分別又建立了Java8、Java17用於安裝不同版本的Java

沒什麼好說的

環境變數配置(為多版本準備)

“此電腦->屬性->高階系統設定->環境變數”

建立一堆環境變數

然後再“系統變數(S)”欄,點選"新建"建立一個新的系統變數,命名為”JAVA_HOME8“,變數值一欄填JDK8的安裝路徑,即D:\coding_environment\Java\Java8

同樣的操作,再建立一個命名為”JAVA_HOME“的系統變數,變數值設定為%JAVA_HOME8%

最後還要建立一個系統變數“CLASSPATH”,其變數值設定為.;%JAVA HOME%\lib\dt.jar;%JAVA HOME%\lib\tools.jar;

配置Path

在“系統變數(S)”欄中找到“Path”,雙擊進去,新增以下兩條內容:

%JAVA_HOME%\bin
%JAVA_HOME%\jre\bin

然後全部確定即可

測試

在cmd中輸入java -veersion即可看到版本資訊

Java多版本共存

再去下載一個JDK17,安裝到Java17目錄下

為JDK17新增環境變數

還是在“系統變數(S)”欄中,建立一個”JAVA_HOME17“的系統變數,變數值為JDK17的安裝路徑,即D:\coding_environment\Java\Java17

開啟Path,將%JAVA_HOME%\bin的優先順序放在第一位

多版本切換的方法

開啟環境變數,將“系統變數(S)”中的”JAVA_HOME“的變數值修改為對應版本即可

例如,原來用JDK8的時候是%JAVA_HOME8%,切換17只需要改成%JAVA_HOME17%即可

初始化springboot應用

初始化springboot應用需要在https://start.springboot.io/進行

在頁面中選擇專案管理工具(Project),一般用Maven

Spring Boot版本選最新的穩定版本就可以,打包方式選擇Jar包

在springboot升級到3.x之後,Java的最低版本要求已經到了17,因此Java8不可選

Dependencies部分根據需要進行選擇

  • Spring Web---提供一些API服務(RESTful)
  • Spring Data JPA---spring對訪問資料庫層的一個抽象
  • MySQL Driver---用了MySQL所以得選

勾選完成之後點選生成即可,之後會下載得到一個壓縮的專案檔案

專案匯入以及結構

解壓,用IDEA匯入工程,即:點選open,選擇解壓目錄中的pom.xml檔案

作為工程開啟後,專案的結構如下:

main目錄下放置所有的Java原始碼,透過不同的packet管理

resources目錄則用於放置前端相關的靜態檔案以及配置檔案,例如全域性配置檔案application.properties就在此處

調整配置檔案

在pom.xml中,暫時註釋掉data-jpa相關的dependencies

在Spring Boot專案中,pom.xml 就像是專案的“說明書”一樣。它告訴了計算機如何構建和管理你的專案。裡面寫著專案的基本資訊,還有它需要用到的各種工具和庫,就像是一張清單,讓你的專案能順利跑起來。---GPT3.5

調整完pom.xml需要在右側側邊欄裡面的M中重新整理一下Maven使其生效。

啟動專案

弄完之後可以到src/main/java/com/tutorial/boot_demo/BootDemoApplication.java下啟動專案

如果此時安裝的是比較old school的JDK8,那麼就會出現以下錯誤,需要切換版本

java: 警告: 源發行版 17 需要目標發行版 17

這個也不難理解,因為我們生成專案的時候選的是JDK17

因為沒有定義介面,瀏覽器訪問http://localhost:8080/會顯示以下內容

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Sun Jun 30 12:23:02 CST 2024
There was an unexpected error (type=Not Found, status=404).

這是正常的現象

編寫測試controller

那要讓它不報錯,就得有對應的API供其呼叫

因此得編寫controller測試一下,所有的API都是以controller的形式進行提供

src/main/java/com/tutorial/boot_demo下新建一個Java Class,TestController

為TestController新增@RestController註解

package com.tutorial.boot_demo;

import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {
    @GetMapping("/hello") //配置api的訪問路徑
    public String hello(){
        return "HellO WoRld";
    }
}

//@RestController
//public class TestController { /返回一個List物件的話
//    @GetMapping("/hello") //配置api的訪問路徑
//    public List<String> hello(){
//        return List.of("HellO", "WoRld");
//    }
//}

訪問http://localhost:8080/hello可以測試該介面

當你想要寫一個返回 JSON 格式資料的介面,比如說 TestController,你得在這個類上加上 @RestController 註解。這個註解告訴 Spring 框架這個類裡的方法要直接返回資料,而不是渲染成網頁或其它格式。這樣做能讓開發更簡單,不用特地去配置或者在每個方法上加標記來告訴系統要返回 JSON 資料 ---GPT3.5

springboot會自動將Java物件進行JSON序列化,變成字串然後返回

也就是說,如果是自己實現的物件的話,要記得同時實現SET/GET方法以確保資料能夠正常返回

以上就是如何編寫一個簡單的API

RESTful API

一般來說,在編寫介面API時,需要滿足RESTful規範

RESTful API 是一種設計風格或架構模式,用於構建分散式系統中的網路服務。

在其核心原則中有以下兩點是現在需要注意的

路徑(基於資源)

“路徑”表示API的具體網址,又被稱為“終點"(endpoint)。即RESTful API中的基於資源(Resource-Based)原則

每個網址代表一種資源(resource),每個資源由唯一的識別符號(URL)表示,客戶端透過HTTP動詞對資源進行操作。

所以網址中不能有動詞,只能有名詞,而且所用的名詞往往需要與資料庫的表格名對應。

使用HTTP動詞

HTTP動詞(GET、POST、PUT、PATCH、DELETE)用於定義操作型別,與資源的狀態轉換相關聯。每個動詞有著特定的語義:

  • GET(SELECT):從伺服器獲取資源的當前狀態或一組資源。
  • POST(CREATE):在伺服器上建立新資源。
  • PUT(UPDATE):在伺服器上更新資源的全部內容。
  • PATCH(UPDATE):在伺服器上更新資源的部分內容。
  • DELETE(DELETE):從伺服器上刪除資源。

業務程式碼編寫

在需求拆解中,資料庫是最底部的層,我們從下往上寫

基本框架

建立資料庫

首先建立一個用於存放玩家資料的資料庫game詳見

然後在game庫中建立player表,如圖所示:

DataAccess層

開始編寫DataAccess層部分的程式碼

首先將pom.xml中對於jpa的註釋解除(記得reload)

然後在src/main/java/com/tutorial/boot_demo下建立對應的package

具體是:src/main/java/com/tutorial/boot_demo/dao

dao下建立一個新的class:PlayerRepository,型別為interface

與之前寫測試程式碼類似,首先要給PlayerRepository新增@Repository註解,表明其為為Repository層程式碼

@Repository 用於標記資料訪問層(DAO)的類,即直接與資料庫進行互動的類,執行 CRUD 操作。

這樣springboot就會將該類視為一個springbean,放在容器中去管理依賴關係

方便實現IOC依賴注入的特性

package com.tutorial.boot_demo.dao;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository //表明下述程式碼為Repository層程式碼
public interface PlayerRepository extends JpaRepository {

}

此外還要繼承一下JpaRepository(是一個泛型介面),裡面的一些方法很實用

我們需要將資料庫中的資料查詢出來然後對映到Java的具體物件當中

因此需要建立一個Player類(src/main/java/com/tutorial/boot_demo/dao/Player.java)負責對映資料庫的表(資料庫也有對應的Player表,首字母沒有大寫)

package com.tutorial.boot_demo.dao;
import jakarta.persistence.*;

@Entity //表明其為一個對映到資料庫的物件
@Table(name="player")
public class Player {
}

首先對class Player進行修飾,@Entity 註解標識了 Player 類是一個JPA實體,表示它會對映到資料庫中的表。

@Table(name="player") 註解用於指定該實體類對映到資料庫中的哪張表。在這裡,name="player" 意味著將 Player 實體對映到資料庫中名為 player 的表格。

當使用Java Persistence API (JPA) 運算元據庫時,JPA會將 Player 類的物件持久化到名為 player 的資料庫表中,表結構和 Player 類的屬性欄位之間會有相應的對映關係。

然後開始寫Player中應該有的欄位,即對應資料庫中有的欄位

package com.tutorial.boot_demo.dao;

import jakarta.persistence.*;

import static jakarta.persistence.GenerationType.IDENTITY;

@Entity //表明其為一個對映到資料庫的物件
@Table(name="player")
public class Player {
    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = IDENTITY) //表示id是一個自增元件,由資料庫生成
    private long id;

    @Column(name = "name") //指定要對映到的資料庫表中的具體Column
    private String name;

    @Column(name = "email") //如果物件名與對映的表名相同可以不用寫,但是大部分情況不同
    private String email;

    @Column(name = "level")
    private int level;

    @Column(name = "exp")
    private int exp;

    @Column(name = "gold")
    private int gold;
}

完成對映類Player的編寫後回來繼續寫PlayerRepository

package com.tutorial.boot_demo.dao;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository //表明下述程式碼為Repository層程式碼
public interface PlayerRepository extends JpaRepository<Player, Long> {

}

PlayerRepository 介面繼承自 JpaRepository<Player, Long>

  • Player: 這是實體類的型別,也就是該 Repository 操作的實體類型別。在這個例子中,PlayerRepository 負責操作名為 Player 的實體類。
  • Long: 這是實體類的主鍵型別,通常是實體類的主鍵屬性的型別。在 Java 中,主鍵屬性通常是一個唯一識別符號,型別可以是 LongIntegerString 等。這裡指定了 Player 實體類的主鍵型別為 Long

JpaRepository<Player, Long>表示PlayerRepository是一個用於操作Player實體類的倉庫介面。它繼承自 Spring Data JPA 提供的JpaRepository介面,這個介面提供了一組用於對實體進行持久化操作的方法,例如儲存、更新、刪除、查詢等。透過指定PlayerLong,我們告訴 Spring Data JPA,PlayerRepository將管理Player實體,其主鍵型別為Long

至此,DataAccess層編寫完成

Service層

接下來到Service層,依然是在src/main/java/com/tutorial/boot_demo下建立對應的package

具體為:src/main/java/com/tutorial/boot_demo/service

因為我們是面向介面程式設計,所以仍然要建立一個介面PlayerService.java,以及在同一目錄下的介面實現類PlayerServiceImpl

還是和之前一樣,需要透過註解將其納入springbean的容器管理中

package com.tutorial.boot_demo.service;
import org.springframework.stereotype.Service;

@Service
public class PlayerServiceImpl implements PlayerService{
}

PlayerServiceImpl 是服務層的實現類,負責實現業務邏輯,而不是直接與資料庫互動。

因此,它被標記為 @Service,表示它是一個服務層的元件。

接下來需要在PlayerService.java介面中給出透過id查詢玩家的一個方法

package com.tutorial.boot_demo.service;

import com.tutorial.boot_demo.dao.Player;

public interface PlayerService {
   Player getPlayerById(long id);
}

再到實現類PlayerServiceImpl 裡面實現該方法

package com.tutorial.boot_demo.service;

import com.tutorial.boot_demo.dao.Player;
import org.springframework.stereotype.Service;

@Service
public class PlayerServiceImpl implements PlayerService{

    @Override
    public Player getPlayerById(long id) {
        return null;
    }
}

注意,這裡比之前多了一些程式碼,為IDEA自動補全

在這裡要注入我們定義的PlayerRepository

package com.tutorial.boot_demo.service;

import com.tutorial.boot_demo.dao.Player;
import com.tutorial.boot_demo.dao.PlayerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PlayerServiceImpl implements PlayerService{

    @Autowired
    private PlayerRepository playerRepository;

    @Override
    public Player getPlayerById(long id) {
        return null;
    }
}

在使用Spring Framework開發應用程式時,經常會遇到需要依賴注入的情況。

依賴注入是指在一個物件中,透過容器(如Spring容器)自動將依賴的物件注入到需要使用它的地方。

在Spring中,透過@Autowired註解來實現這一自動裝配的功能。

PlayerServiceImpl是一個服務實現類,在這個類中,我們宣告瞭一個私有欄位playerRepository,它的型別是PlayerRepository。這是我們用來訪問資料庫或持久層的介面。

透過在playerRepository欄位上新增@Autowired註解,我們告訴Spring框架:“請幫我把一個符合型別的PlayerRepository例項注入到這個欄位中”。Spring在啟動時會掃描並識別PlayerRepository介面的具體實現,並建立該實現的例項。然後,它將這個例項自動注入到playerRepository欄位中,使得我們可以在PlayerServiceImpl類中方便地使用playerRepository來執行與資料庫相關的操作,如查詢、儲存、更新和刪除Player實體。

此時可以去呼叫父介面提供的方法進行查詢

...
    public Player getPlayerById(long id) { //此時可以直接呼叫父介面提供的方法進行查詢
        return playerRepository.findById(id).orElseThrow(RuntimeException::new);
    }

playerRepository.findById(id) 是 Spring Data JPA 提供的方法,用於根據主鍵(id)從資料庫中查詢實體物件

.orElseThrow(RuntimeException::new) 的作用是在查詢資料庫後,如果未找到對應實體,則丟擲執行時異常,以確保方法呼叫者能夠適當地處理空結果的情況,或者在必要時進行異常處理。【不加就報錯】

Service層編寫完成

API層(Controller層)

開始寫Controller層的程式碼

起手步驟還是跟之前兩個層一樣,建package,建類

package com.tutorial.boot_demo.controller;

import com.tutorial.boot_demo.dao.Player;
import com.tutorial.boot_demo.service.PlayerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PlayerController {
    @Autowired
    private PlayerService playerService;

    @GetMapping("/player/{id}")
    public Player getPlayerById(@PathVariable long id){ //@PathVariable表示{id}
        return playerService.getPlayerById(id); //呼叫service層的方法查詢id
    }
}

{id} 是一個路徑變數(Path Variable)的佔位符,用於指示這個 GetMapping 註解處理的請求路徑中會有一個名為 id 的變數。具體來說,/player/{id} 表示這個介面可以接收一個形如 /player/123 的請求,其中 123 是具體的 id 值。

疑問:使用@Autowired注入playerService可不可以類比理解為“匯入一個方法”?

@Autowired 註解用於告訴 Spring 容器,需要將一個符合型別的 Bean 注入到 playerService 這個欄位中。在你的程式碼中,playerService 是一個 PlayerService 介面型別的欄位,透過 @Autowired 註解,Spring 會自動尋找並注入一個符合 PlayerService 介面的實現類的物件。

類比理解為“匯入一個方法”可能不太準確。實際上,@Autowired 更像是告訴 Spring:“我需要一個 PlayerService 的例項,請幫我找一個並且注入到這個欄位中”。Spring 會根據配置和約定(如實現類或者其他配置方式)來例項化並注入相應的物件。

---GPT3.5

到這裡就寫完了一個基本框架

配置資料庫路徑

在啟動專案之前需要到src/main/resources/application.properties中新增連線MySQL資料庫的url

spring.application.name=boot-demo

spring.datasource.username=root
spring.datasource.url=jdbc:mysql://192.168.xx.xxx:3306/game?characterEncoding=utf-8
spring.datasource.password=1***30

然後我們就可以透過路由器對資料庫進行查詢了

發現查詢結果為空,但實際上表中是有對應資料的

懷疑是沒有為Player對映類建立對應的SET/GET方法導致的

透過IDEA右鍵的generate補上Setter/Getter之後,可以正常查詢

查詢介面存在的問題與改進

透過上面的程式碼,我們完成了最基本的一個查詢API的開發

但是實現上還有不合理之處,與實際開發有出入

存在的問題

1、Controller層直接返回資料庫的物件

src/main/java/com/tutorial/boot_demo/controller/PlayerController.java直接返回了資料庫查詢物件

這會將所有資料資訊都暴露給前端(包括可能存在的加密欄位等),這樣處理是不合理的

2、沒有對後端狀態資訊進行返回

在Controller層中,應該要新增對於後端報錯資訊以及執行狀態資訊的一個返回方法

將後端返回結果進行統一封裝,使得前端可以判斷後端一些介面請求是否正常

改進:新增DTO層

定義DTO層

"DTO層"通常指的是資料傳輸物件(Data Transfer Object)

DTO是一種設計模式,用於在不同層之間傳輸資料,通常用於解耦和傳遞資料,以及在不同層(如控制器層、服務層、持久層)之間傳遞資料。

這裡增加DTO層的主要目的是指定需要傳輸的資料,避免過多或不必要的資料傳輸

與之前的層的編寫方式類似,也是要在src/main/java/com/tutorial/boot_demo下新建一個package

具體是:src/main/java/com/tutorial/boot_demo/dto/PlayerDTO.java

package com.tutorial.boot_demo.dto;

public class PlayerDTO {
    private long id;

    private String name;

    private String email;

    public long getId() {
        return id;
    }
	//記得生成對應Setter/Getter方法
}
使用DTO層

那麼之前相關層中的對應呼叫也要修改

src/main/java/com/tutorial/boot_demo/service/PlayerService.javaService層的介面類中的介面方法要換用PlayerDTO

...
public interface PlayerService {
   PlayerDTO getPlayerById(long id);
}

並且其介面實現src/main/java/com/tutorial/boot_demo/service/PlayerServiceImpl.java中也要做對應修改

package com.tutorial.boot_demo.service;

import com.tutorial.boot_demo.dao.Player;
import com.tutorial.boot_demo.dao.PlayerRepository;
import com.tutorial.boot_demo.dto.PlayerDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PlayerServiceImpl implements PlayerService{

    @Autowired
    private PlayerRepository playerRepository;

    @Override
    public PlayerDTO getPlayerById(long id) { //此時可以直接呼叫父介面提供的方法進行查詢
        //要把Player物件轉換為一個PlayerDTO物件
        Player player = playerRepository.findById(id).orElseThrow(RuntimeException::new);
        //此處未寫完
    }
}

因為首先查出來的肯定是一個Player物件,需要做處理將其轉換為PlayerDTO物件

轉換Player物件

這個工作由一個額外的converter類實現,位於src/main/java/com/tutorial/boot_demo/converter/PlayerConverter.java

package com.tutorial.boot_demo.converter;

import com.tutorial.boot_demo.dao.Player;
import com.tutorial.boot_demo.dto.PlayerDTO;

public class PlayerConverter {
    public static PlayerDTO convertPlayer(Player player){ //將Player轉換為PlayerDTO
        PlayerDTO playerDTO = new PlayerDTO();
        playerDTO.setId(player.getId());//獲取player物件中,需要給到前端的資料,放入playerDTO中
        playerDTO.setName(player.getName());
        playerDTO.setEmail(player.getEmail());
        return playerDTO;
    }
}

這裡也可以寫成非靜態方法

靜態方法和非靜態方法在Spring Boot中的使用方式主要受到依賴注入機制的影響。靜態方法一般用於工具類或者無需依賴物件狀態的場景,而非靜態方法則更適合用來定義和管理Spring Bean,並且能夠利用依賴注入實現物件之間的解耦和協作。

現在可以繼續完成PlayerServiceImpl.java中的介面實現

...
@Service
public class PlayerServiceImpl implements PlayerService{

    @Autowired
    private PlayerRepository playerRepository;

    @Override
    public PlayerDTO getPlayerById(long id) { //此時可以直接呼叫父介面提供的方法進行查詢
        //要把Player物件轉換為一個PlayerDTO物件
        Player player = playerRepository.findById(id).orElseThrow(RuntimeException::new);
        return PlayerConverter.convertPlayer(player);//返回一個PlayerDTO物件
    }
}

改進:新增response類

src/main/java/com/tutorial/boot_demo下(不用單獨建package)新建一個response

用於統一後端介面的格式,新增例如錯誤資訊、狀態碼等欄位

src/main/java/com/tutorial/boot_demo/Response.java,需要將其定義為一個泛型類

package com.tutorial.boot_demo;

public class Response <T>{
    //統一後端格式,所有介面返回結果時都有success和errorMsg,從而方便判斷傑闊呼叫情況
    private T data;
    private boolean success;
    private String errorMsg;

    //封裝一些靜態方法來使用這些泛型資料
    public static <K> Response<K> newSuccess(K data){
        Response<K> response = new Response<>();
        response.setData(data);
        response.setSuccess(true);
        return response;
    }

    public static Response<Void> newFail(String errorMsg){
        Response<Void> response = new Response<>();
        response.setErrorMsg(errorMsg);
        response.setSuccess(false);
        return response;
    }
    ...
	//這裡省略了data、success和errorMsg的Setter/Getter方法,記得用IDEA補全
}

為什麼要用泛型類?---GPT3.5

主要目的是為了實現通用的響應資料結構,使其能夠適應不同型別的資料作為返回結果。

具體來說,泛型的作用包括以下幾點:

  • 通用性和複用性: 使用泛型使得 Response 類可以處理任意型別的資料作為返回結果。例如,可以返回一個包含具體資料的成功響應 (Response<T>) 或者一個不包含具體資料但包含錯誤資訊的失敗響應 (Response<Void>)。

  • 型別安全: 泛型能夠在編譯期間提供型別檢查,避免了在執行時因型別不匹配而導致的錯誤。這對於大型專案和團隊協作非常重要,能夠減少因型別錯誤引發的bug。

  • 框架整合: 在 Spring Boot 中,泛型可以幫助實現更加靈活和型別安全的資料互動。例如,在控制器中返回 Response<User> 可以明確告訴框架響應的資料型別是 User 型別,這樣可以更好地進行資料繫結和轉換,同時也能方便前後端資料互動。

好了現在需要將Controller層(src/main/java/com/tutorial/boot_demo/controller/PlayerController.java)中使用的PlayerDTO物件傳給 Response

...
@RestController
public class PlayerController {
    @Autowired
    private PlayerService playerService;

    @GetMapping("/player/{id}")
    public Response<PlayerDTO> getPlayerById(@PathVariable int id){ //@PathVariable表示{id}
        return Response.newSuccess(playerService.getPlayerById(id)); //呼叫service層的方法查詢id
    }
}

至此,一個具備關鍵資訊隱藏且後端資料結構化的查詢介面就基本完成了

測試一下

編寫新增介面(POST)

程式碼編寫

在需求拆解部分有提到,API層負責處理GET\POST\PUT\DELETE請求

查詢介面處理的是GET,那不難推知新增資料介面應該是處理POST請求

可以在Controller層中新增一個addNewPlayer的介面用於處理由POST帶來的新增玩家資料的操作請求

com/tutorial/boot_demo/controller/PlayerController.java

...
    @RestController
    public class PlayerController {
        @Autowired
        private PlayerService playerService;

        @GetMapping("/player/{id}")
        public Response<PlayerDTO> getPlayerById(@PathVariable int id){ //@PathVariable表示{id}
            return Response.newSuccess(playerService.getPlayerById(id)); //呼叫service層的方法查詢id
        }

        @PostMapping("/player")
        public long addNewPlayer(@RequestBody PlayerDTO){
            //理論上這裡還需要做一些校驗,這裡先省略了
        }
    }

注意,新增資料時,前端一般是採用JSON格式將資料傳到後端

我們是透過DTO物件與前端互動,前端傳送JSON到後端,springboot會對資料進行反序列化然後放到對應的Java物件中以供使用

透過Java物件處理前端傳回的資料即可

...
    @PostMapping("/player")
    public Response<Long> addNewPlayer(@RequestBody PlayerDTO playerDTO){
        return Response.newSuccess(playerService.addNewPlayer(playerDTO));
    }

統一用Response返回後端介面的結果,下面開始實現對應的Service層程式碼

src/main/java/com/tutorial/boot_demo/service/PlayerService.java建立addNewPlayer方法(可以直接用IDE快速建立)

...
    public interface PlayerService {
    	PlayerDTO getPlayerById(long id);
    	Long addNewPlayer(PlayerDTO playerDTO);
}

然後去寫對應的Service層實現(即src/main/java/com/tutorial/boot_demo/service/PlayerServiceImpl.java

...
    @Service
    public class PlayerServiceImpl implements PlayerService{

        @Autowired
        private PlayerRepository playerRepository;

        @Override
        public PlayerDTO getPlayerById(long id) { //此時可以直接呼叫父介面提供的方法進行查詢
            //要把Player物件轉換為一個PlayerDTO物件
            Player player = playerRepository.findById(id).orElseThrow(RuntimeException::new);
            //此處未寫完
            return PlayerConverter.convertPlayer(player);
        }

        @Override//可以透過IDE自動生成
        public Long addNewPlayer(PlayerDTO playerDTO) {
            return 0;
        }
    }

一般來說,玩家的郵箱地址(email欄位)是唯一的,所以在新增玩家資料時需要檢查郵箱的唯一性

...
    @Service
    public class PlayerServiceImpl implements PlayerService{
...
        @Override//可以透過IDE自動生成
        public Long addNewPlayer(PlayerDTO playerDTO) {
    		playerRepository.findByEmail(playerDTO.getEmail());//檢查郵箱唯一性
             return 0;
        }
    }

很自然的會想用findByEmail()去校驗郵箱,不好意思,PlayerRepository繼承的JpaRepository裡面沒有這個方法

得自己去寫一個

跳轉到src/main/java/com/tutorial/boot_demo/dao/PlayerRepository.java

package com.tutorial.boot_demo.dao;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository //表明下述程式碼為Repository層程式碼
public interface PlayerRepository extends JpaRepository<Player, Long> {

    List<Player> findByEmail(String email);
}

注意:

1、寫findByEmail的時候可以按照JpaRepository的規範來,也就是先在PlayerServiceImpl.java裡面寫好:playerRepository.findByEmail(playerDTO.getEmail());然後用IDE右鍵跳轉到PlayerRepository.java自動生成findByEmail

2、findByEmail的寫法是參照JpaRepository中已有的方法(例如findById)來寫的,那麼JpaRepository會自動按照你的命名去查詢資料庫中對應的欄位。(例如findByEmail,Jpa就知道要去查email欄位)

然後去實現這個介面

src/main/java/com/tutorial/boot_demo/service/PlayerServiceImpl.java

...
@Service
public class PlayerServiceImpl implements PlayerService{

    @Autowired
    private PlayerRepository playerRepository;

    @Override
    public PlayerDTO getPlayerById(long id) { //此時可以直接呼叫父介面提供的方法進行查詢
        //要把Player物件轉換為一個PlayerDTO物件
        Player player = playerRepository.findById(id).orElseThrow(RuntimeException::new);
        //此處未寫完
        return PlayerConverter.convertPlayer(player);
    }

    @Override
    public Long addNewPlayer(PlayerDTO playerDTO) {
        List<Player> playerList = playerRepository.findByEmail(playerDTO.getEmail());//檢查郵箱唯一性
        //判斷
        if(!CollectionUtils.isEmpty(playerList)){//郵箱重複,丟擲異常
            throw new IllegalStateException("email:" + playerDTO.getEmail() + " has been used");
        }
        //這裡返回值也需要轉換
        return 0;
    }
}

這裡需要引入一個概念:領域物件(domain object)

  • 領域物件是指在領域模型中具體描述業務領域中的實體和規則的物件。它們通常直接對映到資料庫中的表結構
  • 領域物件包含業務邏輯、資料持久化以及業務規則等資訊,是業務邏輯的核心物件。

在實際的 Spring Boot 應用中,通常會涉及到從資料庫中讀取領域物件(entity),然後將其轉換為適合前端展示或傳輸的 DTO。

同樣地,當接收到前端傳來的 DTO 資料時,需要將其轉換為領域物件以便於進行業務邏輯處理和持久化操作

因此,上述程式碼中,為了將新增的玩家資料持久化,需要將前端傳來的DTO資料轉換為domain object進而持久化到資料庫中

src/main/java/com/tutorial/boot_demo/converter/PlayerConverter.java

package com.tutorial.boot_demo.converter;

import com.tutorial.boot_demo.dao.Player;
import com.tutorial.boot_demo.dto.PlayerDTO;

public class PlayerConverter {
    public static PlayerDTO convertPlayer(Player player){ //將Player轉換為PlayerDTO
        PlayerDTO playerDTO = new PlayerDTO();
        playerDTO.setId(player.getId());//獲取player物件中,需要給到前端的資料,放入playerDTO中
        playerDTO.setName(player.getName());
        playerDTO.setEmail(player.getEmail());
        return playerDTO;
    }

    public static Player convertPlayer(PlayerDTO playerDTO){ //將PlayerDTO轉換為Player
        Player player = new Player();
        player.setName(player.getName());
        player.setEmail(player.getEmail());
        return player;
    }
}

回到PlayerServiceImpl.java

	@Override
    public Long addNewPlayer(PlayerDTO playerDTO) {
        List<Player> playerList = playerRepository.findByEmail(playerDTO.getEmail());//檢查郵箱唯一性
        //判斷
        if(!CollectionUtils.isEmpty(playerList)){//郵箱重複,丟擲異常
            throw new IllegalStateException("email:" + playerDTO.getEmail() + " has been used");
        }
        //這裡返回值也需要轉換
        Player player = playerRepository.save(PlayerConverter.convertPlayer(playerDTO));
        
        return player.getId();
    }

至此,新增介面寫完了

有問題啊,使用postman測試返回500錯誤

錯誤日誌如下:

org.hibernate.HibernateException: The database returned no natively generated identity value : com.tutorial.boot_demo.dao.Player
	at org.hibernate.id.IdentifierGeneratorHelper.getGeneratedIdentity(IdentifierGeneratorHelper.java:86) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.id.insert.GetGeneratedKeysDelegate.performInsert(GetGeneratedKeysDelegate.java:112) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.engine.jdbc.mutation.internal.MutationExecutorPostInsertSingleTable.execute(MutationExecutorPostInsertSingleTable.java:100) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.persister.entity.mutation.InsertCoordinator.doStaticInserts(InsertCoordinator.java:175) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.persister.entity.mutation.InsertCoordinator.coordinateInsert(InsertCoordinator.java:113) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2858) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.action.internal.EntityIdentityInsertAction.execute(EntityIdentityInsertAction.java:81) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.engine.spi.ActionQueue.execute(ActionQueue.java:670) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.engine.spi.ActionQueue.addResolvedEntityInsertAction(ActionQueue.java:291) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.engine.spi.ActionQueue.addInsertAction(ActionQueue.java:272) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.engine.spi.ActionQueue.addAction(ActionQueue.java:322) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.event.internal.AbstractSaveEventListener.addInsertAction(AbstractSaveEventListener.java:388) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:302) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:221) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:135) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:175) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.event.internal.DefaultPersistEventListener.persist(DefaultPersistEventListener.java:93) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:77) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:54) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:758) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:742) ~[hibernate-core-6.4.9.Final.jar:6.4.9.Final]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
	at org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.java:364) ~[spring-orm-6.1.10.jar:6.1.10]
	at jdk.proxy2/jdk.proxy2.$Proxy104.persist(Unknown Source) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
	at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:319) ~[spring-orm-6.1.10.jar:6.1.10]
	at jdk.proxy2/jdk.proxy2.$Proxy104.persist(Unknown Source) ~[na:na]
	at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:619) ~[spring-data-jpa-3.2.7.jar:3.2.7]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:354) ~[spring-aop-6.1.10.jar:6.1.10]
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:277) ~[spring-data-commons-3.2.7.jar:3.2.7]
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170) ~[spring-data-commons-3.2.7.jar:3.2.7]
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158) ~[spring-data-commons-3.2.7.jar:3.2.7]
	at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:516) ~[spring-data-commons-3.2.7.jar:3.2.7]
	at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285) ~[spring-data-commons-3.2.7.jar:3.2.7]
	at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:628) ~[spring-data-commons-3.2.7.jar:3.2.7]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.10.jar:6.1.10]
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:168) ~[spring-data-commons-3.2.7.jar:3.2.7]
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:143) ~[spring-data-commons-3.2.7.jar:3.2.7]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.10.jar:6.1.10]
	at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:70) ~[spring-data-commons-3.2.7.jar:3.2.7]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.10.jar:6.1.10]
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-6.1.10.jar:6.1.10]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:392) ~[spring-tx-6.1.10.jar:6.1.10]
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-6.1.10.jar:6.1.10]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.10.jar:6.1.10]
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:138) ~[spring-tx-6.1.10.jar:6.1.10]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.10.jar:6.1.10]
	at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:164) ~[spring-data-jpa-3.2.7.jar:3.2.7]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.10.jar:6.1.10]
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) ~[spring-aop-6.1.10.jar:6.1.10]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.10.jar:6.1.10]
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223) ~[spring-aop-6.1.10.jar:6.1.10]
	at jdk.proxy2/jdk.proxy2.$Proxy109.save(Unknown Source) ~[na:na]
	at com.tutorial.boot_demo.service.PlayerServiceImpl.addNewPlayer(PlayerServiceImpl.java:36) ~[classes/:na]
	at com.tutorial.boot_demo.controller.PlayerController.addNewPlayer(PlayerController.java:23) ~[classes/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255) ~[spring-web-6.1.10.jar:6.1.10]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188) ~[spring-web-6.1.10.jar:6.1.10]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.1.10.jar:6.1.10]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:926) ~[spring-webmvc-6.1.10.jar:6.1.10]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:831) ~[spring-webmvc-6.1.10.jar:6.1.10]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.1.10.jar:6.1.10]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.10.jar:6.1.10]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.10.jar:6.1.10]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.10.jar:6.1.10]
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914) ~[spring-webmvc-6.1.10.jar:6.1.10]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) ~[tomcat-embed-core-10.1.25.jar:6.0]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.1.10.jar:6.1.10]
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.25.jar:6.0]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.25.jar:10.1.25]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.1.10.jar:6.1.10]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.10.jar:6.1.10]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.1.10.jar:6.1.10]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.10.jar:6.1.10]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.1.10.jar:6.1.10]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.10.jar:6.1.10]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:389) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:904) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.25.jar:10.1.25]
	at java.base/java.lang.Thread.run(Thread.java:842) ~[na:na]

參考解決方案【未解決】:

https://stackoverflow.com/questions/7172657/org-hibernate-hibernateexception-the-database-returned-no-natively-generated-id

問題推測

【解決之後會更新後續】

1、嘗試用更簡單的資料,在資料庫中重新建一個表test用於介面測試

2、懷疑是資料庫中的資料設定問題,可能不能進行id自增?

臨時處理

重新建表

由於時間問題以及當前我對於Jpa的熟悉程度,我覺得“繞過”上述bug

解決方案是重新建立一個資料庫以及新的表,並對原來的程式碼做相關調整

以下是資料庫表的建立程式碼:

# 建庫
CREATE DATABASE test4springbootdemo
	CHARACTER SET utf8mb4 # 資料庫的字符集為 utf8mb4
	COLLATE utf8mb4_general_ci; # 不區分大小寫的一般性校對規則

建立一個名為 test4springbootdemo 的資料庫,並確保它能夠支援儲存和處理包含各種語言和特殊字元(包括 emoji)的資料

CREATE TABLE player(
	id INT AUTO_INCREMENT PRIMARY KEY, # 之前用的現成的資料庫可能沒有設定這個“自增元件”
    name VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL,
    player_level INT  
);

建立一個名為 player 的表,用於儲存玩家的資訊,包括每位玩家的唯一標識 id、名字 name、郵箱 email 和玩家級別 player_level

這次的資料庫相對於之前的,欄位有所減少,然後新增了id欄位的自增(之前的忘了有沒有設定)

插入一條資料

INSERT INTO player (name, email, player_level) VALUES ('xixi', 'xixi@163.com', 15);

結果:

mysql> SELECT * FROM player;
+----+------+--------------+--------------+
| id | name | email        | player_level |
+----+------+--------------+--------------+
|  1 | xixi | xixi@163.com |           15 |
+----+------+--------------+--------------+
1 row in set (0.00 sec)

修改相關程式碼

修改配置檔案

首先需要在application.properties修改連線的資料庫

spring.application.name=boot-demo

spring.datasource.username=root
spring.datasource.url=jdbc:mysql://192.168.91.128:3306/test4springbootdemo?characterEncoding=utf-8
spring.datasource.password=102030
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
修改Player對映類

就把精簡掉的欄位給去掉就好了,其他部分也不需要修改

src/main/java/com/tutorial/boot_demo/dao/Player.java

package com.tutorial.boot_demo.dao;

import jakarta.persistence.*;

import static jakarta.persistence.GenerationType.IDENTITY;

@Entity //表明其為一個對映到資料庫的物件
@Table(name="player")
public class Player {
    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = IDENTITY) //表示id是一個自增元件,由資料庫生成
    private long id;

    @Column(name = "name") //指定要對映到的資料庫表中的具體Column
    private String name;

    @Column(name = "email") //如果物件名與對映的表名相同可以不用寫,但是大部分情況不同
    private String email;

    @Column(name = "player_level")
    private int player_level;

//    @Column(name = "exp")
//    private int exp;
//
//    @Column(name = "gold")
//    private int gold;
    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public int getPlayer_level() {
        return player_level;
    }
    public void setPlayer_level(int player_level) {
        this.player_level = player_level;
    }
}
測試

訪問http://localhost:8080/player/1以及http://localhost:8080/hello都是正常返回的

測試POST

還是向http://localhost:8080/player POST如下資料

{
    "name": "riffdk",
    "email": "didi@cctv.com",
    "player_level": 3
}

得到的返回結果如下:

{
    "timestamp": "2024-07-04T02:54:19.325+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/player"
}

報錯是:java.sql.SQLIntegrityConstraintViolationException: Column 'email' cannot be null

不對啊,這麼還有錯呢?

不過這次id到時沒問題了,根據報錯提示,那把emai改成“可以為null”不就好了?試試

SHOW COLUMNS FROM player;檢視錶 player 的結構

+--------------+--------------+------+-----+---------+----------------+
| Field        | Type         | Null | Key | Default | Extra          |
+--------------+--------------+------+-----+---------+----------------+
| id           | int          | NO   | PRI | NULL    | auto_increment |
| name         | varchar(50)  | NO   |     | NULL    |                |
| email        | varchar(100) | NO   |     | NULL    |                |
| player_level | int          | YES  |     | NULL    |                |
+--------------+--------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

ALTER TABLE player MODIFY COLUMN email VARCHAR(100);更改email欄位屬性

mysql> ALTER TABLE player MODIFY COLUMN email VARCHAR(100);
Query OK, 0 rows affected (0.02 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> DESCRIBE player;
+--------------+--------------+------+-----+---------+----------------+
| Field        | Type         | Null | Key | Default | Extra          |
+--------------+--------------+------+-----+---------+----------------+
| id           | int          | NO   | PRI | NULL    | auto_increment |
| name         | varchar(50)  | NO   |     | NULL    |                |
| email        | varchar(100) | YES  |     | NULL    |                |
| player_level | int          | YES  |     | NULL    |                |
+--------------+--------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

然後再次嘗試POST,這次報錯java.sql.SQLIntegrityConstraintViolationException: Column 'name' cannot be null

行行,那再把name欄位屬性也改了唄

這次終於POST成功了,返回資訊如下:

{
    "data": 2,
    "success": true,
    "errorMsg": null
}

data對應的是自增的id

查詢一下現在player表中的資料

mysql> SELECT * FROM player;
+----+------+--------------+--------------+
| id | name | email        | player_level |
+----+------+--------------+--------------+
|  1 | xixi | xixi@163.com |           15 |
|  2 | NULL | NULL         |            0 |
+----+------+--------------+--------------+
2 rows in set (0.00 sec)

事情還沒完。。。

後面透過排查程式碼發現,在src/main/java/com/tutorial/boot_demo/converter/PlayerConverter.java中,DTO資料轉player物件的程式碼寫得有問題

public static Player convertPlayer(PlayerDTO playerDTO){ //將Player轉換為PlayerDTO
        Player player = new Player();
        player.setName(playerDTO.getName()); //原先報錯的時候寫的是player...
        player.setEmail(playerDTO.getEmail());
        return player;
    }

改完後正常

修改資料庫【解決無法插入資料的問題】

切換回最開始使用的資料庫,報錯依舊,但是這次可以定位問題到id欄位了,列印game資料庫中的player表結構

mysql> DESCRIBE player;
+-------+---------------+------+-----+---------+-------+
| Field | Type          | Null | Key | Default | Extra |
+-------+---------------+------+-----+---------+-------+
| id    | int           | YES  |     | NULL    |       |
| name  | varchar(100)  | YES  |     | NULL    |       |
| sex   | varchar(10)   | YES  |     | NULL    |       |
| email | varchar(100)  | YES  |     | NULL    |       |
| level | int           | YES  |     | 1       |       |
| exp   | int           | YES  |     | NULL    |       |
| gold  | decimal(10,2) | YES  |     | NULL    |       |
+-------+---------------+------+-----+---------+-------+
7 rows in set (0.00 sec)

對比我們新建的表很明顯了,首先這裡的所有欄位都是允許NULL值的

其次,並沒有將id欄位的Key設為PRI

改了試試,將表結構按以下指令修改

ALTER TABLE player
    MODIFY COLUMN id INT AUTO_INCREMENT PRIMARY KEY;

這條命令將 id 欄位的型別修改為 INT,並設定為 AUTO_INCREMENT,同時將其設為主鍵 (PRIMARY KEY)。這樣就確保了每次插入新資料時,id 欄位會自動遞增,並且保證唯一性。

結果如下:

mysql> DESCRIBE player;
+-------+---------------+------+-----+---------+----------------+
| Field | Type          | Null | Key | Default | Extra          |
+-------+---------------+------+-----+---------+----------------+
| id    | int           | NO   | PRI | NULL    | auto_increment |
| name  | varchar(100)  | YES  |     | NULL    |                |
| sex   | varchar(10)   | YES  |     | NULL    |                |
| email | varchar(100)  | YES  |     | NULL    |                |
| level | int           | YES  |     | 1       |                |
| exp   | int           | YES  |     | NULL    |                |
| gold  | decimal(10,2) | YES  |     | NULL    |                |
+-------+---------------+------+-----+---------+----------------+

對味兒了這次

再去測一下POST

測試透過,可以正常插入資料

其實這裡還有一個小問題,就是由於之前增加DTO層的時候沒有處理expgold這幾個欄位,所以透過POST加進去的時候會變成0,有空再可以改一下

小結

總結一下,導致開頭無法插入資料這種情況的原因是兩個:

1、convertPlayer方法寫錯

這導致就算插入資料,也無法正常的將其轉換為所需資料(即全為NULL)

2、資料庫建庫問題【主要問題】

在game資料庫建立之初,沒有將id欄位設定為自增主鍵,從而導致介面提供的資料無法正常插入

編寫刪除介面(DELETE)

實現刪除介面

首先在Controller層(src/main/java/com/tutorial/boot_demo/controller/PlayerController.java)定義介面

	...	
	@DeleteMapping("/player/{id}")
    public void deletePlayerById(@PathVariable long id){ //沒有返回值所以用void
        playerService.deletePlayerById(id);
    }
	...

然後去實現該介面(src/main/java/com/tutorial/boot_demo/service/PlayerServiceImpl.java

	...
	@Override
    public void deletePlayerById(long id) {
        //根據id找資料,找不到的話就報錯
        playerRepository.findById(id).orElseThrow(()-> new IllegalArgumentException("id:" + id + "dosen't exist!"));
        playerRepository.deleteById(id); // 找到就給丫刪了
        
    }
	...	

測試能夠正常刪除

編寫更新介面(UPDATE)

與上面的過程類似,還是先到Controller層(src/main/java/com/tutorial/boot_demo/controller/PlayerController.java)定義介面

...
    @PutMapping("/player/{id}")
    public Response<PlayerDTO> updatePlayerById(@PathVariable long id, @RequestParam(required = false) String name,
                                                @RequestParam(required = false) String email){
        return Response.newSuccess(playerService.updatePlayerById(id, name, email));
    }
...

@RequestParam 是 Spring Framework 提供的註解,用於從 HTTP 請求中提取特定的引數值,支援從 URL 查詢引數、表單資料或請求體中獲取,並可設定引數是否必需

然後還是去實現該介面(src/main/java/com/tutorial/boot_demo/service/PlayerServiceImpl.java

...
    @Override
    @Transactional //操作失敗就回滾對應資料
    public PlayerDTO updatePlayerById(long id, String name, String email) {
        //同樣要檢查一下id,以及email
        Player playerIntoDB = playerRepository.findById(id).orElseThrow(()-> new IllegalArgumentException("id:" + id + "dosen't exist!"));//先查詢要更新的id對應的資料

    //判斷更新欄位是否合法,合法就set更新一下    
    	if(StringUtils.hasLength(name) && !playerIntoDB.getName().equals(name)){
            playerIntoDB.setName(name);
        }
        if(StringUtils.hasLength(email) && !playerIntoDB.getEmail().equals(email)){
            playerIntoDB.setEmail(email);
        }
        Player player = playerRepository.save(playerIntoDB);//儲存更新後的資料至一個新的物件並持久化到資料庫
        return PlayerConverter.convertPlayer(player);//返回一個更新之後的player
    }
...

測試

現在有的資料如下:

...
| 201 | 嬴政         | 男   | yingzheng@gmail.com        |    65 |   97 |   4.00 |
| 202 | 妲己         | 女   | daji@163.com               |    96 |   55 |  56.00 |
| 203 | 墨子         | 男   | mozi@qq.com                |    70 |  100 |  66.00 |
| 204 | 趙雲         | 男   | zhaoyun@gmail.com          |    40 |   88 |  30.00 |
| 205 | 小喬         | 女   | xiaoqiao@geekhour.net      |    83 |   60 |  59.00 |
| 206 | 廉頗         | 男   | lianpo@163.com             |    84 |   90 |  73.00 |
| 207 | 李白         | 男   | libai@qq.com               |    53 |   20 |  39.00 |
| 208 | 獨孤求敗     | 男   | duguqiubai@gmail.com       |   100 |  100 |   1.00 |
| 209 | 東方不敗     |      | dongfangbubai@geekhour.net |    95 |   95 |   2.00 |
+-----+--------------+------+----------------------------+-------+------+--------+
209 rows in set (0.00 sec)

更新第207條,用postman測試一下

沒問題,資料庫中也對應產生變化

專案打包

透過上面的步驟我們的專案demo已經開發完成,在springboot中透過編寫介面的方式,實現了對遠端連線的執行在Ubuntu下的MySQL資料庫的一個CURD操作,並使用postman對介面進行測試。

下面將專案進行打包

找到IDEA中的終端,輸入mvn clean install命令進行打包

啊哈!沒裝Maven。。。

Maven安裝

參考

首先去下載Maven,因為IDEA中標了個3.9.6(File--New Projects Setup--Settings for New Projects-->Build, Execution, Deployment--Build Tools--Maven),所以下個3.9.6保險一點

【™的安裝完整合IDEA後在終端還是識別不到,不搞了,之後有時間再更新】

bye,come back soon

相關文章