基于SpringBoot + Vue的个人博客系统02——简单的CRUD

简介

数据库使用 MySQL 、持久层框架使用 Mybatis-plus,本篇文章介绍对文章表简单的增删改查。

接口遵循 RESTful 规范,详情可参考:理解RESTful架构——阮一峰

代码实现

1、首先导入依赖

<!-- mybatis-plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.2</version>
</dependency>
<!-- mysql 数据库连接 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.18</version>
</dependency>

Mybatis-plus 官网:https://mp.baomidou.com

2、配置数据源,以及 dao 层日志级别

application-dev.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/blog?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
    username: root
    password: 111111
logging:
  level:
    pers.qianyucc.qblog.dao: trace

3、新建文章表

CREATE TABLE `articles` (
  `id` varchar(30) NOT NULL,
  `author` varchar(20) NOT NULL,
  `category` varchar(50) DEFAULT NULL,
  `tabloid` varchar(255) DEFAULT NULL,
  `content` text,
  `tags` varchar(50) DEFAULT NULL,
  `title` varchar(100) DEFAULT NULL,
  `type` int(11) DEFAULT NULL,
  `views` int(11) DEFAULT NULL,
  `gmt_create` bigint(20) DEFAULT NULL,
  `gmt_update` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4、新建对应的实体类

package pers.qianyucc.qblog.model.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.*;

import java.io.*;

@Data
@TableName("articles")
public class ArticlePO implements Serializable {
    private static final long serialVersionUID = -1849698844197610571L;
    @TableId
    private String id;
    private String author;
    private String title;
    private String content;
    private String tags;
    private Integer type;
    private String category;
    private Long gmtCreate;
    private Long gmtUpdate;
    private String tabloid;
    private Integer views;
}

说明:

  • @TableName:手动指定表名,如果表名和实体类名相同的话可以不加
  • @TableId:表示这个属性对应的是数据库中表的主键

5、新建 dao 层接口

package pers.qianyucc.qblog.dao;

import com.baomidou.mybatisplus.core.mapper.*;
import pers.qianyucc.qblog.model.entity.*;

@Mapper
public interface ArticleMapper extends BaseMapper<ArticlePO> {
}

说明:

  • Mybatis-plus 的使用方法和 Spring Data Jpa 类似,都是 dao层的接口继承了一个特定接口之后就有了对数据表操作的基本功能

6、配置分页,注意加上注解扫描

package pers.qianyucc.qblog.config;

import com.baomidou.mybatisplus.extension.plugins.*;
import com.baomidou.mybatisplus.extension.plugins.pagination.optimize.*;
import org.mybatis.spring.annotation.*;
import org.springframework.context.annotation.*;
import org.springframework.transaction.annotation.*;

@EnableTransactionManagement
@Configuration
@MapperScan("pers.qianyucc.qblog.dao")
public class MybatisPlusConfig {
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
        paginationInterceptor.setOverflow(false);
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
        paginationInterceptor.setLimit(10);
        // 开启 count 的 join 优化,只针对部分 left join
        paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
        return paginationInterceptor;
    }
}

7、创建 VO 转换接口,用于由 PO 转换成 VO

package pers.qianyucc.qblog.model.vo;

public interface IConverter<T,E> {
    /**
     * VO 转换函数
     *
     * @param t 目标对象
     * @return 转换结果
     */
    E convertToVO(T t);
}

说明:

  • 对于 PO 向 VO 的转化,我们可以直接在业务层使用他们的 set、get方法,但是这样的话代码可读性就会变差,常常会在业务层出现一大堆与业务无关的代码,这就是封装 PO 到 VO 转换器的理由。

8、创建 ArticleVO 类,并实现 PO 到 VO 转换的方法

package pers.qianyucc.qblog.model.vo;

import cn.hutool.core.bean.*;
import cn.hutool.core.bean.copier.*;
import lombok.*;
import pers.qianyucc.qblog.model.entity.*;
import pers.qianyucc.qblog.model.enums.*;
import pers.qianyucc.qblog.utils.*;

@Data
public class ArticleVO {
    private String id;
    private String author;
    private String title;
    private String content;
    private String[] tags;
    private String type;
    private String category;
    private String gmtCreate;
    private String gmtUpdate;
    private String tabloid;
    private Integer views;

    /**
     * 根据 PO 创建 VO 对象
     * @param articlePO PO对象
     * @return VO对象
     */
    public static ArticleVO fromArticlePO(ArticlePO articlePO) {
        return new Converter().convertToVO(articlePO);
    }

    private static class Converter implements IConverter<ArticlePO, ArticleVO> {
        @Override
        public ArticleVO convertToVO(ArticlePO article) {
            final ArticleVO vo = new ArticleVO();
            BeanUtil.copyProperties(article, vo, CopyOptions.create()
                    .ignoreNullValue().ignoreError());
            vo.setTags(article.getTags().split(","));
            for (ArticleTypeEnum item : ArticleTypeEnum.values()) {
                if (item.getFlag() == article.getType()) {
                    vo.setType(item.getNotes());
                }
            }
            vo.setGmtCreate(BlogUtils.formatDatetime(article.getGmtCreate()));
            vo.setGmtUpdate(BlogUtils.formatDatetime(article.getGmtUpdate()));
            return vo;
        }
    }
}

