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

springboot,tutorial,demo · 浏览次数 : 10

小编点评

本文介绍了如何在Spring Boot框架下创建一个基本的玩家数据管理项目。项目旨在熟悉Spring Boot的使用流程,包括创建工程、数据库操作、API接口编写以及前后端交互。 **项目概述** - **项目目标**:管理玩家数据 - **技术栈**:Spring Boot、MySQL - **项目结构**:前端API层、Service层、DataAccess层 **开发流程** 1. **创建Spring Boot项目** - 选择Spring Boot版本 - 设置项目名称和依赖 2. **数据库设计与实现** - 创建数据库和表结构 - 编写DAO层代码 - 实现Player实体类和Repository接口 3. **API接口编写** - 创建Controller层代码 - 编写Service层代码 - 实现PlayerService接口 - 编写PlayerServiceImpl实现类 4. **前后端交互** - 编写前端代码 - 使用Postman测试API接口 5. **项目打包与测试** - 打包项目 - 在Ubuntu下运行项目 **遇到的问题及解决方法** - **ID自增问题**:数据库中id字段未设置为自增主键,导致无法正常插入数据。 - **空值问题**:convertPlayer方法错误地处理了空值,导致数据不全。 - **数据库权限问题**:尝试删除数据时,报错提示权限不足。 **总结** 通过本项目的开发,读者可以熟悉Spring Boot框架的使用,掌握数据库操作的CRUD方法,并学会编写RESTful API。同时,项目还包含了前后端交互的实践,有助于提升开发技能。

正文

跟着官方文档熟悉一遍创建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

与【冷启动#2】实用的springboot tutorial入门demo相似的内容:

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

跟着官方文档熟悉一遍创建spring工程的步骤 https://spring.io/guides/gs/spring-boot https://juejin.cn/post/7077958723829760008 demo简介 整个demo的预期目标是: 管理一堆玩家的数据,数据库使用的是现成的我们

【冷启动#1】实用的MySQL基础

简单安装一下MySQL Windows下(5.7.x) 本体安装 1、首先先下载安装包,名字如下: mysql-5.7.19-winx64.zip 2、配置环境变量,将解压之后的bin目录添加一下 3、在解压目录下创建my.ini文件,内容如下: [ client ] port=3306 defau

推荐2款.NET开源、轻便、实用的Windows桌面启动器

Flow Launcher Flow Launcher是一款.NET开源(MIT License)、免费、功能强大、方便实用的 Windows 文件搜索和应用程序启动器,能够帮助你快速查找文件、启动应用程序和执行系统操作,提高工作效率和操作便利性。并且生态完善,有插件商店,你可以查看完整的插件列表,

我的第一个项目(四):(前端)发送请求以及表单校验

好家伙,本篇将继续完善前端界面 效果展示: 1.注册登陆 (后端已启动) 2.注册表单验证 (前端实现的表单验证) 在此之前: 我的第一个项目(二):使用Vue做一个登录注册界面 - 养肥胖虎 - 博客园 (cnblogs.com) 后端部分: 我的第一个项目(三):注册登陆功能(后端) - 养肥胖

[转帖]systemd写微服务启动脚本范例

https://blog.csdn.net/m0_46897923/article/details/125338465?spm=1001.2014.3001.5501 systemd写微服务启动脚本范例 1.下面是一个使用systemd写的服务的启动脚本模板:2.下面是笔者服务的实例3.system

[转帖]PostgreSQL进程结构

http://www.pgsql.tech/article_101_10000099 1、简介 本文简单的介绍了 PostgreSQL 的主要进程类型与功能。 2、PostgreSQL进程分为主进程与辅助进程。 2.1、主进程: PostMaster进程是整个数据库实例的总控进程,负责启动关闭该数据

.NET静态代码织入——肉夹馍(Rougamo)发布2.0

肉夹馍(https://github.com/inversionhourglass/Rougamo)通过静态代码织入方式实现AOP的组件,其主要特点是在编译时完成AOP代码织入,相比动态代理可以减少应用启动的初始化时间让服务更快可用,同时还能对静态方法进行AOP。 摆烂半年又一更,感谢各位的支持,那

MongoDB安装、基础操作和聚合实例详解

虽然MongoDB这些年很流行,但笔者之前没研究过,现在有需求研究这类NoSQL的数据库,是为了验证其是否可被替换。 MongoDB是很轻量的文档数据库,简单测试也懒得专门准备虚拟机环境了,直接在macOS上安装测试下其基础功能。 1.使用 Homebrew 安装 MongoDB 2.启动/停止 M

[转帖]服务注册与发现:Nacos Discovery

目录 一、概述 二、Nacos discovery——服务的注册与发现 1. 版本关系 2. 下载安装 (1)下载 (2)启动 (3)浏览器访问 三、Nacos服务注册与发现实战 1. 构建Spring Cloud Alibaba工程 (1)创建父工程 (2)创建子项目 2. 编写测试Control

Unity 利用Cache实现边下边玩

现在手机游戏的常规更新方案都是在启动时下载所有资源更新,游戏质量高的、用户粘性大的有底气,先安装2个G,启动再更新2个G,文件小了玩家还觉得品质不行不想玩。 最近在做微信、抖音小游戏,使用他们提供的资源缓存方案,现在要转成Android APP, 也想用这种边下边玩的机制把首包做小。 其实很简单,直