美文网首页
SpringBoot入门—MybatisPlus(二)

SpringBoot入门—MybatisPlus(二)

作者: 遇见编程 | 来源:发表于2024-07-04 17:28 被阅读0次

    一、高级功能

    1.代码生成

    import com.baomidou.mybatisplus.annotation.DbType;
    import com.baomidou.mybatisplus.annotation.IdType;
    import com.baomidou.mybatisplus.generator.AutoGenerator;
    import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
    import com.baomidou.mybatisplus.generator.config.GlobalConfig;
    import com.baomidou.mybatisplus.generator.config.PackageConfig;
    import com.baomidou.mybatisplus.generator.config.StrategyConfig;
    import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
    
    public class GeneratorCode {
        private static String author ="hjy";//作者名称
        private static String outputDir ="F:\\JavaProject\\authority-system\\src\\main\\java\\";//生成的位置
        private static String driver ="com.mysql.cj.jdbc.Driver";//驱动,注意版本
        //连接路径,注意修改数据库名称
        private static String url ="jdbc:mysql://localhost:3306/db_authority_system?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC";
        private static String username ="root";//数据库用户名
        private static String password ="123456";//数据库密码
        private static String tablePrefix ="sys_";//数据库表的前缀,如t_user
        private static String [] tables = {"sys_user","sys_role","sys_permission","sys_department"};   //生成的表
        private static String parentPackage = "com.hjy";//顶级包结构
        private static String dao = "dao";//数据访问层包名称
        private static String service = "service";//业务逻辑层包名称
        private static String entity = "entity";//实体层包名称
        private static String controller = "controller";//控制器层包名称
        private static String mapperxml = "mapper";//mapper映射文件包名称
    
        public static void main(String[] args) {
            //1. 全局配置
            GlobalConfig config = new GlobalConfig();
            config.setAuthor(author) // 作者
                    .setOutputDir(outputDir) // 生成路径
                    .setFileOverride(true)  // 文件覆盖
                    .setIdType(IdType.AUTO) // 主键策略
                    .setServiceName("%sService")  // 设置生成的service接口的名字的首字母是否为I,加%s则不生成I
                    .setBaseResultMap(true)    //映射文件中是否生成ResultMap配置
                    .setBaseColumnList(true);  //生成通用sql字段
    
            //2. 数据源配置
            DataSourceConfig dsConfig  = new DataSourceConfig();
            dsConfig.setDbType(DbType.MYSQL)  // 设置数据库类型
                    .setDriverName(driver) //设置驱动
                    .setUrl(url)         //设置连接路径
                    .setUsername(username) //设置用户名
                    .setPassword(password);    //设置密码
    
            //3. 策略配置
            StrategyConfig stConfig = new StrategyConfig();
            stConfig.setCapitalMode(true) //全局大写命名
                    .setNaming(NamingStrategy.underline_to_camel) // 数据库表映射到实体的命名策略
                    .setTablePrefix(tablePrefix) //表前缀
                    .setInclude(tables)  // 生成的表
                    .setEntityLombokModel(true);//支持Lombok
    
            //4. 包名策略配置
            PackageConfig pkConfig = new PackageConfig();
            pkConfig.setParent(parentPackage)//顶级包结构
                    .setMapper(dao)    //数据访问层
                    .setService(service)   //业务逻辑层
                    .setController(controller) //控制器
                    .setEntity(entity) //实体类
                    .setXml(mapperxml);    //mapper映射文件
    
            //5. 整合配置
            AutoGenerator ag = new AutoGenerator();
            ag.setGlobalConfig(config)
                    .setDataSource(dsConfig)
                    .setStrategy(stConfig)
                    .setPackageInfo(pkConfig);
            //6. 执行
            ag.execute();
        }
    }
    
    

    在使用MybatisPlus以后,基础的Mapper、Service、PO代码相对固定,重复编写也比较麻烦。因此MybatisPlus官方提供了代码生成器根据数据库表结构生成PO、Mapper、Service等相关代码。只不过代码生成器同样要编码使用,也很麻烦。
    这里推荐大家使用一款MybatisPlus的插件,它可以基于图形化界面完成MybatisPlus的代码生成,非常简单。
    安装号插件,重启Idea即可使用。

    选择Config Database

    选择Code Generator

    2.静态工具

    有的时候Service之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致,也可以帮助我们实现CRUD功能:

    简单使用:

    @Test
    void testDbGet() {
        User user = Db.getById(1L, User.class);
        System.out.println(user);
    }
    
    @Test
    void testDbList() {
        // 利用Db实现复杂条件查询
        List<User> list = Db.lambdaQuery(User.class)
                .like(User::getUsername, "o")
                .ge(User::getBalance, 1000)
                .list();
        list.forEach(System.out::println);
    }
    
    @Test
    void testDbUpdate() {
        Db.lambdaUpdate(User.class)
                .set(User::getBalance, 2000)
                .eq(User::getUsername, "Rose");
    }
    

    需求:改造根据id用户查询的接口,查询用户的同时返回用户收货地址列表
    首先,我们要添加一个收货地址的VO对象:

    package com.itheima.mp.domain.vo;
    
    import io.swagger.annotations.ApiModel;
    import io.swagger.annotations.ApiModelProperty;
    import lombok.Data;
    
    @Data
    @ApiModel(description = "收货地址VO")
    public class AddressVO{
    
        @ApiModelProperty("id")
        private Long id;
    
        @ApiModelProperty("用户ID")
        private Long userId;
    
        @ApiModelProperty("省")
        private String province;
    
        @ApiModelProperty("市")
        private String city;
    
        @ApiModelProperty("县/区")
        private String town;
    
        @ApiModelProperty("手机")
        private String mobile;
    
        @ApiModelProperty("详细地址")
        private String street;
    
        @ApiModelProperty("联系人")
        private String contact;
    
        @ApiModelProperty("是否是默认 1默认 0否")
        private Boolean isDefault;
    
        @ApiModelProperty("备注")
        private String notes;
    }
    

    然后,改造原来的UserVO,添加一个地址属性:

    接下来,修改UserController中根据id查询用户的业务接口:

    @GetMapping("/{id}")
    @ApiOperation("根据id查询用户")
    public UserVO queryUserById(@PathVariable("id") Long userId){
        // 基于自定义service方法查询
        return userService.queryUserAndAddressById(userId);
    }
    

    由于查询业务复杂,所以要在service层来实现。首先在IUserService中定义方法:

    package com.itheima.mp.service;
    
    import com.baomidou.mybatisplus.extension.service.IService;
    import com.itheima.mp.domain.po.User;
    import com.itheima.mp.domain.vo.UserVO;
    
    public interface IUserService extends IService<User> {
        void deduct(Long id, Integer money);
    
        UserVO queryUserAndAddressById(Long userId);
    }
    

    然后,在UserServiceImpl中实现该方法:

    @Override
    public UserVO queryUserAndAddressById(Long userId) {
        // 1.查询用户
        User user = getById(userId);
        if (user == null) {
            return null;
        }
        // 2.查询收货地址
        List<Address> addresses = Db.lambdaQuery(Address.class)
                .eq(Address::getUserId, userId)
                .list();
        // 3.处理vo
        UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
        userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
        return userVO;
    }
    

    在查询地址时,我们采用了Db的静态方法,因此避免了注入AddressService,减少了循环依赖的风险。

    3.逻辑删除

    对于一些比较重要的数据,我们往往会采用逻辑删除的方案,即:

    • 在表中添加一个字段标记数据是否被删除
    • 当删除数据时把标记置为true
    • 查询时过滤掉标记为true的数据
      一旦采用了逻辑删除,所有的查询和删除逻辑都要跟着变化,非常麻烦。

    注意,只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。

    使用:

    mybatis-plus:
      global-config:
        db-config:
          logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
          logic-delete-value: 1 # 逻辑已删除值(默认为 1)
          logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
    

    注意:
    逻辑删除本身也有自己的问题,比如:

    • 会导致数据库表垃圾数据越来越多,从而影响查询效率
    • SQL中全都需要对逻辑删除字段做判断,影响查询效率
      因此,我不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。

    4.通用枚举

    像这种字段我们一般会定义一个枚举,做业务判断的时候就可以直接基于枚举做比较。但是我们数据库采用的是int类型,对应的PO也是Integer。因此业务操作时必须手动把枚举与Integer转换,非常麻烦。
    因此,MybatisPlus提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换。

    1.定义枚举

    package com.itheima.mp.enums;
    
    import com.baomidou.mybatisplus.annotation.EnumValue;
    import lombok.Getter;
    
    @Getter
    public enum UserStatus {
        NORMAL(1, "正常"),
        FREEZE(2, "冻结")
        ;
        private final int value;
        private final String desc;
    
        UserStatus(int value, String desc) {
            this.value = value;
            this.desc = desc;
        }
    }
    

    然后把User类中的status字段改为UserStatus 类型:

    要让MybatisPlus处理枚举与数据库类型自动转换,我们必须告诉MybatisPlus,枚举中的哪个字段的值作为数据库值。
    MybatisPlus提供了@EnumValue注解来标记枚举属性:

    2.配置枚举处理器

    mybatis-plus:
      configuration:
        default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
    

    为了使页面查询结果也是枚举格式,我们需要修改UserVO中的status属性:

    并且,在UserStatus枚举中通过@JsonValue注解标记JSON序列化时展示的字段:

    最后,在页面查询,结果如下:

    5.JSON类型处理器

    格式像这样:

    {"age": 20, "intro": "佛系青年", "gender": "male"}
    

    定义一个对象实体类:

    package com.itheima.mp.domain.po;
    
    import lombok.Data;
    
    @Data
    public class UserInfo {
        private Integer age;
        private String intro;
        private String gender;
    }
    

    6.配置加密

    目前我们配置文件中的很多参数都是明文,如果开发人员发生流动,很容易导致敏感信息的泄露。所以MybatisPlus支持配置文件的加密和解密功能。
    我们以数据库的用户名和密码为例。

    1.生成秘钥
    首先,我们利用AES工具生成一个随机秘钥,然后对用户名、密码加密:

    package com.itheima.mp;
    
    import com.baomidou.mybatisplus.core.toolkit.AES;
    import org.junit.jupiter.api.Test;
    
    class MpDemoApplicationTests {
        @Test
        void contextLoads() {
            // 生成 16 位随机 AES 密钥
            String randomKey = AES.generateRandomKey();
            System.out.println("randomKey = " + randomKey);
    
            // 利用密钥对用户名加密
            String username = AES.encrypt("root", randomKey);
            System.out.println("username = " + username);
    
            // 利用密钥对用户名加密
            String password = AES.encrypt("MySQL123", randomKey);
            System.out.println("password = " + password);
    
        }
    }
    

    结果:

    randomKey = 6234633a66fb399f
    username = px2bAbnUfiY8K/IgsKvscg==
    password = FGvCSEaOuga3ulDAsxw68Q==
    

    2.修改配置
    修改application.yaml文件,把jdbc的用户名、密码修改为刚刚加密生成的密文:

    spring:
      datasource:
        url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
        driver-class-name: com.mysql.cj.jdbc.Driver
        username: mpw:QWWVnk1Oal3258x5rVhaeQ== # 密文要以 mpw:开头
        password: mpw:EUFmeH3cNAzdRGdOQcabWg== # 密文要以 mpw:开头
    

    3.测试

    在启动项目的时候,需要把刚才生成的秘钥添加到启动参数中,像这样:
    --mpw.key=6234633a66fb399f
    单元测试的时候不能添加启动参数,所以要在测试类的注解上配置:

    2.分页插件

    2.1基本配置与使用

    1.配置分页插件

    import com.baomidou.mybatisplus.annotation.DbType;
    import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
    import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class MybatisConfig {
    
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            // 初始化核心插件
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            // 添加分页插件
            interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
            return interceptor;
        }
    }
    

    测试:

    @Test
    void testPageQuery() {
        // 1.分页查询,new Page()的两个参数分别是:页码、每页大小
        Page<User> p = userService.page(new Page<>(2, 2));
        // 2.总条数
        System.out.println("total = " + p.getTotal());
        // 3.总页数
        System.out.println("pages = " + p.getPages());
        // 4.数据
        List<User> records = p.getRecords();
        records.forEach(System.out::println);
    }
    

    常见的API如下:

    int pageNo = 1, pageSize = 5;
    // 分页参数
    Page<User> page = Page.of(pageNo, pageSize);
    // 排序参数, 通过OrderItem来指定
    page.addOrder(new OrderItem("balance", false));
    userService.page(page);
    

    2.通用分页实体

    1.实体类

    @Data
    @ApiModel(description = "分页查询实体")
    public class PageQuery {
        @ApiModelProperty("页码")
        private Long pageNo;
        @ApiModelProperty("每页数据条数")
        private Long pageSize;
        @ApiModelProperty("排序字段")
        private String sortBy;
        @ApiModelProperty("是否升序")
        private Boolean isAsc;
    }
    
    @EqualsAndHashCode(callSuper = true)
    @Data
    @ApiModel(description = "用户查询条件实体")
    public class UserQuery extends PageQuery {
        @ApiModelProperty("用户名关键字")
        private String name;
        @ApiModelProperty("用户状态:1-正常,2-冻结")
        private Integer status;
        @ApiModelProperty("余额最小值")
        private Integer minBalance;
        @ApiModelProperty("余额最大值")
        private Integer maxBalance;
    }
    

    @EqualsAndHashCode(callSuper = true)是Lombok注解之一,用于自动生成equals(Object other)和hashCode()方法。当我们使用该注解时,Lombok会自动为我们生成equals(Object other)和hashCode()方法的实现代码。其中,callSuper属性设置为true表示要调用父类的equals和hashCode方法,以确保在多层继承结构中也能正确比较对象的相等性。

    @Data
    @ApiModel(description = "分页结果")
    public class PageDTO<T> {
        @ApiModelProperty("总条数")
        private Long total;
        @ApiModelProperty("总页数")
        private Long pages;
        @ApiModelProperty("集合")
        private List<T> list;
    }
    

    2.开发接口

    @RestController
    @RequestMapping("users")
    @RequiredArgsConstructor
    public class UserController {
    
        private final UserService userService;
    
        @GetMapping("/page")
        public PageDTO<UserVO> queryUsersPage(UserQuery query){
            return userService.queryUsersPage(query);
        }
    
        // 。。。 略
    }
    

    然后在IUserService中创建queryUsersPage方法:

    PageDTO<UserVO> queryUsersPage(PageQuery query);
    

    接下来,在UserServiceImpl中实现该方法:

    @Override
    public PageDTO<UserVO> queryUsersPage(PageQuery query) {
        // 1.构建条件
        // 1.1.分页条件
        Page<User> page = Page.of(query.getPageNo(), query.getPageSize());
        // 1.2.排序条件
        if (query.getSortBy() != null) {
            page.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc()));
        }else{
            // 默认按照更新时间排序
            page.addOrder(new OrderItem("update_time", false));
        }
        // 2.查询
        page(page);
        // 3.数据非空校验
        List<User> records = page.getRecords();
        if (records == null || records.size() <= 0) {
            // 无数据,返回空结果
            return new PageDTO<>(page.getTotal(), page.getPages(), Collections.emptyList());
        }
        // 4.有数据,转换
        List<UserVO> list = BeanUtil.copyToList(records, UserVO.class);
        // 5.封装返回
        return new PageDTO<UserVO>(page.getTotal(), page.getPages(), list);
    }
    

    3.改造简化

    • 改造PageQuery实体
    @Data
    public class PageQuery {
        private Integer pageNo;
        private Integer pageSize;
        private String sortBy;
        private Boolean isAsc;
    
        public <T>  Page<T> toMpPage(OrderItem ... orders){
            // 1.分页条件
            Page<T> p = Page.of(pageNo, pageSize);
            // 2.排序条件
            // 2.1.先看前端有没有传排序字段
            if (sortBy != null) {
                p.addOrder(new OrderItem(sortBy, isAsc));
                return p;
            }
            // 2.2.再看有没有手动指定排序字段
            if(orders != null){
                p.addOrder(orders);
            }
            return p;
        }
    
        public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc){
            return this.toMpPage(new OrderItem(defaultSortBy, isAsc));
        }
    
        public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {
            return toMpPage("create_time", false);
        }
    
        public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {
            return toMpPage("update_time", false);
        }
    }
    

    这样我们在开发也时就可以省去对从PageQuery到Page的的转换:

    Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
    
    • 改造PageDTO实体
    package com.itheima.mp.domain.dto;
    
    import cn.hutool.core.bean.BeanUtil;
    import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.util.Collections;
    import java.util.List;
    import java.util.function.Function;
    import java.util.stream.Collectors;
    
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class PageDTO<V> {
        private Long total;
        private Long pages;
        private List<V> list;
    
        /**
         * 返回空分页结果
         * @param p MybatisPlus的分页结果
         * @param <V> 目标VO类型
         * @param <P> 原始PO类型
         * @return VO的分页对象
         */
        public static <V, P> PageDTO<V> empty(Page<P> p){
            return new PageDTO<>(p.getTotal(), p.getPages(), Collections.emptyList());
        }
    
        /**
         * 将MybatisPlus分页结果转为 VO分页结果
         * @param p MybatisPlus的分页结果
         * @param voClass 目标VO类型的字节码
         * @param <V> 目标VO类型
         * @param <P> 原始PO类型
         * @return VO的分页对象
         */
        public static <V, P> PageDTO<V> of(Page<P> p, Class<V> voClass) {
            // 1.非空校验
            List<P> records = p.getRecords();
            if (records == null || records.size() <= 0) {
                // 无数据,返回空结果
                return empty(p);
            }
            // 2.数据转换
            List<V> vos = BeanUtil.copyToList(records, voClass);
            // 3.封装返回
            return new PageDTO<>(p.getTotal(), p.getPages(), vos);
        }
    
        /**
         * 将MybatisPlus分页结果转为 VO分页结果,允许用户自定义PO到VO的转换方式
         * @param p MybatisPlus的分页结果
         * @param convertor PO到VO的转换函数
         * @param <V> 目标VO类型
         * @param <P> 原始PO类型
         * @return VO的分页对象
         */
        public static <V, P> PageDTO<V> of(Page<P> p, Function<P, V> convertor) {
            // 1.非空校验
            List<P> records = p.getRecords();
            if (records == null || records.size() <= 0) {
                // 无数据,返回空结果
                return empty(p);
            }
            // 2.数据转换
            List<V> vos = records.stream().map(convertor).collect(Collectors.toList());
            // 3.封装返回
            return new PageDTO<>(p.getTotal(), p.getPages(), vos);
        }
    }
    
    • 最终,业务层的代码可以简化为:
    @Override
    public PageDTO<UserVO> queryUserByPage(PageQuery query) {
        // 1.构建条件
        Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
        // 2.查询
        page(page);
        // 3.封装返回
        return PageDTO.of(page, UserVO.class);
    }
    

    如果是希望自定义PO到VO的转换过程,可以这样做:

    @Override
    public PageDTO<UserVO> queryUserByPage(PageQuery query) {
        // 1.构建条件
        Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
        // 2.查询
        page(page);
        // 3.封装返回
        return PageDTO.of(page, user -> {
            // 拷贝属性到VO
            UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
            // 用户名脱敏
            String username = vo.getUsername();
            vo.setUsername(username.substring(0, username.length() - 2) + "**");
            return vo;
        });
    }
    

    相关文章

      网友评论

          本文标题:SpringBoot入门—MybatisPlus(二)

          本文链接:https://www.haomeiwen.com/subject/uzpvcjtx.html