MyBatis-Plus

快速上手

最小单元

逆向工程只能生成 mapper 接口、映射文件、实体类 Model。MyBatis-Plus 可以生成 mapper 接口、映射文件、实体类 Model、service、controller。

<!-- connector -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- MyBatis-Plus 核心依赖 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.2</version>
</dependency>
<!-- lombok 简化实体类 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8&characterEncoding=utf-8&userSSL=false
    username: <username>
    password: <password>
# MyBatis-Plus 相关配置
mybatis-plus:
  configuration:
    #配置日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
@SpringBootApplication
@MapperScan("com.zszy.mybatisplus.mapper")
public class MybatisplusApplication { // mapper 接口所在包和映射文件所在包
    public static void main(String[] args) { SpringApplication.run(MybatisplusApplication.class, args); }
}
@Data // 等于 @NoArgsConstructor + @AllArgsConstructor + @Getter + @Setter + @EqualsAndHashCode
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
}
@Repository
public interface UserMapper extends BaseMapper<User> {}
@SpringBootTest
public class MyBatisPlusTest {
    // @Autowired 飘红是由于 MapperScan 扫描包下所有的接口, 并不是把接口交给 IOC 管理 => IOC 只能存在类所对应的 Bean, 不能存在接口对应的 Bean
    // 故是将 UserMapper 动态生成的代理类交给 IOC 容器管理 => IDEA 在编译时认为 UserMapper 无法进行自动装配 => 其实在运行阶段没有问题
    @Resource
    private UserMapper userMapper;
    /* 测试查询所有数据 */
    @Test
    void testSelectList(){
        // 通过条件构造器查询一个list集合 => 若没有条件则可设置null为参数
        List<User> users = userMapper.selectList(null);
        users.forEach(System.out::println);
    }
}

BaseMapper => 不用写 sql 也能实现数据库操作,但是没有通用 service 的批量添加。在开发场景中添加的数据可能是海量的,此时这些数据全部拼在一个 sql 语句是不现实的,故批量添加的方法在 service 中而不在 mapper。

mapper 层 => dao 层,用作对数据库进行数据持久化操作。

controller 层 => 负责具体模块的业务流程控制,需要调用 service 逻辑设计层的接口来控制业务流程。接收前端传来的参数并进行业务操作,再将处理结果返回。

CRUD

BaseMapper<T>

基本 crud 的操作在内置的 BaseMapper 中都已得到实现,因此在 xxxMapper 中只需继承该接口后就可以直接使用。

  • BaseMapper 接口的源码封装
/* BaseMapper 源码 */
public interface BaseMapper<T> extends Mapper<T> {
    // 插入一条记录
    int insert(T entity);
    // 根据 ID 删除
    int deleteById(Serializable id);
    // 根据 entity 条件删除记录
    int deleteById(T entity);
    // 根据 columnMap 条件删除记录
    int deleteByMap(@Param("cm") Map<String, Object> columnMap);

    int delete(@Param("ew") Wrapper<T> queryWrapper);
    // 根据 ID 批量删除
    int deleteBatchIds(@Param("coll") Collection<?> idList);
    // 根据 ID 修改
    int updateById(@Param("et") T entity);
    // 根据 Wrapper 条件更新记录
    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);
    // 根据 columnMap 条件查询
    List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);
    // 根据 entity 条件查询一条记录
    default T selectOne(@Param("ew") Wrapper<T> queryWrapper) {
        List<T> ts = this.selectList(queryWrapper);
        if (CollectionUtils.isNotEmpty(ts)) {
            if (ts.size() != 1) {
                throw ExceptionUtils.mpe("One record is expected, but the query result is multiple records", new Object[0]);
            } else {
                return ts.get(0);
            }
        } else {
            return null;
        }
    }

    default boolean exists(Wrapper<T> queryWrapper) {
        Long count = this.selectCount(queryWrapper);
        return null != count && count > 0L;
    }
    // 根据 Wrapper 条件查询总记录数
    Long selectCount(@Param("ew") Wrapper<T> queryWrapper);

    List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);
    // 根据 Wrapper 条件查询全部记录
    List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);
    // 根据 Wrapper 条件查询全部记录 => 注意只返回第一个字段的值
    List<Object> selectObjs(@Param("ew") Wrapper<T> queryWrapper);
    // 根据 entity 条件查询全部记录(并翻页)
    <P extends IPage<T>> P selectPage(P page, @Param("ew") Wrapper<T> queryWrapper);
    // 根据 Wrapper 条件查询全部记录(并翻页)
    <P extends IPage<Map<String, Object>>> P selectMapsPage(P page, @Param("ew") Wrapper<T> queryWrapper);
}
  • 调用 Mapper 层实现 CRUD