这里的文章类型为一个枚举类型

package pers.qianyucc.qblog.model.enums;

public enum ArticleTypeEnum {
    /**
     * 文章类型:原创
     */
    ORIGINAL("原创", 1),
    /**
     * 文章类型:转载
     */
    REPRINT("转载", 0);;
    private String notes;
    private int flag;

    public String getNotes() {
        return notes;
    }

    public int getFlag() {
        return flag;
    }

    ArticleTypeEnum(String notes, int flag) {
        this.notes = notes;
        this.flag = flag;
    }
}

9、定义 PageVO 来储存分页信息

package pers.qianyucc.qblog.model.vo;

import lombok.*;

import java.util.*;

@Data
@Builder
public class PageVO<T> {
    protected List<T> records;
    protected long total;
    protected long size;
    protected long current;
}

10、编写 ArticleService

package pers.qianyucc.qblog.service;

import com.baomidou.mybatisplus.extension.plugins.pagination.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.stereotype.*;
import pers.qianyucc.qblog.dao.*;
import pers.qianyucc.qblog.model.entity.*;
import pers.qianyucc.qblog.model.vo.*;

import java.util.*;
import java.util.stream.*;

@Service
public class ArticleService {
    @Autowired
    private ArticleMapper articleMapper;

    public PageVO<ArticleVO> getArticles(int page, int limit) {
        QueryWrapper<ArticlePO> qw = new QueryWrapper<>();
        qw.select(ArticlePO.class, i -> !"content".equals(i.getColumn()));
        Page<ArticlePO> res = articleMapper.selectPage(new Page<>(page, limit), qw);
        List<ArticleVO> articleVOS = res.getRecords().stream()
                .map(ArticleVO::fromArticlePO)
                .collect(Collectors.toList());
        PageVO<ArticleVO> pageVO = PageVO.<ArticleVO>builder()
                .records(articleVOS.isEmpty() ? new ArrayList<>() : articleVOS)
                .total(res.getTotal())
                .current(res.getCurrent())
                .size(res.getSize())
                .build();
        return pageVO;
    }
}

说明:

  • 因为我们分页查询的时候,通常不需要文章内容,所以这里使用 QueryWrapper 的 select 方法,过滤了 content 字段。

11、编写 ArticleController

package pers.qianyucc.qblog.controller;

import io.swagger.annotations.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.web.bind.annotation.*;
import pers.qianyucc.qblog.model.vo.*;
import pers.qianyucc.qblog.service.*;

@Api("与文章相关的api接口")
@RestController
public class ArticleController {
    @Autowired
    private ArticleService articleService;

    @ApiOperation("批量获取文章")
    @GetMapping("/articles")
    public Results<PageVO> getArticles(
            @ApiParam("页码")
            @RequestParam(required = false, defaultValue = "1") Integer page,
            @ApiParam("每页存放的记录数")
            @RequestParam(required = false, defaultValue = "5") Integer limit) {
        return Results.ok(articleService.getArticles(page, limit));
    }
}

12、启动项目,访问http://localhost:9000/api/v1/doc.html,使用 Swagger 测试接口

分页查询测试

插入

1、新建 ArticleDTO 封装从前端获取到的数据

package pers.qianyucc.qblog.model.dto;

import cn.hutool.core.bean.*;
import io.swagger.annotations.*;
import lombok.*;
import org.hibernate.validator.constraints.*;
import pers.qianyucc.qblog.model.entity.*;

import javax.validation.constraints.NotEmpty;

@Data
@ApiModel(value = "文章类", description = "前端传入的文章信息")
public class ArticleDTO {
    @NotEmpty(message = "文章作者不能为空")
    @ApiModelProperty(notes = "文章作者", example = "竹林笔墨")
    private String author;
    @NotEmpty(message = "文章标题不能为空")
    @ApiModelProperty(notes = "文章标题", example = "快速入门SpringBoot")
    private String title;
    @NotEmpty(message = "文章内容不能为空")
    @ApiModelProperty(notes = "文章内容")
    private String content;
    @NotEmpty(message = "文章标签不能为空")
    @ApiModelProperty(notes = "文章标签,用英文逗号隔开", example = "java,集合")
    private String tags;
    @Range(min = 0, max = 1, message = "文章类型必须为1或者0")
    @ApiModelProperty(notes = "文章类型,0表示转载,1表示原创", example = "1")
    private Integer type;
    @NotEmpty(message = "文章分类不能为空")
    @ApiModelProperty(notes = "文章分类", example = "设计模式")
    private String category;
    @NotEmpty(message = "文章简介不能为空")
    @ApiModelProperty(notes = "文章摘要")
    private String tabloid;

    /**
     * 将 DTO 转换为 PO
     *
     * @param articleDTO 要转换的DTO
     * @param isUpdate   此对象是否为更新对象
     * @return 转换结果
     */
    public ArticlePO toArticlePO(boolean isUpdate) {
        ArticlePO po = new Converter().convertToPO(this);
        po.setViews(isUpdate ? null : 0);
        po.setGmtCreate(isUpdate ? null : po.getGmtUpdate());
        return po;
    }

