SpringBoot项目拥抱Mybatis-Plus持久层框架实践

本文目录

SpringBoot项目拥抱Mybatis-Plus持久层框架实践_第1张图片

前言

自从 Mybatis-Plus推出以来,越来越多的公司在自己的项目中选择Mybatis-Plus框架替换了持久层框架Mybatis。因为Mybatis-Plus用起来既有Mybatis的手写复杂sql语句的灵活性,又兼具了Spring Data Jpa自动提供了单表CRUD操作的通用框架方法,只需要自定义一个Mapper并继承BaseMapper即可,为开发人员使用持久层框架节约了很多工作量。同时Mybatis-Plus还提供了链式查询和分页查询等诸多通用API方法,开发人员可直接使用。本文的目的是指导新手如何在自己的spring-boot项目中集成mybatis-plus持久层框架完成数据的增删改查功能。

1 Mybatis-Plus简介

MyBatis-Plus (简称 MP)是一个 MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

愿景: 我们的愿景是成为 MyBatis 最好的搭档,就像 魂斗罗 中的 1P、2P,基友搭配,效率翻倍。

SpringBoot项目拥抱Mybatis-Plus持久层框架实践_第2张图片

1.1 特性

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  • 内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

1.2 支持数据库

任何能使用 mybatis 进行 CRUD, 并且支持标准 SQL 的数据库,具体支持情况如下:

  • mysql,oracle,db2,h2,hsql,sqlite,postgresql,sqlserver,Phoenix,Gauss ,clickhouse,Sybase,OceanBase,Firebird,cubrid,goldilocks,csiidb
  • 达梦数据库,虚谷数据库,人大金仓数据库,南大通用(华库)数据库,南大通用数据库,神通数据库,瀚高数据库

1.3 框架结构

SpringBoot项目拥抱Mybatis-Plus持久层框架实践_第3张图片

2 快速开始

2.1 新建spring-boot项目

打开IDEA,依次点击File->New->Project->选择Spring Initializr,为节省接下来项目下载依赖包需要花费太长的时间,首先点击下图中第一个红色方框中最右侧的设置图标按钮,将ServerUrl参数值改为阿里的地址:http://start.aliyun.com(默认地址为https://spring.io/)

然后填写好项目名以及项目在本地电脑上的存储位置(下图中的Name和Location右侧输入框中的内容),同时命名好项目的GroupId和artifactId(下图中Group和Artifact右侧输入框中的内容),Java版本选择8

SpringBoot项目拥抱Mybatis-Plus持久层框架实践_第4张图片
然后点击Next按钮进入选择依赖控制界面,我们选择项目需要的依赖,这里笔者选择了Lombok、Spring Web、Mybatis Plus Framework、 MySQL Driver、Apache Commons Lang 和Fastjson等项目依赖模块
SpringBoot项目拥抱Mybatis-Plus持久层框架实践_第5张图片
然后点击Finish按钮生产项目骨架,程序帮我们选择了Spring Boot 2.3.7.RELEASE版本,Mybatis-Plus版本自动选择了3.4.2版本。但是笔者在实践的过程中发现项目启动时报了一系列Spring Boot 2.3.7.RELEASE版本中内置的springframeword-XX-5.2.12.RELEASE.jar无法打开的错误问题,于是我把Spring Boot版本换成了2.2.7.RELEASE版本,它内置的springframwork版本使用了5.2.6.RELEASE版本,这个版本的jar包在项目启动时没有出现打不开的错误。然后我在Spring Boot项目的启动类时添加@MapperScan注解时始终无法找到这个注解,笔者估计是3.4.2版本的Mybatis-Plus与2.2.7.RELEASE版本的Spring Boot不兼容,于是把Mybatis-Plus的版本也换成了3.1.0版本的,换了之后发现可以找到@MapperScan这个注解了。

使用spring-boot-2.3.7.RELEASE版本启动报错日志:

java: 读取D:\mavenRepository\.m2\repository\org\springframework\spring-jdbc\5.2.12.RELEASE\spring-jdbc-5.2.12.RELEASE.jar时出错; error in opening zip file

java: 读取D:\mavenRepository\.m2\repository\org\springframework\spring-tx\5.2.12.RELEASE\spring-tx-5.2.12.RELEASE.jar时出错; error in opening zip file