@SpringBootTest
public class MyBatisPlusTest {
    ...
    /* 测试插入数据 */
    @Test
    void testInsert(){
        // 新增用户信息
        User user = new User();
        user.setName("zs");
        user.setAge(22);
        user.setEmail("zs@mail.com");
        int insert = userMapper.insert(user);
        System.out.println("insert => " + insert + "---" + "userId => " +user.getUid());
    }
    /* 测试删除数据 */
    @Test
    void testDelete(){
        // 删除用户信息
//        int i = userMapper.deleteById(1538798688154005506L);
//        System.out.println(i);
        // 根据 map 集合所设置的条件删除用户信息
        // 执行 SQL => DELETE FROM user WHERE name = ? AND age = ?
//        Map<String, Object> map = new HashMap<>();
//        map.put("name","zs");
//        map.put("age",22);
//        int i = userMapper.deleteByMap(map);
//        System.out.println(i);
        // 通过多个 id 实现批量删除 => DELETE * FROM user WHERE id IN (?,?,?)
        List<Long> list = Arrays.asList(1L, 2L, 3L);
        int i = userMapper.deleteBatchIds(list);
        System.out.println("result => " +i);
    }
    /* 测试更新数据 */
    @Test
    void testUpdate(){
        // UPDATE user SET name=?, age=?, email=? WHERE id=?
        User user = new User();
        user.setUid(4L);
        user.setName("jr");
        user.setAge(20);
        user.setEmail("jr@mail.com");
        int i = userMapper.updateById(user);
        System.out.println(i);
    }
    /* 测试查询数据 */
    @Test
    void testSelect(){
//        User user = userMapper.selectById(1L);
//        System.out.println(user);
//        List<Long> list = Arrays.asList(1L,2L,3L);
//        List<User> users = userMapper.selectBatchIds(list);
//        users.forEach(System.out::println);
        // 根据 map 集合条件查询 => SELECT id,name,age,email FROM user WHERE name = ? AND age = ?
//        Map<String, Object> map = new HashMap<>();
//        map.put("name","jr");
//        map.put("age","20");
//        List<User> users = userMapper.selectByMap(map);
//        users.forEach(System.out::println);
//        List<User> users = userMapper.selectList(null);
//        users.forEach(System.out::println);
        // 测试自定义查询
        Map<String, Object> stringObjectMap = userMapper.selectMapByIdByZs(1L);
        System.out.println(stringObjectMap);
    }
}
@Repository
public interface UserMapper extends BaseMapper<User> {
    /**
     * 根据 uid 查询用户信息为 map 的集合
     * @param uid
     * @return
     */
    @MapKey("uid")
    Map<String,Object> selectMapByIdByZs(Long uid);
}
<mapper namespace="com.zszy.mybatisplus.mapper.UserMapper">
    <!-- Map<String,Object> selectMapByIdByZs(Long id); -->
    <select id="selectMapByIdByZs" resultType="map">
        select uid,user_name,age,email from t_user where uid = #{uid};
    </select>
</mapper>

Iservice<T>

MybatisPlus 不止提供通用的 mapper 还提供通用的 service。即 service 也可使用其提供的通用 service 中的方法。IService 与其实现类 ServiceImpl 封装了常见的业务层逻辑。

  • IService
// IService 简易源码
public interface IService<T> {... save、remove、update、get、page、list}
  • ServiceImpl
// ServiceImpl 简易源码
// M 即开发者定义的 xxxMapper、T 是当前操作的实体类对象
public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {}
  • 调用 Service 层实现 CRUD
