【冷启动#2】实用的springboot tutorial入门demo

news/2024/7/7 16:59:23/文章来源:https://www.cnblogs.com/DAYceng/p/18280514

跟着官方文档熟悉一遍创建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") //指定要映射到的数据库表中的具体Columnprivate 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{@Overridepublic 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{@Autowiredprivate PlayerRepository playerRepository;@Overridepublic 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 {@Autowiredprivate 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-demospring.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{@Autowiredprivate PlayerRepository playerRepository;@Overridepublic 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转换为PlayerDTOPlayerDTO 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{@Autowiredprivate PlayerRepository playerRepository;@Overridepublic 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 {@Autowiredprivate 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

...@RestControllerpublic class PlayerController {@Autowiredprivate 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

...@Servicepublic class PlayerServiceImpl implements PlayerService{@Autowiredprivate PlayerRepository playerRepository;@Overridepublic 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字段)是唯一的,所以在新增玩家数据时需要检查邮箱的唯一性

...@Servicepublic 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{@Autowiredprivate PlayerRepository playerRepository;@Overridepublic PlayerDTO getPlayerById(long id) { //此时可以直接调用父接口提供的方法进行查询//要把Player对象转换为一个PlayerDTO对象Player player = playerRepository.findById(id).orElseThrow(RuntimeException::new);//此处未写完return PlayerConverter.convertPlayer(player);}@Overridepublic 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转换为PlayerDTOPlayerDTO 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转换为PlayerPlayer player = new Player();player.setName(player.getName());player.setEmail(player.getEmail());return player;}
}

回到PlayerServiceImpl.java

	@Overridepublic 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.Playerat 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 test4springbootdemoCHARACTER SET utf8mb4 # 数据库的字符集为 utf8mb4COLLATE 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-demospring.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") //指定要映射到的数据库表中的具体Columnprivate 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: 0mysql> 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转换为PlayerDTOPlayer 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 playerMODIFY 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){ //没有返回值所以用voidplayerService.deletePlayerById(id);}...

然后去实现该接口(src/main/java/com/tutorial/boot_demo/service/PlayerServiceImpl.java

	...@Overridepublic 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,以及emailPlayer 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/738189.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CentOS8部署zerotier异地组网

本文介绍了在CentOS 8系统上部署ZeroTier以实现异地组网的方法,包括安装、配置、加入网络、开启IP转发和设置路由表等步骤,并提出进一步实现公网无缝组网的展望。CentOS8部署zerotier异地组网 CentOS8部署zerotier异地组网 一、前言 本文在此前研究部署FRP进行内网穿透,但FR…

《DNK210使用指南 -CanMV版 V1.0》第九章 打印输出实验

第九章 打印输出实验 1)实验平台:正点原子DNK210开发板 2)章节摘自【正点原子】DNK210使用指南 - CanMV版 V1.0 3)购买链接:https://detail.tmall.com/item.htm?&id=782801398750 4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/k210/ATK-DN…

分享一款可编辑本地电脑文件的在线编辑器

之前见过在线版的VSCode,被惊讶到了。网页上竟然可以编辑电脑本地的文件,打破了网页无法编辑本地电脑文件的限制。一直好奇怎么做的。抽空研究了一下,然后发现其实也不难。背景 之前见过在线版的VSCode,被惊讶到了。网页上竟然可以编辑电脑本地的文件,打破了网页无法编辑本…

xhcms

xhcms 目录结构 admin --管理后台文件夹 css --存放css的文件夹 files --存放页面的文件夹 images --存放图片的文件夹 inc --存放网站配置文件的文件夹 install --网站进行安装的文件夹 seacmseditor --编辑器文件夹 templ…

lrzsz安装完成后rz乱码

rz选择文件夹后乱码传输不到linux里面, 这时候只需要rz -be再选择文件 这样就可以进行后续的解压了tar -zxvf jdk-8u65-linux-x64.tar.gz

椭流线法设计配光器

本文介绍了利用椭流线法设计高效均匀的LED配光器,通过对边光原理、反射定律及椭圆几何特性的深入分析,结合Matlab和SolidWorks软件实现光学仿真,最终成功设计出接收效率高、均匀度优的配光器。椭流线法设计配光器 椭流线法设计配光器 一、设计原理 1、边光原理 边光原理是非…

wx云开发增删改查

首先是.wxml文件,此处为固定数据的新增<button type="primary" bind:tap="addData"> //调用.js中addData方法插入数据 </button>对应.js文件//添加数据addData(){wx.showLoading({ //showLoading(api接口)(防止用户多次…

剪裁法设计配光器