在创建项目之后,笔者通过IDEA开发工具右侧的Maven生命周期和依赖管理发现存在很多重复引用jar包的问题,于是使用标签将重复的依赖去除,只是注意要把一些模块内要用到但是却重复引用的依赖单独放到标签下,以免造成项目中依赖的jar包缺失导致性项目启动失败。实际上项目中存在重复引用依赖jar包只还要不存在jar包冲突的情况是不会造成项目启动失败错误的,只是会造成项目的依赖管理变得臃肿。
SpringBoot项目拥抱Mybatis-Plus持久层框架实践_第6张图片
下面的pom.xml文件是我经过替换spring-boot 2.2.7.RELEASE版本,并排除大部分重复依赖且项目可以成功启动后的pom.xml文件内容

pom.xml

2.2 完成项目启动配置

在项目的src/main/resources目录下的application.properties环境配置文件中添加spring、tomcat服务器和mybatis-pus的配置项,spring-boot项目在启动时会去根据自动配置类在初始化相关beans时会去加载这些参数

application.propertis

# 应用名称
spring.application.name=mybatis-plus
# 应用服务 WEB 访问端口
server.port=8080
# 应用上下文路径
server.servlet.context-path=/mybatis-plus
# 激活dev环境
spring.profiles.active=dev
# 设置时区,防止json序列化对象日期参数时比真是日期早了8小时
spring.jackson.time-zone=GMT+8