/* service */
public interface UserService extends IService<User> {}
/* service.impl */
// mybatis-plus 提供的通用 service => 不必自己重写 -> 直接继承 IService 的实现类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {}
@SpringBootTest
public class MyBatisPlusServiceTest {
    @Autowired
    private UserService userService;
    @Test
    public void testGetCount(){
        // 查询总记录数
        long count = userService.count();
        System.out.println("总记录数 => " + count);
    }
    // 批量添加
    @Test
    public void testInsertMore(){
        List<User> list = new ArrayList<>();
        for (int i = 1; i <= 5; i++){
            User user = new User();
            user.setName("gz"+i);
            user.setAge(20+i);
            list.add(user);
        }
        boolean b = userService.saveBatch(list);
        System.out.println(b);
    }
}

个人小结

  • 关于批处理

IService 接口是对 BaseMapper 的进一步封装,为什么主要是封装批量操作?(为什么在 BaseMapper 没有批量添加,而 Iservice 有?)

从分层角度来看,BaseMapper 是 DAO 层的 CRUD 封装,而 IService 是业务逻辑层的 CRUD 封装,所以增加批量 CRUD 操作封装,符合官方指南中的阐述。

从源码角度来看,IService 是对 BaseMapper 的扩展 => BaseMapper、IService、ServiceImpl。

从具体实现来看,若继承 BaseMapper(确实是继承接口),那么不需要去实现其内部方法,而是根据 MyBatis 的动态代理即实现的 CRUD 操作;如果是用自定义 IBaseService 去继承 IService,那么需要去实现 IService 中的方法。

常用注解

MyBatis-Plus 提供的注解常用作解决数据库与实体间相互映射的问题。

@TableName

异常 BadSqlGrammarException => 实体类的类名和操作表的表名不一致,导致出现当前操作的表不存在的提示 => Table 'xxx' doesn't exist。

  • 处理方式一 => 在 pojo 类上使用注解 @TableName("指定数据库中表名")
@Data
@TableName("xxx_xxx")
public class Xxx(){}
  • 处理方式二 => 通过全局配置对所有实体类加上对应的表的前缀
mybatis-plus:
  ...
  global-config:
    db-config:
      table-prefix: t_ # 对实体类所有表加上这个指定的前缀

@TableId

MyBatis-Plus 在实现 CRUD 时,会默认将 id 作为主键列,并在插入数据时,默认基于雪花算法的策略生成 id。

当实体类和表中表示主键的不是 id,而是其他字段时,MyBatisPlus 不会自动识别出主键。

异常 DataIntegrityViolationException => 没有在实体类中发现其默认的 id,同时指明当前实体类中本意作为主键的属性没有默认值 => Field 'xxxx' doesn't have a default value。

  • 解决方式 => 对实体类中指定属性以 @TableId 标识其为主键
@Date
public class Xxx {
    @TableId
    private Long uid;
    ...
}
  • @TableId 的 value 属性

异常 BadSqlGrammarException => 当实体类主键对应 id,表中主键对应 uid,且不对 @TableId 进行 value 属性的指定 => Unknown column 'id' in 'field list'。

原因是 MyBatis-Plus 仍然会将 id 作为表的主键进行操作,而表中表示主键的是字段 uid。解决方式是显式指明 value。

@TableId("uid") or @TableId(value="uid")
  • @TableId 的 type 属性 => 定义主键的生成策略 -> 默认为雪花算法
描述
IdType.ASSIGN_ID(default) 雪花算法生成数据 id,与数据库 id 是否设置自增无关
IdType.AUTO 使用数据库的自增策略,注意该类型在使用时应确保数据库已设置 id 的自增
public class User {
    @TableId("uid", type=IdType.AUTO)
    private Long id;
    ...
}
#MyBatis-Plus相关配置
mybatis-plus:
  configuration:
    #配置日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      # 配置mp的主键策略为自增
      id-type: auto
      # 设置实体类所对应的表的统一前缀
      table-prefix: t_

分表与雪花算法

