实验:基于SpringBoot+MyBatis-Plus实现文章列表增删改查

发布于:2025-06-03 ⋅ 阅读:(32) ⋅ 点赞:(0)

实验内容

  1. 搭建Spring Boot后台,使用MapperServiceController三层架构。
  2. 使用Mybatis-Plus访问数据库。
  3. 必须用到Mybatis-Plus自带的增删改查功能,还有分页查询功能。
  4. 查询不能直接写SQL语句,必须用到条件构造器(QueryWrapperLambdaQueryWrapper)。
  5. 必须用到参数校验。
  6. 根据接口文档实现处理请求的方法。

前言

实验内容的前三个和上一个实验:搭建Spring Boot+MyBatis-Plus后台一样,具体如何创建SpringBoot项目、添加依赖和创建MapperServiceController,还有添加分页和配置类就不详细说明了,还有数据库连接参数的设置。
不一样的地方就是实体类由Person换成了Article,大差不差的。

一、添加新的依赖

跟上次实验不一样的是,添加了新的依赖,首先是lombok用于自动生成GetterSetter方法

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

然后是参数校验用的依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

二、配置连接MySQL数据库

修改了src/main/resources文件夹下的application.properties文件的扩展名为层级分明的.yml.yaml文件,添加/修改内容为如下配置。

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver # 连接操作MySQL数据库的类
    url: jdbc:mysql://localhost:3306/exp2 # 最后为数据库的名字、再前一个是端口
    username: root # 数据库用户名
    password: 123456 # 数据库密码

接下来就是在数据库中创建后面会用到的表,根据接口文档中获取文章详情的响应数据样例中返回的文章data内容可以推断出article表所拥有的字段,顺便再创建一个数据库,所生成的SQL如下。

DROP DATABASE IF EXISTS exp2;
CREATE DATABASE exp2 CHARACTER SET utf8;

USE exp2;

CREATE TABLE article (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    content TEXT NOT NULL,
    cover_img VARCHAR(255),
    state VARCHAR(50),
    category_id INT,
    create_date DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_date DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) CHARSET=utf8;

三、创建实体类以及Mapper、Service和Controller三层架构

POJO

实体类Article相对于上次实验手动一个一个地添加GetterSetter方法,这次就用了lombok来简化代码,让其自动生成,另外使用了@JsonFormat注解规定了Date在转换成Json后的格式。。

package org.peanut.exp2.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;

@Data
public class Article {
    @TableId(type = IdType.AUTO)
    private Integer id;

    private String title;
    private String content;
    private String coverImg;
    private String state;
    private Integer categoryId;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createDate;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateDate;
}

Mapper

package org.peanut.exp2.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.peanut.exp2.pojo.Article;

public interface ArticleMapper extends BaseMapper<Article> {
}

Service

IService

package org.peanut.exp2.service;

import com.baomidou.mybatisplus.extension.service.IService;
import org.peanut.exp2.pojo.Article;

import java.util.List;

public interface IArticleService extends IService<Article> {
}

ServiceImpl

package org.peanut.exp2.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.peanut.exp2.mapper.ArticleMapper;
import org.peanut.exp2.pojo.Article;
import org.peanut.exp2.service.IArticleService;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article>
        implements IArticleService {
}

Controller

package org.peanut.exp2.controller;

import org.peanut.exp2.service.IArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
public class ArticleController {

    @Autowired
    IArticleService articleService;
}

四、添加配置类、响应类和全局异常处理类

配置类主要是Mybatis-Plus的,用于添加它的分页插件以及相关配置,如Mapper扫描配置。

package org.peanut.exp2.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("org.peanut.exp2.mapper") // 记得改成自己Mapper所在的路径
public class MyBatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

然后根据接口文档的响应数据样例,可以发现返回的内容有很高的相似性,为此创建了响应类Result<T>,而且用到了泛型,为了提高通用性,因为里面的数据类型是多样的,以及创建了快速生成成功或失败响应的静态方法,添加的@Getter注解是为了SpringBoot在响应时能够正常地生成Json格式的内容。

package org.peanut.exp2.common;

import lombok.Getter;

@Getter
public class Result<T> {
    Integer code;
    String message;
    T data;