# mybatis-plus配置
mybatis-plus.mapper-locations=classpath:com/example/mybatisplus/mapper/*Mapper.xml
mybatis-plus.configuration.map-underscore-to-camel-case=true
# 这个配置可以查看sql执行的详细日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

另外新建application-dev.properties配置文件,对应开发环境的配置参数,在这个属性文件中我们配置开发环境的数据源参数,将来有测试环境和生产环境时还可以继续添加application-test.properties文件和application-prod.properties文件配置测试环境和生产环境对应的环境配置参数

application-dev.properties

# database driver
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# datasource name
spring.datasource.name=hikariDataSource
# connection URL
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
# login user
spring.datasource.username=root
# login password
spring.datasource.password=<你的数据库root用户连接密码>

注意上面的数据库连接URL对应的spring.datasource.url配置项中必须加上characterEncoding=UTF-8和serverTimezone=Asia/Shanghai两个参数,前者是为了防止数据库中文乱码,后者是为了数据库中保存的日期字段时间准确,默认的时间会比我们中国北京时区早8个小时。

项目的启动类上加上@MapperScan注解,basePackages属性中填写数据库访问接口Mapper类所在的包名

package com.example.mybatisplus;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan(basePackages = {"com.example.mybatisplus.mapper"})
publicclass MybatisPlusApplication {

    public static void main(String[] args) {
        SpringApplication.run(MybatisPlusApplication.class, args);
    }

}
package com.example.mybatisplus.configuration;

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.optimize.JsqlParserCountOptimize;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
publicclass MybatisPlusConfig {

     @Bean
     public PaginationInterceptor paginationInterceptor() {
         PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
         paginationInterceptor.setOverflow(true);
         paginationInterceptor.setDialectClazz("com.baomidou.mybatisplus.extension.plugins.pagination.dialects.MySqlDialect");
         paginationInterceptor.setSqlParser(new JsqlParserCountOptimize());
         return paginationInterceptor;
     }
}

这里的配置与mybatis-plus官网的配置稍有不同,官网用的是3.4.2版本,而我用的3.1.0版本。主要是new了一个分页拦截器类PaginationInterceptor,然后设置它的数据库方言类和sqlParser属性。如果用的不是Mysql数据库,读者可使用com.baomidou.mybatisplus.extension.plugins.pagination.dialects包下面与自己数据库对应的方言类
SpringBoot项目拥抱Mybatis-Plus持久层框架实践_第7张图片

2.3 浅析Mybatis-Plus自动配置类源码

解读mybatis-plus自动配置类的源码的目的是为了帮助我们跟更好的理解Mybatis-Plus的工作原理和指导我们如何正确的配置mybatis-plus的属性参数

mybatis-plus的自动配置类为MybatisPlusAutoConfiguration类,该类位于mybatis-plus-boot-starter-3.1.0.jar包中的com.baomidou.mybatisplus.autoconfigure包下
SpringBoot项目拥抱Mybatis-Plus持久层框架实践_第8张图片
进入MybatisPlusAutoConfiguration类中我们可以看到下面的两个@bean注解标注的方法sqlSessionFactory(DataSource dataSource)sqlSessionTemplate(SqlSessionFactory sqlSessionFactory)帮我们配置了Spring集成Mybatis时必须要用到的连个bean:SqlSessionTemplateSqlSessionTemplate。这两个bean都会在开发人员没有自定义配置这两个类的bean的条件下自动初始化并注入到Spring IOC容器中
SpringBoot项目拥抱Mybatis-Plus持久层框架实践_第9张图片
sqlSessionTemplate
我们可以看到MybatisPlusAutoConfiguration类上加上了@EnableConfigurationProperties({MybatisPlusProperties.class})@AutoConfigureAfter({DataSourceAutoConfiguration.class})两个注解,前者表示项目中具有·MybatisPlusProperties·这个属性配置类时初始化·MybatisPlusAutoConfiguration·配置类及其下面配置的bean并添加到Spring IOC容器中;后者表示在完成数据源自动配置类DataSourceAutoConfiguration初始化之后进行。

进入MybatisPlusProperties类中我们发现它要求所有与mybatis-plus相关的属性参数都以mybatis-plus为前缀
SpringBoot项目拥抱Mybatis-Plus持久层框架实践_第10张图片
这个类会去解析环境配置文件中以mybatis-plus开头的键值对并在初始化时填充到它的属性值中

更多源码读者可自己通过IDEA打开项目后查看。

3 使用Mybatis-Plus完成数据库CRUD功能

这里我为了减少文章篇幅,仅演示单表的CRUD操作,主要涉及单条和多条数据的添加、修改、查询和分页查询功能的实现,使用Mybatis-Plus实现同时查询多张表的连表查询与在Mybatis中通过自定义mapper.xml实现一致。网上的案例也比较多,我在本文就不再延伸演示了

3.1 建表

这里我在mysql的test数据库中新建了一张stock_info表,代表商品信息表,也是为了方便学习分布式电商项目的需要。建表sql脚本如下:

DROPTABLEIFEXISTS`stock_info`;
CREATETABLE`stock_info` (
                              `id`bigint(20) NOTNULL AUTO_INCREMENT COMMENT'主键',
                              `good_code`varchar(30) NOTNULLCOMMENT'商品编码',
                              `good_name`varchar(100) DEFAULTNULLCOMMENT'商品名称',
                              `count`int(11) DEFAULT'0'COMMENT'商品数量',
                              `created_date` datetime NOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',
                              `created_by`varchar(30) NOTNULLDEFAULT'system'COMMENT'创建人',
                              `last_updated_date` datetime NOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'最后更新时间',
                              `last_updated_by`varchar(30) NOTNULLDEFAULT'system'COMMENT'最后更新人',
                              `unit_price`int(11) DEFAULT'0'COMMENT'单价,单位分',
                              PRIMARY KEY (`id`),
                              UNIQUEKEY`uk_good_code` (`good_code`)
) ENGINE=InnoDB AUTO_INCREMENT=22DEFAULTCHARSET=utf8mb4;

读者也可以使用可视化的数据库客户端工具如Navicat实现新建一张表,然后把建表sql脚本dump下来保存到项目中存储脚本的文件夹下。

编码部分

项目的编码我们按照分层模式进行,分为控制器层(controller包下的XXController类)、服务层(service包下的XXService类)和数据库访问层(mapper包下的XXMapper类)

3.2 新建与表对应的实体类

com.example.mybatisplus.pojo包下新建一个基础类BaseEntity,主要包含一张表中通用的创建人、创建时间、最后修改人和最后修改时间等字段

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

@Data
publicclass BaseEntity implements Serializable {

    /**
     * 创建人
     */
    @TableField(value = "created_by", fill = FieldFill.INSERT)
    private String createdBy;

    /**
     * 创建日期(带时间)
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(value = "created_date", fill = FieldFill.INSERT)
    private Date createdDate;

    /**
     * 修改人用户ID
     */
    @TableField(value = "last_updated_by", fill = FieldFill.INSERT_UPDATE)
    private String lastUpdatedBy;

    /**
     * 修改日期(带时间)
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(value = "last_updated_date", fill = FieldFill.INSERT_UPDATE)
    private Date lastUpdatedDate;

}

日期字段加上日期格式化注解@JsonFormat注解,在pattern属性中指定日期格式

同样在com.example.mybatisplus.pojo下新建与stock_info表对应的实体类StockInfo类并继承上面的BaseEntity类

package com.example.mybatisplus.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("stock_info")
publicclass StockInfo extends BaseEntity {
    /**
     * 主键ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;
    /**
     * 商品代码
     */
    @TableField(value = "good_code")
    private String goodCode;

    /**
     * 商品名称
     */
    @TableField(value = "good_name")
    private String goodName;

    /**
     * 库存数量
     */
    @TableField(value = "count")
    private Integer count;

    /**
     * 商品单价,单位:分
     */
    @TableField(value = "unit_price")
    private Long unitPrice;

}

关于@TableName@TableId@TableField等注解的用法及详细介绍,读者可以通过阅读官方文档注解部分掌握,我在这里就不作解读了。本项目源码已提交到gitee个人代码仓库,其他接口出参通用类如ResponseVo、form表单查询参数封装类StockParam类和BaseParam类可通过文末给出的项目gitee地址查看。