为应对数据规模的增长,以应对逐渐增长的访问压力和数据量。数据库的扩展可以使用业务分库、主从复制以及数据库分表。

  • 垂直分表

垂直分表适合将表中某些不常用且占据大量空间的列拆分出去。简而言之就是将不常用的字段独立到另外一张表。

  • 水平分表

水平分表适合表行数特别大的表,通常表的数据量达到千万级别时,就应该考虑架构的性能瓶颈或者隐患。

以自增主键进行阶段分表的优势在于可以随着数据的增加平滑地扩充新表,但是可能造成分布不均的情况。取模分表的复杂性在于初始表数量的确定,单数具有分布均匀的优势,缺点就是扩充新表需要重新将数据分布。

  • 雪花算法

雪花算法是分布式主键的生成算法,能够保证不同表的主键的不重复性,以及相同表主键的有序性。

其值是由 1bit 符号位、41bit 时间截、10bit 机器 ID 以及 12bit 流水号组成。

@TbaleField

@TbaleField => 处理实体类中的属性名和数据库中字段名不一致的情况

在实体类的属性中使用驼峰命名,表中字段使用的下划线命名时,自动将下划线命名风格转化为驼峰命名风格。但是其他情况下,纯粹的实体类属性名与数据库字段不一致时,要在实体类属性上使用 @TableField("xxx") 以对应字段名。

public class User {
    @TableId("uid")
    private Long id;
    @TableField("username")
    private String name;
    ...
}

@TableLogic

物理删除是真实删除,即从数据库删除对应数据,之后的查询就不能查到此条被删除的记录;逻辑删除是表面删除,是将对应数据中代表是否被删除字段的状态修改为"被删除的状态",之后在数据库中仍旧能看到此条数据的记录,方便数据恢复。

  • 于数据库创建逻辑删除状态列 is_deleted,默认值为零
  • 在实体类中添加逻辑删除属性 isDeleted,并标记 @TableLogic 注释
  • 此后调用删除方法就会进行逻辑删除

条件构造器

Wrapper

Wrapper 条件构造器用于封装当前操作的条件,也是当前条件构造器最顶层的抽象类。BaseMapper 中带有条件构造器的方法在使用时要确定好是具体 Wrapper 类型的哪种子类。

AbstractWrapper1.AbstractLambdaWrapper1.1.LambdaUpdateWrapper1.1.1.LambdaQueryWrapper1.1.2.UpdateWrapper1.2.QueryWrapper1.3.Wrapper
  • 源码分析
// AbstractWrapper 源码
public abstract class AbstractWrapper<T, R, Children extends AbstractWrapper<T, R, Children>> extends Wrapper<T> implements Compare<Children, R>, Nested<Children, Children>, Join<Children>, Func<Children, R> {
    ...
    public Children like(boolean condition, R column, Object val) {} // like(数据库字段名, 模糊值)
    public Children between(boolean condition, R column, Object val1, Object val2) {} // between(数据库字段名, 范围1, 范围2)
    public Children isNotNull(boolean condition, R column) {} // isNotNull(数据库字段名)
}
  • 组装查询条件
@SpringBootTest
public class MyBatisPlusWrapperTest {
    @Autowired
    private UserMapper userMapper;
    /* 条件构造器 */
    // 查询条件
    @Test
    public void test01(){
        QueryWrapper<User> queryWrapper = new QueryWrapper<User>();
        queryWrapper.like("user_name","a")
                        .between("age",20,30)
                                .isNotNull("email");
        List<User> list = userMapper.selectList(queryWrapper);
        list.forEach(System.out::println);
    }
}
  • 组装排序条件
@SpringBootTest
public class MyBatisPlusWrapperTest {
    @Autowired
    private UserMapper userMapper;
    /* 条件构造器 */
    // 排序条件
    @Test
    public void test02(){
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        // 先按照年龄降序排再按照升序排
        queryWrapper.orderByDesc("age")
                        .orderByAsc("uid");
        List<User> list = userMapper.selectList(queryWrapper);
        list.forEach(System.out::println);
    }
}
  • 组装删除条件