    public Result(Integer code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public static <T> Result<T> success(T data) {
        return new Result<>(0, "操作成功", data);
    }

    public static <T> Result<T> error(String message) {
        return new Result<>(1, message, null);
    }
}

为了让接口的调用者能够知道SpringBoot发出的所有异常,并通过Result中的message来响应,所以创建了GlobalExceptionHandler类。

package org.peanut.exp2.handler;

import org.peanut.exp2.common.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public Result<Void> exception(Exception e) {
        e.printStackTrace();
        return Result.error(e.getMessage());
    }
}

五、根据接口文档编写控制器方法并测试接口

1.新增文章接口

1.1 基本信息

接口描述:该接口用于新增文章(发布文章)
请求路径:/article
请求方式: POST

1.2 请求参数

请求参数格式:application/json
请求参数说明:

参数名称 说明 类型 是否必须 备注
title 文章标题 string 1~10个非空字符
content 文章正文 string -
coverImg 封面图像地址 string 必须是url地址
state 发布状态 string 已发布 | 草稿
categoryId 文章分类ID number -

请求参数样例:

{
    "title": "陕西旅游攻略",
    "content": "兵马俑,华清池,法门寺,华山...爱去哪去哪...",
    "coverImg": "https://big-event-gwd.oss-cn-asdbeijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
    "state": "草稿",
    "categoryId": 2
}

1.3 响应数据

响应数据类型:application/json
响应参数说明:

名称 类型 是否必须 默认值 备注 其他信息
code number 必须 响应码, 0-成功,1-失败
message string 非必须 提示信息
data object 非必须 返回的数据

响应数据样例:

{
    "code": 0,
    "message": "操作成功",
    "data": null
}

处理请求的方法

根据接口文档可以得知请求路径为/article,且请求方法为POST,那么用的就是@PostMapping注解,然后根据请求格式application/json以及请求样例,可以确定传的参数为实体类,所以参数使用了@RequestBody注解,只是在数据库创建一条数据,所以直接用MyBatis-Plus写好的save方法就行。最后根据响应样例,判断是否返回数据,然后返回相应处理结果的响应。

    @PostMapping("/article")
    public Result<Void> addArticle(@RequestBody Article article) {
        judgeArticleValid(article);
        if(articleService.save(article)) {
            return Result.success(null);
        }
        return Result.error("操作失败");
    }

而调用的judgeArticleValid方法是用来校验传过来的实体类参数,根据文档的参数说明进行校验,否则抛出带有message的校验异常。

    private void judgeArticleValid(Article article) {
        if(article.getTitle().isEmpty() || article.getTitle().length() > 10 || article.getTitle().isBlank()) {
            throw new ValidationException("文章标题不允许为空或只有空字符,且长度需要在1~10内!");
        }
        if (article.getContent() == null) {
            throw new ValidationException("文章正文不允许为空!");
        }
        if (article.getCoverImg() == null
                || !article.getCoverImg().matches("^(https?|ftp)://[\\w.-]+(?:/[\\w.-]*)*")) {
            throw new ValidationException("封面图像地址不允许为空,且必须为URL地址!");
        }
        if (article.getState() == null || !article.getState().matches("已发布|草稿")) {
            throw new ValidationException("发布状态不能为空,且只能是“已发布”或“草稿”!");
        }
        if(article.getCategoryId() == null) {
            throw new ValidationException("文章分类ID不允许为空!");
        }
    }

测试接口

然后就是使用Postman进行接口测试,首先是成功请求的效果,如下图所示,选择POST方法,填入请求链接并添加JSON的参数。
在这里插入图片描述
其他就再演示一个使用正则校验的封面图像地址校验效果,如下图所示。
在这里插入图片描述

2.文章列表(条件分页)接口

1.1 基本信息

接口描述:该接口用于根据条件查询文章,带分页
请求路径:/article
请求方式: GET

1.2 请求参数

请求参数格式:queryString
请求参数说明:

参数名称 说明 类型 是否必须 备注
pageNum 当前页码 number
pageSize 每页条数 number
categoryId 文章分类ID number
state 发布状态 string 已发布|草稿

请求参数样例:

?pageNum=1&pageSize=3&categoryId=2&state=草稿

1.3 响应数据

响应数据类型:application/json
响应参数说明:

名称 类型 是否必须 默认值 备注 其他信息
code number 必须 响应码, 0-成功,1-失败
message string 非必须 提示信息
data object 必须 返回的数据
|-total number 必须 总记录数
|-items array 必须 数据列表
|-id number 非必须 主键ID
|-title string 非必须 文章标题
|-content string 非必须 文章正文
|-coverImg string 非必须 文章封面图像地址
|-state string 非必须 发布状态 已发布 | 草稿
|-categoryId number 非必须 文章分类ID
|-createTime string 非必须 创建时间
|-updateTime string 非必须 更新时间

响应数据样例:

{
    "code": 0,
    "message": "操作成功",
    "data": {
        "total": 1,
        "items": [
            {
                "id": 5,
                "title": "陕西旅游攻略",
                "content": "兵马俑,华清池,法门寺,华山...爱去哪去哪...",
                "coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
                "state": "草稿",
                "categoryId": 2,
                "createTime": "2025-06-01 11:55:30",
                "updateTime": "2025-06-01 11:55:30"
            }
        ]
    }
}

处理请求的方法

请求路径一样为/article,且请求方法为GET,那么用的就是@GetMapping注解,然后根据请求格式queryString以及请求样例,可以确定需要传的参数,并且参数使用需要用@RequestParam注解,且非必要的参数设置requiredfalse。其中用到了@Pattern注解来对参数进行校验,用了这个就得在控制类前添加@Validated注解。最后根据响应样例,判断是否返回数据,然后返回相应处理结果的响应。

    @GetMapping("/article")
    public Result<ArticleList> getArticleList(
            @RequestParam Integer pageNum,
            @RequestParam Integer pageSize,
            @RequestParam(required = false) Integer categoryId,
            @RequestParam(required = false) @Pattern(regexp = "已发布|草稿", message = "发布状态只能是“已发布”或“草稿”")  String state) {

        Page<Article> page = new Page<>(pageNum, pageSize);
        ArticleList al = new ArticleList(articleService.getArticleList(page, categoryId, state));
        return Result.success(al);
    }

其中不仅到了分页,还有自定义条件查询,为此在IArticleService接口创建了一个带条件的分页查询的抽象方法,来获取满足条件的文章列表。

    List<Article> getArticleList(Page<Article> page, Integer categoryId, String state);

然后是在ArticleServiceImpl实现这个新添加的方法,里面用到了条件构造器LambdaQueryWrapper来进行条件查询,只有在条件参数不为空才作为其中一个过滤条件。

    @Override
    public List<Article> getArticleList(Page<Article> page, Integer categoryId, String state) {
        LambdaQueryWrapper<Article> lqw = new LambdaQueryWrapper<>();
        // 当条件不为空才添加到生成的 SQL 中
        lqw.eq(categoryId != null, Article::getCategoryId, categoryId);
        lqw.eq(state != null, Article::getState, state);
        return baseMapper.selectList(page, lqw);
    }

根据响应数据样例,返回的数据不能只是普通的List,还多了一个属性,为此创建了一个新类ArticleList来作为响应的data类型。

package org.peanut.exp2.pojo;

import lombok.Data;
import java.util.List;

@Data
public class ArticleList {
    int total;
    List<Article> items;

    public ArticleList(List<Article> list) {
        this.total = list.size();
        this.items = list;
    }
}

测试接口

接下来就是测试这个接口了,首先是成功请求的效果,选择POST方法,填入请求链接并添加查询参数。
在这里插入图片描述
然后是缺少查询参数的效果。
在这里插入图片描述

2.获取文章详情接口

1.1 基本信息

接口描述:该接口用于根据ID获取文章详细信息
请求路径:/article/detail
请求方式: GET

1.2 请求参数

请求参数格式:queryString
请求参数说明:

参数名称 说明 类型 是否必须 备注
id 主键ID number

请求参数样例:

?id=1

1.3 响应数据

响应数据类型:application/json
响应参数说明:

名称 类型 是否必须 默认值 备注 其他信息
code number 必须 响应码, 0-成功,1-失败
message string 非必须 提示信息
data object 必须 返回的数据
|-id number 非必须 主键ID
|-title string 非必须 文章标题
|-content string 非必须 文章正文
|-coverImg string 非必须 文章封面图像地址
|-state string 非必须 发布状态 已发布 | 草稿
|-categoryId number 非必须 文章分类ID
|-createTime string 非必须 创建时间
|-updateTime string 非必须 更新时间