3.3 数据库访问层编码

新建StockMapper接口并继承BaseMapper接口类,并自定义两个方法

package com.example.mybatisplus.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.mybatisplus.pojo.StockInfo;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

@Repository
publicinterface StockMapper extends BaseMapper<StockInfo> {
    /**
     * 根据商品码查找库存数量
     * @param goodCode
     * @return
     */
    Integer findCountByGoodCode(String goodCode);

    /**
     * 更新商品库存数量
     * @param id
     * @param count
     * @return
     */
    Integer updateStockById(@Param("id") Long id, @Param("count") Integer count);
}

我们按住Ctrl键,点击BaseMapper进入BaseMapper类可以看到该类定义了单表的常用CRUD方法,为开发人员节省了自定义CRUD方法的时间

package com.baomidou.mybatisplus.core.mapper;

import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Param;
// BaseMapper中的泛型参数T为表对应的实体类
publicinterface BaseMapper<T> {
    // 添加条记录方法
    int insert(T entity);
    // 通过ID删除方法
    int deleteById(Serializable id);
    // 通过列值匹配删除方法
    int deleteByMap(@Param("cm") Map<String, Object> columnMap);
    // 通过查询条件删除
    int delete(@Param("ew") Wrapper<T> wrapper);
    // 根据ID集合批量删除
    int deleteBatchIds(@Param("coll") Collection<? extends Serializable> idList);
    // 根据ID更新记录
    int updateById(@Param("et") T entity);
    // 根据查询条件更新
    int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);
    // 根据ID查询单条记录
    T selectById(Serializable id);
    // 根据ID集合批量查询
    List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);
    // 根据列值匹配查询,返回多条记录
    List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);
    // 根据查询条件查询单条记录
    T selectOne(@Param("ew") Wrapper<T> queryWrapper);
    // 根据条件查询满足条件的数量
    Integer selectCount(@Param("ew") Wrapper<T> queryWrapper);
    //根据查询条件查询多条记录,返回实体对象集合
    List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);
    //根据查询条件查询多条记录,返回Map集合  
    List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);
    // 根据查询条件查询多条记录,返回对象集合
    List<Object> selectObjs(@Param("ew") Wrapper<T> queryWrapper);
    // 带查询参数的分页查询,返回带实体对象集合的分页对象
    IPage<T> selectPage(IPage<T> page, @Param("ew") Wrapper<T> queryWrapper);
    // 带查询参数的分页查询,返回带Map集合的分页对象
    IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param("ew") Wrapper<T> queryWrapper);
}

src/main/resources目录下我们逐层新建至com/example/muybatisplus/mapper文件夹,然后在mapper文件夹下新建StockMapper.xml完成StockMapper.java中自定义方法的实现。这一步与以往的Mybatis作为持久层框架手写Mapper.xml一样

StockMapper.xml


DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mybatisplus.mapper.StockMapper">
    <select id="findCountByGoodCode" parameterType="java.lang.String" resultType="java.lang.Integer">
        select `count` from stock_info
        where good_code = #{goodCode, jdbcType=VARCHAR}
    select>

    <update id="updateStockById">
        update stock_info set `count` = #{count, jdbcType=INTEGER}
        where id = #{id, jdbcType=BIGINT}
    update>
mapper>

3.4 Service层编码

新建StockService接口类并集成IService接口类

package com.example.mybatisplus.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.mybatisplus.params.StockParam;
import com.example.mybatisplus.pojo.ResponseVo;
import com.example.mybatisplus.pojo.StockInfo;

import java.util.List;

publicinterface StockService extends IService<StockInfo> {
    /**
     * 单个添加保存
     *
     * @param stockInfo
     * @return
     */
    ResponseVo saveOne(StockInfo stockInfo);

    /**
     * 批量添加保存
     *
     * @param stockInfoList
     * @return
     */
    ResponseVo saveBatch(List<StockInfo> stockInfoList);

    /**
     * 修改单个
     *
     * @param stockInfo
     * @return
     */
    ResponseVo updateOne(StockInfo stockInfo);

    /**
     * 批量修改
     * @param stockInfoList
     * @return
     */
    ResponseVo updateBatch(List<StockInfo> stockInfoList);

    /**
     * 带个条件分页查询
     * @param stockParam
     * @param pageNo
     * @param pageSize
     * @return
     */
    ResponseVo findPageByCondition(StockParam stockParam, int pageNo, int pageSize);
    /**
     * 自定义查找商品库存
     * @param goodCode
     * @return
     */
    ResponseVo findCountByGoodCode(String goodCode);

    /**
     * 自定义更商品库存
     * @param id
     * @param count
     * @return
     */
    ResponseVo updateStockCountById(Long id, Integer count);
}