剪裁法设计配光器通过光源角分割和目标面分割,利用边光原理和反射定律,计算并构建光学母线,以实现高均匀度和高光效的均匀圆斑光学设计。剪裁法设计配光器 剪裁法设计配光器 一、设计原理边光原理 边光原理是非成像光学中的一个基础原理,其内容可以表述为:来自光源边缘的光…

iOS-列表视图

在iOS开发中,UITableView和UICollectionView是两个非常核心的用于展示集合数据的UI组件。它们都能以列表的形式展示数据,但各自的特点和使用场景有所不同。 UITableView UITableView用于展示和管理垂直滚动的单列数据列表。它是以行的形式展示数据,每行(cell)可以展示相同…

CPC配光系统设计

本文详细介绍了CPC(复合抛物线聚光器)配光系统的设计过程,包括设计原理、抛物流线几何特性及其设计要求和流程,并通过Matlab和SolidWorks绘制模型,最后在TracePro中进行仿真验证,确保系统满足均匀照度和高效接收率的目标。CPC配光系统设计 CPC配光系统设计 一、设计原理 …

VMware vSphere Tanzu部署_14_部署容器应用

1.部署运行容器应用 1.1. 登录tkc集群 jianhua@napp:~/tkc$ kubectl vsphere login --server=192.168.203.194 \ --tanzu-kubernetes-cluster-name tkc-dev-cluster \ --tanzu-kubernetes-cluster-namespace tkc-01 \ --vsphere-username administrator@vsphere.local \ --ins…

贝塞尔曲线原理、推导及Matlab实现

本文详细解析了贝塞尔曲线的定义、性质、构建方法以及多种阶数的推导公式,并提供了完整的Matlab代码用于绘制和计算贝塞尔曲线。贝塞尔曲线原理、推导及Matlab实现 贝塞尔曲线原理、推导及Matlab实现 一、简介 贝塞尔曲线提出 在数学的数值分析领域中,贝塞尔曲线(English:B…

刘积仁的大健康“长跑”

软件是一个长命的产业,但软件企业的寿命都很短。懂得怕死,才能有机会活得长久。 这一次,刘积仁又为东软医疗找到了强大助力!中国通用技术(集团)控股有限责任公司(以下简称通用技术集团)所属资本公司战略投资东软集团在医疗健康领域资的创新业务公司——东软医疗,双方也由…

【AppStore】一文让你学会IOS应用上架Appstore

咱们国内现在手机分为两类,Android手机与苹果手机,现在用的各类APP,为了手机的使用安全,避免下载到病毒软件,官方都极力推荐使用手机自带的应用商城进行下载,但是国内Android手机品类众多,手机商城各式各样,做不到统一,所以Android的APP上架得一个一个平台去申请上架,…

关于airtest生成的报告中缺少poco语句问题

1、airtest生成的报告只显示airtest的相关操作,如果是poco和airtest-selenium的操作则不记录。因此需要在报告中引用插件。支持poco语句插件,poco.utils.airtest.report 支持airtest-selenium语句插件,airtest_selenium.report2、在IDE运行 .py 脚本报告生成的依据是脚本运行…

xshell7的下载ssh远程连接

1.下载地址家庭/学校免费 - NetSarang Website (xshell.com) 2.下载后一路next,来到主页面,我们来连接一下试一试吧,这里主机就是ifconfig得到的 3.用户名一般都是root密码是自己设置的那个 4.连接的时候注意比如我要连接Node1那么node1就要保持开启并却防火墙已经关闭 5.看一…

xhcms1.0

xhcms1.0 目录结构 admin --管理后台文件夹 css --存放css的文件夹 files --存放页面的文件夹 images --存放图片的文件夹 inc --存放网站配置文件的文件夹 install --网站进行安装的文件夹 seacmseditor --编辑器文件夹 te…

关于巴图自动化Profinet协议转Modbus协议网关模块怎么配置IP地址教学

Profinet协议和Modbus协议是工业通讯常用协议,通过巴图自动化PN转Modbus网关模块(BT-MDPN10)实现连接。常见的协议有:ModbusTCP协议,Profibus协议,Profibus DP协议,EtherCAT协议,EtherNET协议,CAN,CANOPEN等Profinet协议和Modbus协议是工业领域中常用的两种通讯协议,…

基础篇:Stable Diffusion 基础原理详述

【基础篇】Stable Diffusion 基础原理详述前言我认为学习 ComfyUI 应该先从理论学起。与传统绘图工具(如 Photoshop 或 Figma)相比,AI 绘图工具有着显著不同。首先,许多设置和操作在 AI 绘图工具中是非可视化的,这意味着即使你更改了某个配置,界面上也未必会有任何变化,…