响应数据样例:

{
    "code": 0,
    "message": "操作成功",
    "data": {
        "id": 4,
        "title": "北京旅游攻略",
        "content": "天安门,颐和园,鸟巢,长城...爱去哪去哪...",
        "coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
        "state": "已发布",
        "categoryId": 2,
        "createTime": "2025-06-01 11:57:30",
        "updateTime": "2025-06-01 11:57:30"
    }
}

处理请求的方法

请求路径为/article/detail,且请求方法也为GET,那么用的就是@GetMapping注解,然后根据请求格式queryString以及请求样例,可以确定需要传的参数,并且为参数添加@RequestParam注解。根据响应样例确定data的数据类型。

    @GetMapping("/article/detail")
    public Result<Article> getArticle(@RequestParam Integer id) {
        return Result.success(articleService.getById(id));
    }

测试接口

测试查询成功效果

在这里插入图片描述
无论是否查询到对应的数据,都会是成功的,除非是没有传入查询参数。
在这里插入图片描述

4.更新文章接口

1.1 基本信息

接口描述:该接口用于更新文章信息
请求路径:/article
请求方式: PUT

1.2 请求参数

请求参数格式:application/json
请求参数说明:

参数名称 说明 类型 是否必须 备注
id 主键ID number
title 文章标题 string
content 文章正文 string
coverImg 封面图像地址 string
state 发布状态 string 已发布 | 草稿
categoryId 文章分类ID number

请求参数样例:

{
    "id": 4,
    "title": "北京旅游攻略",
    "content": "天安门,颐和园,鸟巢,长城...爱去哪去哪...",
    "coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
    "state": "已发布",
    "categoryId": 2
}

1.3 响应数据

响应数据类型:application/json
响应参数说明:

名称 类型 是否必须 默认值 备注 其他信息
code number 必须 响应码, 0-成功,1-失败
message string 非必须 提示信息
data object 非必须 返回的数据

响应数据样例:

{
    "code": 0,
    "message": "操作成功",
    "data": null
}

处理请求的方法

请求路径为/article,且请求方法为PUT,那么用的就是@PutMapping注解,根据请求格式以及请求样例,可以确定传的参数为实体类,虽然参数说明中没有明确地对参数进行内容限制,不过应该还是要和新建文章的限制保持一致,所以也调用了judgeArticleValid进行参数校验,另外还需要校验id是否为空,因为是靠id来更新数据库内容的。

    @PutMapping("/article")
    public Result<Void> updateArticle(@RequestBody Article article) {

        if (article.getId() == null) {
            throw new ValidationException("文章id不允许为空!");
        }
        judgeArticleValid(article);
        if(articleService.updateById(article)) {
            return Result.success(null);
        }
        return Result.error("操作失败");
    }

测试接口

更新文章成功效果。
在这里插入图片描述
缺少文章id参数的校验效果。
在这里插入图片描述

5.删除文章接口

1.1 基本信息

接口描述:该接口用于根据ID删除文章
请求路径:/article
请求方式: DELETE

1.2 请求参数

请求参数格式:queryString
请求参数说明:

参数名称 说明 类型 是否必须 备注
id 主键ID number

请求参数样例:

?id=1

1.3 响应数据

响应数据类型:application/json

名称 类型 是否必须 默认值 备注 其他信息
code number 必须 响应码, 0-成功,1-失败
message string 非必须 提示信息
data object 非必须 返回的数据

响应数据样例:

{
    "code": 0,
    "message": "操作成功",
    "data": null
}

处理请求的方法

请求路径为/article,且请求方法也为DELETE,那么用的就是@DeleteMapping注解,然后根据请求格式queryString以及请求样例,可以确定需要传的参数,并且为参数添加@RequestParam注解。

    @DeleteMapping("/article")
    public Result<Void> deleteArticle(@RequestParam Integer id) {
        if (articleService.removeById(id)) {
            return Result.success(null);
        }
        return Result.error("操作失败");
    }

测试接口

删除文章成功效果。
在这里插入图片描述
当对应id的文章不存在时,会响应失败。
在这里插入图片描述
缺少id参数也是不会成功的。
在这里插入图片描述