在StockService接口类中,我定义了一些访问数据库的CRUD抽象方法,方法返回类型统一为ResponseVo

然后新建StockServiceImpl类继承ServiceImpl类并实现StockService接口类

package com.example.mybatisplus.service.impl;

import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;

import com.example.mybatisplus.mapper.StockMapper;
import com.example.mybatisplus.params.StockParam;
import com.example.mybatisplus.pojo.ResponseVo;
import com.example.mybatisplus.pojo.StockInfo;
import com.example.mybatisplus.service.StockService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@Slf4j
publicclass StockServiceImpl extends ServiceImpl<StockMapper, StockInfo> implements StockService {

    privatestaticfinal String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";

    @Override
    public ResponseVo saveOne(StockInfo stockInfo) {
        log.info("invoke StockServiceImpl#saveOne method start --------");
        log.info("stockInfo={}", JSON.toJSON(stockInfo));
        setCreatedByAndCreatedByDate(stockInfo);
        setLastUpdatedByAndLastUpdatedDate(stockInfo);
        boolean result = super.save(stockInfo);
        ResponseVo responseVo;
        if (result) {
            responseVo = ResponseVo.success(result);
        } else {
            responseVo = ResponseVo.error("保存失败");
        }
        return responseVo;
    }

    @Override
    public ResponseVo saveBatch(List<StockInfo> stockInfoList) {
        log.info("invoke StockServiceImpl#saveBatch method start --------");
        log.info("stockInfoList={}", JSON.toJSON(stockInfoList));
        for (StockInfo stockInfo : stockInfoList) {
            setCreatedByAndCreatedByDate(stockInfo);
            setLastUpdatedByAndLastUpdatedDate(stockInfo);
        }
        boolean result = super.saveBatch(stockInfoList);
        ResponseVo responseVo;
        if (result) {
            responseVo = ResponseVo.success(result);
        } else {
            responseVo = ResponseVo.error("批量保存失败");
        }
        return responseVo;
    }

    @Override
    public ResponseVo updateOne(StockInfo stockInfo) {
        log.info("invoke StockServiceImpl#updateOne method start --------");
        log.info("stockInfo={}", JSON.toJSON(stockInfo));
        setLastUpdatedByAndLastUpdatedDate(stockInfo);
        boolean result = super.updateById(stockInfo);
        ResponseVo responseVo;
        if (result) {
            responseVo = ResponseVo.success(result);
        } else {
            responseVo = ResponseVo.error("更新失败");
        }
        return responseVo;
    }

    @Override
    public ResponseVo updateBatch(List<StockInfo> stockInfoList) {
        log.info("invoke StockServiceImpl#updateBatch method start --------");
        log.info("stockInfoList={}", JSON.toJSON(stockInfoList));
        for (StockInfo stockInfo : stockInfoList) {
            setLastUpdatedByAndLastUpdatedDate(stockInfo);
        }
        boolean result = super.updateBatchById(stockInfoList);
        ResponseVo responseVo;
        if (result) {
            responseVo = ResponseVo.success(result);
        } else {
            responseVo = ResponseVo.error("批量更新失败");
        }
        return responseVo;
    }

    @Override
    public ResponseVo findPageByCondition(StockParam stockParam, int pageNo, int pageSize) {
        log.info("invoke StockServiceImpl#findPageByCondition method start --------");
        log.info("pageNo={}, pageSize={}", pageNo, pageSize);
        if (pageNo <= 0) {
           pageNo = 1;
        }
        if (pageSize <= 10) {
            pageSize = 10;
        }
        if (pageSize > 500) {
            pageSize = 500;
        }
        IPage<StockInfo> pageParam = new Page<>(pageNo, pageSize);
        QueryWrapper<StockInfo> queryWrapper = getQueryWrapperByParam(stockParam);
        IPage<StockInfo> pageData = super.page(pageParam, queryWrapper);
        ResponseVo responseVo = ResponseVo.success(pageData);
        return responseVo;
    }

    @Override
    public ResponseVo findCountByGoodCode(String goodCode) {
        log.info("invoke StockServiceImpl#findCountByGoodCode method start --------");
        log.info("goodCode={}", goodCode);
        ResponseVo responseVo;
        if (StringUtils.isEmpty(goodCode)) {
            responseVo = ResponseVo.error(HttpStatus.BAD_REQUEST.value(), "goodCode cannot be null");
            return responseVo;
        }
        Integer count = this.baseMapper.findCountByGoodCode(goodCode);
        if (count == 1) {
            responseVo = ResponseVo.success(count);
        } else {
            responseVo = ResponseVo.error("更新库存失败");
        }
        return responseVo;
    }

    @Override
    public ResponseVo updateStockCountById(Long id, Integer count) {
        log.info("invoke StockServiceImpl#updateStockCountById method start --------");
        ResponseVo responseVo;
        if (id == null || id < 1) {
            responseVo = ResponseVo.error(HttpStatus.BAD_REQUEST.value(), "invalid request param of id");
            return responseVo;
        }
        if (count == null || count < 1) {
            responseVo = ResponseVo.error(HttpStatus.BAD_REQUEST.value(), "invalid request param of count");
            return responseVo;
        }
        Integer resultCount = this.baseMapper.updateStockById(id, count);
        if (resultCount == 1) {
            responseVo = ResponseVo.success(resultCount);
        } else {
            responseVo = ResponseVo.error("更新商品库存失败");
        }
        return responseVo;
    }

    /**
     * 设置创建人与创建时间
     *
     * @param stockInfo
     */
    private void setCreatedByAndCreatedByDate(StockInfo stockInfo) {
        if (StringUtils.isEmpty(stockInfo.getCreatedBy())){
            stockInfo.setCreatedBy("system");
        }
        if (stockInfo.getCreatedDate() == null) {
            stockInfo.setCreatedDate(DateUtil.date(System.currentTimeMillis()));
        }
    }

    /**
     * 设置最后修改人与最后修改时间
     *
     * @param stockInfo
     */
    private void setLastUpdatedByAndLastUpdatedDate(StockInfo stockInfo) {
        if (StringUtils.isEmpty(stockInfo.getLastUpdatedBy())){
            stockInfo.setLastUpdatedBy("system");
        }
        if (stockInfo.getLastUpdatedDate() == null) {
            stockInfo.setLastUpdatedDate(DateUtil.date(System.currentTimeMillis()));
        }
    }

    /**
     * 处理动态查询
     *
     * @param stockParam
     * @return queryWrapper
     */
    private QueryWrapper<StockInfo> getQueryWrapperByParam(StockParam stockParam) {
        QueryWrapper<StockInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.select("id", "good_code", "good_name", "unit_price", "count",
                "created_date", "created_by", "last_updated_by", "last_updated_date");
        // 注意加了唯一索引的列使用like查询会导致查不出结果
        queryWrapper.eq(!StringUtils.isEmpty(stockParam.getGoodCode()), "good_code", stockParam.getGoodCode());

        queryWrapper.likeRight(!StringUtils.isEmpty(stockParam.getGoodName()), "good_name", stockParam.getGoodName());

        queryWrapper.eq(stockParam.getCount() != null, "count", stockParam.getCount());

        queryWrapper.ge(stockParam.getQueryMinUnitPrice() != null,"unit_price", stockParam.getQueryMinUnitPrice());

        queryWrapper.le(stockParam.getQueryMaxUnitPrice() != null,"unit_price", stockParam.getQueryMaxUnitPrice());

        queryWrapper.eq(StringUtils.isNotBlank(stockParam.getCreatedBy()), "created_by", stockParam.getCreatedBy());

        if (!StringUtils.isEmpty(stockParam.getQueryMinCreatedDate())) {
            DateTime queryMinCreatedDate = DateUtil.parse(stockParam.getQueryMinCreatedDate(), DATE_TIME_FORMAT);
            queryWrapper.ge("created_date", queryMinCreatedDate);
        }
        if (!StringUtils.isEmpty(stockParam.getQueryMaxCreatedDate())) {
            DateTime queryMaxCreatedDate = DateUtil.parse(stockParam.getQueryMaxCreatedDate(), DATE_TIME_FORMAT);
            queryWrapper.le("created_date", queryMaxCreatedDate);
        }
        if (!StringUtils.isEmpty(stockParam.getLastUpdatedBy())) {
            queryWrapper.eq(StringUtils.isNotBlank(stockParam.getLastUpdatedBy()), "last_updated_by", stockParam.getLastUpdatedBy());
        }
        if (!StringUtils.isEmpty(stockParam.getQueryMinUpdateDate())) {
            DateTime queryMinUpdateDate = DateUtil.parse(stockParam.getQueryMinUpdateDate(), DATE_TIME_FORMAT);
            queryWrapper.ge("last_updated_date", queryMinUpdateDate);
        }
        if (!StringUtils.isEmpty(stockParam.getQueryMaxUpdateDate())) {
            DateTime queryMaxUpdateDate = DateUtil.parse(stockParam.getQueryMaxUpdateDate(), DATE_TIME_FORMAT);
            queryWrapper.le("last_updated_date", queryMaxUpdateDate);
        }
        queryWrapper.orderByAsc("id");
        return queryWrapper;
    }
}