    private static class Converter implements IConverter<ArticleDTO, ArticlePO> {
        @Override
        public ArticlePO convertToPO(ArticleDTO articleDTO) {
            ArticlePO po = new ArticlePO();
            po.setGmtUpdate(System.currentTimeMillis());
            BeanUtil.copyProperties(articleDTO, po);
            return po;
        }
    }
}

说明:

package pers.qianyucc.qblog.model.dto;

public interface IConverter<T,E> {
    /**
     * 将对应的 DTO 转换为 PO
     * @param t 需要转换的 DTO 类
     * @return 转换结果
     */
    E convertToPO(T t);
}

2、ArticleConreoller

@PostMapping("/articles")
@ApiOperation("新增文章")
public Results postArticles(@ApiParam(name = "文章信息", value = "传入json格式", required = true)
                            @RequestBody @Valid ArticleDTO articleDTO) {
    String id = articleService.insArticle(articleDTO);
    return Results.ok(MapUtil.of("id", id));
}

说明:

  • @Valid:用于校验参数,校验失败的话就会抛出 MethodArgumentNotValidException 异常
  • MapUtil.of:为Hutool工具包中工具类,可以快速构建一个键值对的Map

3、ArticleService

public String insArticle(ArticleDTO articleDTO) {
    ArticlePO po = articleDTO.toArticlePO(false);
    String id = IdUtil.objectId();
    po.setId(id);
    articleMapper.insert(po);
    return id;
}

说明:

  • IdUtil 为 Hutool 工具类中一个id生成工具,这里的 ObjectId 是 MongoDB 数据库的一种唯一ID生成策略,是UUID version1的变种

4、别忘了全局异常处理(处理参数校验失败异常)

GlobalExceptionHandler

@ResponseBody
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.OK)
public Results handleValidationExceptions(MethodArgumentNotValidException e) {
    Map<String, String> errors = new HashMap<>();
    e.getBindingResult().getAllErrors().forEach((error) -> {
        String fieldName = ((FieldError) error).getField();
        String errorMessage = error.getDefaultMessage();
        errors.put(fieldName, errorMessage);
    });
    return Results.error("参数错误", errors);
}

5、使用 Swagger 测试

测试插入

当某一字段为空时:

插入时字段空白异常

根据 id 获取文章

1、ArticleController

@GetMapping("/article/{id}")
@ApiOperation("根据id查询文章信息")
@ApiImplicitParam(name = "id", value = "文章id", required = true, dataType = "String", paramType = "path")
public Results<ArticleVO> getArticle(@PathVariable String id) {
    ArticleVO articleVO = articleService.findById(id);
    return Results.ok(articleVO);
}

2、ArticleService

public ArticleVO findById(String id) {
    ArticlePO articlePO = articleMapper.selectById(id);
    if (Objects.isNull(articlePO)) {
        throw new BlogException(INVALID_ID);
    }
    return ArticleVO.fromArticlePO(articlePO);
}

说明:

  • 其中INVALID_IDErrorInfoEnum中定义:INVALID_ID(4008, "你的id不合法"),

使用 Swagger 测试

获取图片测试

删除文章

1、ArticleController

@DeleteMapping("/article/{id}")
@ApiOperation("根据id删除文章")
@ApiImplicitParam(name = "id", value = "文章id", required = true, dataType = "String", paramType = "path")
public Results deleteArticle(@PathVariable String id) {
    articleService.deleteArticle(id);
    return Results.ok("删除成功", null);
}

2、ArticleService

public void deleteArticle(String id) {
    int i = articleMapper.deleteById(id);
    if (i <= 0) {
        throw new BlogException(INVALID_ID);
    }
}

使用 Swagger 测试

删除文章测试

再次查询文章,发现文章已经不存在

再次访问,文章已不存在

修改文章

1、ArticleController 注意这里不使用 @Valid 注解,因为我们不一定总是需要更新全部字段

@PutMapping("/article/{id}")
@ApiOperation("修改文章")
@ApiImplicitParam(name = "id", value = "文章id", required = true, dataType = "String", paramType = "path")
public Results<Map<String, Object>> putArticles(@ApiParam(name = "要修改的文章信息", value = "传入json格式", required = true)
                                                @RequestBody ArticleDTO articleDTO,
                                                @PathVariable String id) {
    articleService.updateArticle(articleDTO, id);
    return Results.ok("更新成功", MapUtil.of("id", id));

2、ArticleService

public void updateArticle(ArticleDTO articleDTO, String id) {
    ArticlePO dbArticle = articleMapper.selectById(id);
    if (Objects.isNull(dbArticle)) {
        throw new BlogException(INVALID_ID);
    }
    ArticlePO articlePO = articleDTO.toArticlePO(true);
    articlePO.setId(id);
    articleMapper.updateById(articlePO);
}

先查询文章信息

查询文章信息

更新文章内容

更新文章内容

再次查询,发现文章内容已经修改

再次查询文章

参考代码:https://gitee.com/qianyucc/QBlog2/tree/v-2.0

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值