@SpringBootTest
public class MyBatisPlusWrapperTest {
    @Autowired
    private UserMapper userMapper;
    /* 条件构造器 */
    // 组装删除条件
    @Test
    public void test03(){
        // 删除邮箱地址为 null 的用户信息
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.isNull("email");
        int i = userMapper.delete(queryWrapper);
        System.out.println(i);
    }
}
  • 通过条件构造器实现修改功能
@SpringBootTest
public class MyBatisPlusWrapperTest {
    @Autowired
    private UserMapper userMapper;
    /* 条件构造器 */
    // queryWrapper 实现修改操作
    @Test
    public void test04(){
        // 修改年龄大于20且用户名包含a或邮箱为null的用户信息
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.gt("age",20)
                .like("user_name","aa")
                .or()
                .isNull("email");
        User user = new User();
        user.setName("zs");
        user.setEmail("zs@mail.com");
        int update = userMapper.update(user, queryWrapper); // 参数一设置修改的内容、参数二设置修改的条件
        System.out.println(update);
    }
}
  • 条件优先级
@SpringBootTest
public class MyBatisPlusWrapperTest {
    @Autowired
    private UserMapper userMapper;
    /* 条件构造器 */
    // 优先级
    @Test
    public void test05(){
        // 修改用户名包含a且(年龄大于20或邮箱为null)的用户信息
        // lambda 中条件优先执行
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.like("user_name","a")
                .and(i -> i.gt("age",20).or().isNull("email"));
        User user = new User();
        user.setName("gz");
        user.setEmail("gz@mail.com");
        int update = userMapper.update(user, queryWrapper);
        System.out.println(update);
    }
}
  • 查询指定字段
@SpringBootTest
public class MyBatisPlusWrapperTest {
    @Autowired
    private UserMapper userMapper;
    /* 条件构造器 */
    // 组装 select 字句
    @Test
    public void test06(){
        // 查询用户名、年龄、邮箱
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.select("user_name","age","email");
        List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
        maps.forEach(System.out::println);
    }
}
  • 组装子查询
@SpringBootTest
public class MyBatisPlusWrapperTest {
    @Autowired
    private UserMapper userMapper;
    /* 条件构造器 */
    // 条件构造器实现子查询
    @Test
    public void test07(){
        // 查询 id 小于等于 100 的用户信息
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.inSql("uid","select uid from t_user where uid <= 100");
        List<User> users = userMapper.selectList(queryWrapper);
        users.forEach(System.out::println);
    }
}

UpdateWrapper

QueryWrapper 与 UpdateWrapper 都能进行修改功能的操作,但是前者需要创建实体类对象,后者不需要显示的创建实体类对象。同时 UpdateWrapper 既可以设置修改条件,也可以设置修改字段。

  • 通过条件构造器实现修改功能
@SpringBootTest
public class MyBatisPlusWrapperTest {
    @Autowired
    private UserMapper userMapper;
    /* 条件构造器 */
    // 通过条件构造器实现修改功能
    @Test
    public void test08(){
        UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
        updateWrapper.like("user_name","a")
                .and(i -> i.gt("age",20).or().isNull("email"));
        updateWrapper.set("user_name","hz").set("email","hz@mail.com");
        int update = userMapper.update(null, updateWrapper);
        System.out.println(update);
    }
}
  • 模拟开发组装条件
@SpringBootTest
public class MyBatisPlusWrapperTest {
    @Autowired
    private UserMapper userMapper;
    /* 条件构造器 */
    @Test
    public void test09(){
        String username = "a";
        Integer ageBegin = null;
        Integer ageEnd = 30;
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        if(StringUtils.isNotBlank(username)){
            //isNotBlank判断某个字符创是否不为空字符串、不为null、不为空白符
            queryWrapper.like("user_name", username);
        }
        if(ageBegin != null){
            queryWrapper.ge("age", ageBegin);
        }
        if(ageEnd != null){
            queryWrapper.le("age", ageEnd);
        }
        List<User> list = userMapper.selectList(queryWrapper);
        list.forEach(System.out::println);
    }
}
  • 使用 condition 组装条件 => 简化开发中的组装过程