进入ServiceImpl类中我们可以看到,它实现了IService接口类,实现了大部分BaseMapper中的抽象方法
SpringBoot项目拥抱Mybatis-Plus持久层框架实践_第11张图片
ServiceImpl类中的第一泛型参数为继承自BaseMapper的自定义Mapper类,第二个泛型参数则是与不表对应的实体类,对应本演示项目中的StockMapperStockInfo两个类。这样自定义Mapper类无需在自定义ServiceImpl中注入,而是通过this.baseMapper拿到数据库访问代理对象。

3.5 Controller层编码

这一层编码就非常简单了,直接调用注入的StockService服务类完成操作即可, 只是需要在类和方法上加上一些spring-mvc的注解

package com.example.mybatisplus.controller;

import com.example.mybatisplus.params.StockParam;
import com.example.mybatisplus.pojo.ResponseVo;
import com.example.mybatisplus.pojo.StockInfo;
import com.example.mybatisplus.service.StockService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.List;

@RestController
@RequestMapping("/stock")
publicclass StockController {

    @Resource
    private StockService stockService;
    /**
     * 保存单个
     *
     * @param stockInfo
     * @return
     */
    @PostMapping("/saveOne")
    public ResponseVo saveOne(@RequestBody StockInfo stockInfo) {
        return stockService.saveOne(stockInfo);
    }

    /**
     * 批量保存
     * @param stockInfoList
     * @return
     */
    @PostMapping("/saveBatch")
    public ResponseVo saveBatch(@RequestBody List<StockInfo> stockInfoList) {
        return stockService.saveBatch(stockInfoList);
    }

    /**
     * 更新单个
     * @param stockInfo
     * @return
     */
    @PostMapping("/updateOne")
    public ResponseVo updateOne(@RequestBody StockInfo stockInfo) {
        return stockService.updateOne(stockInfo);
    }

    /**
     * 批量更新
     *
     * @param stockInfoList
     * @return
     */
    @PostMapping("/updateBatch")
    public ResponseVo updateBatch(@RequestBody List<StockInfo> stockInfoList) {
        return stockService.updateBatch(stockInfoList);
    }

    /**
     * 分页查找
     *
     * @param pageNo 当前页
     * @param pageSize 每页记录数
     * @param stockParam 库存查询参数封装对象
     * @return
     */
    @PostMapping("/page/list/{pageNo}/{pageSize}")
    public ResponseVo pageListByCondition(@PathVariable("pageNo") int pageNo, @PathVariable("pageSize") int pageSize, @RequestBody StockParam stockParam) {

        return stockService.findPageByCondition(stockParam, pageNo, pageSize);

    }

    /**
     * 根据商品码查找商品库存数量
     *
     * @param goodCount
     * @return
     */
    @GetMapping("/goodCount")
    public ResponseVo findGoodCount(@RequestParam("goodCount") String goodCount) {
        return stockService.findCountByGoodCode(goodCount);
    }

    /**
     * 修改商品库存数量
     *
     * @param id
     * @param count
     * @return
     */
    @PostMapping("/update/count")
    public ResponseVo updateStockCountById(Long id, Integer count) {
        return stockService.updateStockCountById(id, count);
    }

}