总结

项目文件结构如下图所示。
在这里插入图片描述
有新增内容的代码总览。
IArticleService

package org.peanut.exp2.service;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import org.peanut.exp2.pojo.Article;

import java.util.List;

public interface IArticleService extends IService<Article> {
    List<Article> getArticleList(Page<Article> page, Integer categoryId, String state);
}

ArticleServiceImpl

package org.peanut.exp2.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.peanut.exp2.mapper.ArticleMapper;
import org.peanut.exp2.pojo.Article;
import org.peanut.exp2.service.IArticleService;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ArticleServiceImpl extends ServiceImpl<ArticleMapper, Article>
        implements IArticleService {

    @Override
    public List<Article> getArticleList(Page<Article> page, Integer categoryId, String state) {
        LambdaQueryWrapper<Article> lqw = new LambdaQueryWrapper<>();
        // 当条件不为空才添加到生成的 SQL 中
        lqw.eq(categoryId != null, Article::getCategoryId, categoryId);
        lqw.eq(state != null, Article::getState, state);
        return baseMapper.selectList(page, lqw);
    }
}

ArticleController

package org.peanut.exp2.controller;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.validation.ValidationException;
import jakarta.validation.constraints.Pattern;
import org.peanut.exp2.pojo.Article;
import org.peanut.exp2.common.Result;
import org.peanut.exp2.pojo.ArticleList;
import org.peanut.exp2.service.IArticleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@Validated
public class ArticleController {

    @Autowired
    IArticleService articleService;

    @PostMapping("/article")
    public Result<Void> addArticle(@RequestBody Article article) {
        judgeArticleValid(article);
        if(articleService.save(article)) {
            return Result.success(null);
        }
        return Result.error("操作失败");
    }

    @GetMapping("/article")
    public Result<ArticleList> getArticleList(
            @RequestParam Integer pageNum,
            @RequestParam Integer pageSize,
            @RequestParam(required = false) Integer categoryId,
            @RequestParam(required = false) @Pattern(regexp = "已发布|草稿", message = "发布状态只能是“已发布”或“草稿”")  String state) {

        Page<Article> page = new Page<>(pageNum, pageSize);
        ArticleList al = new ArticleList(articleService.getArticleList(page, categoryId, state));
        return Result.success(al);
    }

    @GetMapping("/article/detail")
    public Result<Article> getArticle(@RequestParam Integer id) {
        return Result.success(articleService.getById(id));
    }

    @PutMapping("/article")
    public Result<Void> updateArticle(@RequestBody Article article) {

        if (article.getId() == null) {
            throw new ValidationException("文章id不允许为空!");
        }
        judgeArticleValid(article);
        if(articleService.updateById(article)) {
            return Result.success(null);
        }
        return Result.error("操作失败");
    }

    @DeleteMapping("/article")
    public Result<Void> deleteArticle(@RequestParam Integer id) {
        if (articleService.removeById(id)) {
            return Result.success(null);
        }
        return Result.error("操作失败");
    }

    private void judgeArticleValid(Article article) {
        if(article.getTitle().isEmpty() || article.getTitle().length() > 10 || article.getTitle().isBlank()) {
            throw new ValidationException("文章标题不允许为空或只有空字符,且长度需要在1~10内!");
        }
        if (article.getContent() == null) {
            throw new ValidationException("文章正文不允许为空!");
        }
        if (article.getCoverImg() == null
                || !article.getCoverImg().matches("^(https?|ftp)://[\\w.-]+(?:/[\\w.-]*)*")) {
            throw new ValidationException("封面图像地址不允许为空,且必须为URL地址!");
        }
        if (article.getState() == null || !article.getState().matches("已发布|草稿")) {
            throw new ValidationException("发布状态不能为空,且只能是“已发布”或“草稿”!");
        }
        if(article.getCategoryId() == null) {
            throw new ValidationException("文章分类ID不允许为空!");
        }
    }
}

参考

官方文档

安装 | MyBatis-Plus
分页插件 | MyBatis-Plus
条件构造器 | MyBatis-Plus

用到的软件

IntelliJ IDEA
Postman API Platform


网站公告

今日签到

点亮在社区的每一天
去签到