在方法中布尔类型的 condition 满足条件则组装内容,不满足则不组装。

@SpringBootTest
public class MyBatisPlusWrapperTest {
    @Autowired
    private UserMapper userMapper;
    /* 条件构造器 */
    // condition
    @Test
    public void test10(){
        String username = "a";
        Integer ageBegin = null;
        Integer ageEnd = 30;
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.like(StringUtils.isNotBlank(username), "user_name", username)
                .ge(ageBegin != null, "age", ageBegin)
                .le(ageEnd != null, "age", ageEnd);
        List<User> list = userMapper.selectList(queryWrapper);
        list.forEach(System.out::println);
    }
}

lambda 前缀

此前常使用字符串类型的字段名作为方法中的第二个参数描述字段,但在 Lambda 前缀的情况下,改为一个函数式接口 SFunction<Xxx, ?>。这个接口主要是为防止将字段名写错,通过一个函数访问实体类的属性所对应的字段名。

  • LambdaQueryWrapper
@SpringBootTest
public class MyBatisPlusWrapperTest {
    @Autowired
    private UserMapper userMapper;
    /* 条件构造器 */
    @Test
    public void test11(){
        String username = "a";
        Integer ageBegin = null;
        Integer ageEnd = 30;
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(StringUtils.isNotBlank(username), User::getName, username)
                // 使用函数式接口访问实体类某个属性对应的字段名 => 防止字段名写错
                .ge(ageBegin != null, User::getAge, ageBegin)
                .le(ageEnd != null, User::getAge, ageEnd);
        List<User> list = userMapper.selectList(queryWrapper); // 顶层抽象类的任何子类都可为其赋值
        list.forEach(System.out::println);
    }
}
  • LambdaUpdateWrapper
@SpringBootTest
public class MyBatisPlusWrapperTest {
    @Autowired
    private UserMapper userMapper;
    /* 条件构造器 */
    @Test
    public void test12(){
        //将用户名中包含有a并且(年龄大于20或邮箱为null)的用户信息修改
        LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.like(User::getName, "aaa")
                .and(i -> i.gt(User::getAge, 20).or().isNull(User::getEmail));
        updateWrapper.set(User::getName, "okk").set(User::getEmail,"okk@mail.com");
        int result = userMapper.update(null, updateWrapper);
        System.out.println("result:"+result);
    }
}

相关插件与逆向工程

分页插件

  • 基本使用
@Configuration
@MapperScan("com.zszy.mybatisplus.mapper")
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){ // MybatisPlusInterceptor 用于配置插件
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}
@SpringBootTest
public class MybatisplusPluginTest {
    @Autowired
    private UserMapper userMapper;
    @Test
    public void testPage(){
        // new Page()中的两个参数分别是当前页码、每页显示数量
        Page<User> page = userMapper.selectPage(new Page<>(1, 2), null);
        List<User> users = page.getRecords();
        users.forEach(System.out::println);
    }
    @Test
    public void testPageVo(){
        Page<User> page = userMapper.selectPageVo(new Page<User>(1,2), 20);
        List<User> users = page.getRecords();
        users.forEach(System.out::println);
    }
}
  • 特殊使用 => 查询语句自定义的情况
@Repository
public interface UserMapper extends BaseMapper<User> {
    /**
     * 根据年龄查询用户列表分页显示
     * @param page 分页对象,xml中可以从里面进行取值,传递参数 Page 即自动分页,必须放在第一位
     * @param age 年龄
     * @return
     */
    Page<User> selectPageVo(@Param("page") Page<User> page, @Param("age") Integer age);
}
<mapper namespace="com.zszy.mybatisplus.mapper.UserMapper">
    <!-- Page<User> selectPageVo(@Param("page") Page<User> page, @Param("age") Integer age); -->
    <select id="selectPageVo" resultType="User">
        select uid,username as name,age,email from t_user where age > #{age}
    </select>
</mapper>
mybatis-plus:
  ...
  type-aliases-package: com.zszy.mybatisplus.pojo # 配置类型别名对应的包