4 效果体验

4.1 启动项目

编码完成,我们在本地启动Mysql服务的前提下开始启动项目,控制台中出现如下日志信息表示启动成功:

021-12-04 15:55:19.483  INFO 16832 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2021-12-04 15:55:19.494  INFO 16832 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2021-12-04 15:55:19.494  INFO 16832 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.34]
2021-12-04 15:55:19.723  INFO 16832 --- [           main] o.a.c.c.C.[.[localhost].[/mybatis-plus]  : Initializing Spring embedded WebApplicationContext
2021-12-04 15:55:19.723  INFO 16832 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2050 ms
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
 _ _   |_  _ _|_. ___ _ |    _ 
| | |\/|_)(_| | |_\  |_)||_|_\ 
     /               |         
                        3.1.0 
Registered plugin: 'AbstractSqlParserHandler(sqlParserList=null, sqlParserFilter=null)'
2021-12-04 15:55:20.020  INFO 16832 --- [           main] com.zaxxer.hikari.HikariDataSource       : hikariDataSource - Starting...
2021-12-04 15:55:20.195  INFO 16832 --- [           main] com.zaxxer.hikari.HikariDataSource       : hikariDataSource - Start completed.
Parsed mapper file: 'file [D:\Mybatis\mybatisplus\target\classes\com\example\mybatisplus\mapper\StockMapper.xml]'
2021-12-04 15:55:20.766  INFO 16832 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2021-12-04 15:55:21.116  INFO 16832 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path '/mybatis-plus'
2021-12-04 15:55:21.120  INFO 16832 --- [           main] c.e.mybatisplus.MybatisPlusApplication   : Started MybatisPlusApplication in 4.34 seconds (JVM running for 9.892)