@SpringBootTest
public class MyBatisPlusPluginsTest {
    @Autowired
    private UserMapper userMapper;
    /* 条件构造器 */
    @Test
    public void testPageVo(){
        Page<User> page = userMapper.selectPageVo(new Page<User>(1,2), 20);
        List<User> users = page.getRecords();
        users.forEach(System.out::println);
    }
}

乐观锁插件

@Data
public class Product {
    private Long id;
    private String name;
    private Integer price;
    @Version // @Version => 乐观锁版本号字段
    private Integer version;
}
@Configuration
@MapperScan("com.zszy.mybatisplus.mapper")
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //添加乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}
@SpringBootTest
public class MybatisplusPluginTest {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private ProductMapper productMapper;
    @Test
    public void testOptimisticLocking(){
        //1.小李获取商品价格
        Product productLi = productMapper.selectById(1);
        System.out.println("小李获取的商品价格为:" + productLi.getPrice());

        //2.小王获取商品价格
        Product productWang = productMapper.selectById(1);
        System.out.println("小李获取的商品价格为:" + productWang.getPrice());

        //3.小李修改商品价格+50
        productLi.setPrice(productLi.getPrice()+50);
        productMapper.updateById(productLi);

        //4.小王修改商品价格-30
        productWang.setPrice(productWang.getPrice()-30);
        int result = productMapper.updateById(productWang);
        if(result == 0){
            //操作失败,重试
            Product productNew = productMapper.selectById(1);
            productNew.setPrice(productNew.getPrice()-30);
            productMapper.updateById(productNew);
        }

        //5.老板查询商品价格
        Product productBoss = productMapper.selectById(1);
        System.out.println("老板获取的商品价格为:" + productBoss.getPrice());
    }
}

MyBatisX

MyBatisX 用于快速代码生成。先在 Idea 配置 Database 数据源,再在指定数据库的数据表上用右键选择 MyBatisX-Generator。生成代码后,在相应的 XxxMapper 中定义见名识意的方法,最好是以 CRUD 开头接操作字段。最后通过快捷键 alt + enter 选择 [MybatisX] Generate Mybatis Sql 进行自动补全。

public interface UserMapper extends BaseMapper<User> {
    int insertSelective(User user); // 传输的实体类对象中属性为 null 不会添加在字段

    int updateAgeAndSexByUid(@Param("age") Integer age, @Param("sex") Integer sex, @Param("uid") Long uid);

    List<User> selectAgeAndSexByAgeBetween(@Param("beginAge") Integer beginAge, @Param("endAge") Integer endAge);

    List<User> selectAllOrderByAgeDesc();
}

代码生成器

逆向工程也是一种代码生成器,即通过表生成实体类、映射文件、Mapper 接口。

MyBatisPlus 中的代码生成器虽然同样是根据表进行逆向生成,但是除了上述产物,还能生成更多类似控制层、业务层、持久层的代码。

  • 相关依赖
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.5.2</version>
</dependency>
<!-- java.lang.NoClassDefFoundError: freemarker/template/Configuration -->
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.31</version>
</dependency>
  • 测试代码
@SpringBootTest
public class FastAutoGeneratorTest {
    @Test
    public void testAutoGenerate() {
        FastAutoGenerator.create("jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8&characterEncoding=utf-8&userSSL=false", "<UserName>", "<PassWord>")
                .globalConfig(builder -> {
                    builder.author("zszy") // 设置作者
                            .enableSwagger() // 开启 swagger 模式
                            .fileOverride() // 覆盖已生成文件
                            .outputDir("/Users/xxx/IdeaProjects/zsSpringBoot/mybatisplus/src/main/java/com/zszy/mybatisplus/generateDir"); // 指定输出目录
                })
                .packageConfig(builder -> {
                    builder.parent("com.zszy.mybatisplus.samples.generator") // 设置父包名
                            .moduleName("mybatisPlus") // 设置父包模块名
                            .pathInfo(Collections.singletonMap(OutputFile.xml, "/Users/xieziyi/IdeaProjects/zsSpringBoot/mybatisplus/src/main/java/com/zszy/mybatisplus/generateDir")); // 设置mapperXml生成路径
                })
                .strategyConfig(builder -> {
                    builder.addInclude("t_user") // 设置需要生成的表名
                            .addTablePrefix("t_", "c_"); // 设置过滤表前缀
                })
                .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                .execute();

    }
}