4.2 测试接口

项目启动成功后,我们就可以打开postman对接口进行测试了

4.2.1 测试添加单条数据

SpringBoot项目拥抱Mybatis-Plus持久层框架实践_第12张图片
响应结果返回转态码200表示接口调用成功, 同时我们可以看到后台控制台打印了如下sql执行语句

==>  Preparing: INSERT INTO stock_info ( good_code, good_name, count, unit_price, created_by, created_date, last_updated_by, last_updated_date ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ? ) 
==> Parameters: GalaxyNote20(String), 三星Noto20(String), 500(Integer), 589900(Long), system(String), 2021-12-04 16:22:31.653(Timestamp), system(String), 2021-12-04 16:22:31.693(Timestamp)
<==    Updates: 1

4.2.2 测试批量添加接口

我们调用批量插入接口一次性插入5条数据,接口返回状态码200表示添加数据成功
SpringBoot项目拥抱Mybatis-Plus持久层框架实践_第13张图片
然后我们通过客户端Navicat查询数据库也可以看到通过调用单个添加和批量添加接口添加的数据入库了,一些数据是我之前调用添加接口写入到数据库中的。
SpringBoot项目拥抱Mybatis-Plus持久层框架实践_第14张图片

4.2.3 测试分页查询接口

最后我们测试下分页查询效果:

1)首先不带查询参数进行分页查询,此时查的是表中全部数据
SpringBoot项目拥抱Mybatis-Plus持久层框架实践_第15张图片
接口响应信息里显示共查出了27条数据,每页显示10条,共3页

2)最后带上查询参数进行分页查询

SpringBoot项目拥抱Mybatis-Plus持久层框架实践_第16张图片
截图时我折叠了records字段中的值,records字段中的数据如下所示:

[
            {
                "createdBy": "system",
                "createdDate": "2021-12-04 16:36:50",
                "lastUpdatedBy": "system",
                "lastUpdatedDate": "2021-12-04 16:36:50",
                "id": 23,
                "goodCode": "GalaxyNote3",
                "goodName": "三星Note3",
                "count": 500,
                "unitPrice": 280000
            },
            {
                "createdBy": "system",
                "createdDate": "2021-12-04 16:36:50",
                "lastUpdatedBy": "system",
                "lastUpdatedDate": "2021-12-04 16:36:50",
                "id": 24,
                "goodCode": "GalaxyNote4",
                "goodName": "三星Note4",
                "count": 500,
                "unitPrice": 300000
            },
            {
                "createdBy": "system",
                "createdDate": "2021-12-04 16:36:50",
                "lastUpdatedBy": "system",
                "lastUpdatedDate": "2021-12-04 16:36:50",
                "id": 25,
                "goodCode": "GalaxyNote5",
                "goodName": "三星Note4",
                "count": 500,
                "unitPrice": 330000
            },
            {
                "createdBy": "system",
                "createdDate": "2021-12-04 16:36:50",
                "lastUpdatedBy": "system",
                "lastUpdatedDate": "2021-12-04 16:36:50",
                "id": 26,
                "goodCode": "GalaxyNote6",
                "goodName": "三星Note6",
                "count": 500,
                "unitPrice": 350000
            },
            {
                "createdBy": "system",
                "createdDate": "2021-12-04 16:36:50",
                "lastUpdatedBy": "system",
                "lastUpdatedDate": "2021-12-04 16:36:50",
                "id": 27,
                "goodCode": "GalaxyNote7",
                "goodName": "三星Note7",
                "count": 500,
                "unitPrice": 380000
            }
        ]

限于文章篇幅,其他接口的测试效果就不一一在此列举了。感兴趣的读者可以把这一项目克隆下来并测试使用mybatis-plus实现更多操作数据库功能。

5 参考链接

【1】mybatis-plus快速开始

【2】mybatis-plus安装

【3】spring-boot集成mybatis-plus

【4】mybatis-plus实现数据库CRUD

【5】mybatis-plus分页插件

本文项目gitee地址:mybatisplus

本文首发个人微信公众号,喜欢我的文章的读者朋友希望能扫码下面的二维码加个微信关注,谢谢!
SpringBoot项目拥抱Mybatis-Plus持久层框架实践_第17张图片

你可能感兴趣的:(springboot项目实战,spring,boot,java,mybatis-plus)