开发常见场景

通用枚举

开发中某些表的字段是固定值,可以使用通用枚举进行实现。

@Getter
public enum SexEnum {
    MALE(1,"male"),
    FEMALE(2,"female");
    @EnumValue // 将注解所标识的属性的值存储到数据库中
    private Integer sex;
    private String sexName;

    SexEnum(Integer sex, String sexName) {
        this.sex = sex;
        this.sexName = sexName;
    }
}
mybatis-plus:
  ...
  # 扫描通用枚举的包
  type-enums-package: com.zszy.mybatisplus.enums
@Data // 等于 @NoArgsConstructor + @AllArgsConstructor + @Getter + @Setter + @EqualsAndHashCode
public class User {
    ...
    private SexEnum sex;
}
@SpringBootTest
public class MyBatisPlusEnumTest {
    @Autowired
    private UserMapper userMapper;
    @Test
    public void test(){
        User user = new User();
        user.setName("admin");
        user.setEmail("admin@mail.com");
        user.setAge(29);
        user.setSex(SexEnum.MALE);
        int insert = userMapper.insert(user);
        System.out.println(insert);
    }
}

多数据源

多数据源适用于纯粹多库、读写分离、一主多从、混合模式的开发,通过多库中的表分别获取数据。

  • 创建测试数据库和表
CREATE DATABASE `mybatis_plus_1` /*!40100 DEFAULT CHARACTER SET utf8mb4 */; use `mybatis_plus_1`; CREATE TABLE product ( id BIGINT(20) NOT NULL COMMENT '主键ID', name VARCHAR(30) NULL DEFAULT NULL COMMENT '商品名称', price INT(11) DEFAULT 0 COMMENT '价格', version INT(11) DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id) );
  • 添加测试数据
INSERT INTO product (id, NAME, price) VALUES (1, 'mbp', 100);
  • 删除 mybatis_plus 库 product 表
use mybatis_plus; DROP TABLE IF EXISTS product;
  • 引入依赖
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>
  • 多数据源配置文件 => application.yml
spring:
  # 配置数据源信息
  datasource:
    dynamic:
      # 设置默认的数据源或者数据源组, 默认值即为 master
      primary: master
      # 严格匹配数据源, 默认false => true -> 未匹配到指定数据源时抛异常, false -> 使用默认数据源
      strict: false
      datasource:
        master: # 和上述设置的 master 保持一致
          url: jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8&characterEncoding=utf-8&userSSL=false
          driver-class-name: com.mysql.cj.jdbc.Driver
          username: <username>
          password: <password>
        slave_1:
          url: jdbc:mysql://localhost:3306/mybatis_plus_1?serverTimezone=GMT%2B8&characterEncoding=utf-8&userSSL=false
          driver-class-name: com.mysql.cj.jdbc.Driver
          username: <username>
          password: <password>
  • 测试文件
@SpringBootTest
class MyBatisPlusXApplicationTests {

    @Test
    void contextLoads() {
    }

    @Autowired
    private UserService userService;
    @Autowired // 忘记写了导致 java.lang.NullPointerException
    private ProductService productService;

    @Test
    public void test01(){
        System.out.println(userService.getById(1));
        System.out.println(productService.getById(1));
    }
}
  • ServiceImpl 文件配置
@Service
@DS("master")
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {}
@Service
@DS("slave_1")
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {}

Bugs 处理

  • AnnotationFormatError + MapperScan
java.lang.annotation.AnnotationFormatError: Invalid default: public abstract java.lang.Class org.mybatis.spring.annotation.MapperScan.factoryBean()

关于 MapperScan.factoryBean 的注解格式化异常,可定位到导入的 Mapper 包范围,应确认导入 import org.mybatis.spring.annotation.MapperScan;

注意在 pom 中应导入 <artifactId>mybatis-plus-boot-starter</artifactId> 而不是 <artifactId>mybatis-plus</artifactId>。

结束

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处!