MyBatis-Plus
MyBatis-Plus 基础
简介
MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
-
无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑。
-
损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作。
-
强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求。
-
支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错。
-
支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题。
-
支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作。
-
内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用。
-
内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询。
-
分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库。
-
内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询。
-
内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作。
入门
创建数据库和表
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`user_id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '0-正常,1-删除',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者',
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新人',
`version` int NULL DEFAULT NULL COMMENT '乐观锁',
PRIMARY KEY (`user_id`) USING BTREE,
INDEX `user_idx1_username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'admin', '123456', '0', '2021-08-29 00:38:43', '2021-08-29 00:38:45', 'admin', 'admin', 1);
INSERT INTO `sys_user` VALUES (2, 'loner', '123456', '0', '2021-08-29 00:38:43', '2021-08-29 00:38:45', 'admin', 'admin', 1);
INSERT INTO `sys_user` VALUES (3, 'Bob', '123456', '0', '2021-08-29 00:38:43', '2021-08-29 00:38:45', 'admin', 'admin', 1);
INSERT INTO `sys_user` VALUES (4, 'Jack', '123456', '0', '2021-08-29 00:38:43', '2021-08-29 00:38:45', 'admin', 'admin', 1);
导入相关依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.3</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--MyBatis-Plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>3.4.3</version>
</dependency>
<!--Druid数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
<!--MySQL驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
编写配置文件
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.1.11:3306/mybatis-plus?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true
username: root
password: MaH00...
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
使用和测试
// 创建用户实体表
@Data
@TableName("sys_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
@TableId(value = "user_id", type = IdType.AUTO)
private Long userId;
/**
* 用户账号
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 删除标志(0代表存在 2代表删除)
*/
@TableLogic
private String delFlag;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private String createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/**
* 乐观锁
*/
private Integer version;
}
// 继承BaseMapper接口
public interface UserMapper extends BaseMapper<User> {
}
// 编写测试代码
@SpringBootTest
public class UserTest {
@Autowired
private UserMapper userMapper;
@Test
public void test() {
// 直接使用BaseMapper定义好的查询方法
List<User> userList = userMapper.selectList(null);
userList.forEach(System.out::println);
}
}
整个MP(简称)的使用过程,就是如此的简单,当我们继承完 BaseMapper 接口后,我们就完成了 CRUD 操作,剩余的只是针对不同的业务进行封装使用罢了。
MyBatis-Plus 核心
CRUD 接口
Mapper CRUD 接口
通用 CRUD 封装 BaseMapper (opens new window)接口,为 Mybatis-Plus 启动时自动解析实体表关系映射转换为 Mybatis 内部对象注入容器,泛型 T 为任意实体对象,参数 Serializable 为任意类型主键,Mybatis-Plus 不推荐使用复合主键约定每一张表都有自己的唯一 id 主键。
// 插入一条记录
int insert(T entity);
// 根据 ID 删除
int deleteById(Serializable id);
// 根据 entity 条件,删除记录
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
// 删除(根据ID 批量删除)
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 columnMap 条件,删除记录
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
// 根据 ID 修改
int updateById(@Param(Constants.ENTITY) T entity);
// 根据 whereWrapper 条件,更新记录
int update(@Param(Constants.ENTITY) T updateEntity, @Param(Constants.WRAPPER) Wrapper<T> whereWrapper);
// 根据 ID 查询
T selectById(Serializable id);
// 根据 entity 条件,查询一条记录
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
// 根据 entity 条件,查询全部记录
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 查询(根据 columnMap 条件)
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
// 根据 Wrapper 条件,查询全部记录
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 entity 条件,查询全部记录(并翻页)
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录(并翻页)
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询总记录数
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
Service CRUD 接口
通用 Service CRUD 封装 IService (opens new window)接口,进一步封装 CRUD,采用 get 查询单行,remove 删除 ,list 查询集合,page 分页,前缀命名方式区分 Mapper 层避免混淆,泛型 T 为任意实体对象。
建议如果存在自定义通用 Service 方法的可能,请创建自己的 IBaseService 继承 Mybatis-Plus 提供的基类
对象 Wrapper 为 条件构造器。
Save
// 插入一条记录(选择字段,策略插入)
boolean save(T entity);
// 插入(批量)
boolean saveBatch(Collection<T> entityList);
// 插入(批量),bathSize指插入批次数量
boolean saveBatch(Collection<T> entityList, int batchSize);
Remove
// 根据 entity 条件,删除记录
boolean remove(Wrapper<T> queryWrapper);
// 根据 ID 删除
boolean removeById(Serializable id);
// 根据 columnMap 条件,删除记录
boolean removeByMap(Map<String, Object> columnMap);
// 删除(根据ID 批量删除)
boolean removeByIds(Collection<? extends Serializable> idList);
Update
// 根据 UpdateWrapper 条件,更新记录 需要设置sqlset
boolean update(Wrapper<T> updateWrapper);
// 根据 whereWrapper 条件,更新记录
boolean update(T updateEntity, Wrapper<T> whereWrapper);
// 根据 ID 选择修改
boolean updateById(T entity);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList, int batchSize);
SaveOrUpdate
// TableId 注解存在更新记录,否插入一条记录
boolean saveOrUpdate(T entity);
// 根据updateWrapper尝试更新,否继续执行saveOrUpdate(T)方法
boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize);
Get
// 根据 ID 查询
T getById(Serializable id);
// 根据 Wrapper,查询一条记录。结果集,如果是多个会抛出异常,随机取一条加上限制条件 wrapper.last("LIMIT 1")
T getOne(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
T getOne(Wrapper<T> queryWrapper, boolean throwEx);
// 根据 Wrapper,查询一条记录
Map<String, Object> getMap(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
<V> V getObj(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);
List
// 查询所有
List<T> list();
// 查询列表
List<T> list(Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
Collection<T> listByIds(Collection<? extends Serializable> idList);
// 查询(根据 columnMap 条件)
Collection<T> listByMap(Map<String, Object> columnMap);
// 查询所有列表
List<Map<String, Object>> listMaps();
// 查询列表
List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper);
// 查询全部记录
List<Object> listObjs();
// 查询全部记录
<V> List<V> listObjs(Function<? super Object, V> mapper);
// 根据 Wrapper 条件,查询全部记录
List<Object> listObjs(Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录
<V> List<V> listObjs(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);
Page
// 无条件分页查询
IPage<T> page(IPage<T> page);
// 条件分页查询
IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper);
// 无条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page);
// 条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page, Wrapper<T> queryWrapper);
条件构造器
基本条件
// 等于,例如 eq("name", "老王") 等价于 name = '老王'
eq(R column, Object val)
// 不等于,例如 ne("name", "老王") 等价于 name <> '老王'
ne(R column, Object val)
// 大于,例如 gt("age", 18) 等价于 age > 18
gt(R column, Object val)
// 小于,例如 lt("age", 18) 等价于 age < 18
lt(R column, Object val)
// 大于等于,例如 ge("age", 18) 等价于 age >= 18
ge(R column, Object val)
// 小于等于,例如 le("age", 18) 等价于 age <= 18
le(R column, Object val)
// BETWEEN 值1 AND 值2,例如 between("age", 18, 30) 等价于 age between 18 and 30
between(R column, Object val1, Object val2))
// NOT BETWEEN 值1 AND 值2,例如 notBetween("age", 18, 30) 等价于 age not between 18 and 30
notBetween(R column, Object val1, Object val2)
// LIKE '%值%',例如 like("name", "王") 等价于 name like '%王%'
like(R column, Object val)
// NOT LIKE '%值%',例如 notLike("name", "王") 等价于 name not like '%王%'
notLike(R column, Object val)
// LIKE '%值',例如 likeLeft("name", "王") 等价于 name like '%王'
likeLeft(R column, Object val)
// LIKE '值%',例如 likeRight("name", "王") 等价于 name like '王%'
likeRight(R column, Object val)
// 字段 IS NULL,例如 isNull("name") 等价于 name is null
isNull(R column)
// 字段 IS NOT NULL,例如 isNotNull("name") 等价于 name is not null
isNotNull(R column)
拼接嵌套
// 字段 IN (v0, v1, ...)
in(R column, Object... values) // in("age", 1, 2, 3) 等价于 age in (1,2,3)
// 字段 IN (value.get(0), value.get(1), ...)
in(R column, Collection<?> value) // 例如 in("age",{1,2,3}) 等价于 age in (1,2,3)
// 字段 NOT IN (v0, v1, ...)
notIn(R column, Object... values) // 例如 notIn("age", 1, 2, 3) 等价于 age not in (1,2,3)
// 字段 NOT IN (value.get(0), value.get(1), ...)
notIn(R column, Collection<?> value) // 例如 notIn("age",{1,2,3}) 等价于 age not in (1,2,3)
// 字段 IN ( sql语句 ),同理还有 字段 NOT IN (SQL 语句)
inSql(R column, String inValue) // // 例如:inSql("id", "select id from table where id < 3") 等价于 id in (id < 3)
// 拼接 OR,注意:主动调用or表示紧接着下一个方法不是用and连接!(不调用or则默认为使用and连接)
or(boolean condition) // 例如:eq("id",1).or().eq("name","老王") 等价于 id = 1 or name = '老王'
// OR 嵌套
or(Consumer<Param> consumer) // 例如:or(i -> i.eq("name", "李白").ne("status", "活着")) 等价于 or (name = '李白' and status <> '活着')
// AND 嵌套
and(Consumer<Param> consumer) // 例如:and(i -> i.eq("name", "李白").ne("status", "活着")) 等价于 and (name = '李白' and status <> '活着')
// 正常嵌套 不带 AND 或者 OR
nested(Consumer<Param> consumer) // 例如:nested(i -> i.eq("name", "李白").ne("status", "活着")) 等价于 (name = '李白' and status <> '活着')
// 无视优化规则直接拼接到 sql 的最后,只能调用一次,多次调用以最后一次为准,有sql注入的风险,请谨慎使用
last(String lastSql) // 例如:last("limit 1"),这是唯一的可以在SQL中使用limit语句的,并且只能是QueryWrapper构造器。
last(boolean condition, String lastSql)
// 拼接 EXISTS ( sql语句 )
exists(String existsSql) // 例如:exists("select id from table where age = 1") 等价于 exists (select id from table where age = 1)
// 拼接 NOT EXISTS ( sql语句 )
notExists(String notExistsSql) // 例如:notExists("select id from table where age = 1") 等价于 not exists (select id from table where age = 1)
// 拼接 SQL,该方法可用于数据库函数,动态入参的params对应前面applySql内部的{index}部分,这样是不会有sql注入风险的,反之会有!
apply(String applySql, Object... params)
// 例如:apply("id = 1") 等价于 id = 1
// 例如:apply("date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")等价于 date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")
// 例如:apply("date_format(dateColumn,'%Y-%m-%d') = {0}", "2008-08-08") 等价于 date_format(dateColumn,'%Y-%m-%d') = '2008-08-08'")
分组排序
// 分组:GROUP BY 字段, ...
groupBy(R... columns) // 例如:groupBy("id", "name") 等价于 group by id,name
// 排序:ORDER BY 字段, ... [规则]
orderByAsc(R... columns) // 例如:orderByAsc("id", "name") 等价于 order by id ASC,name ASC
orderByDesc(R... columns) // 例如:orderByDesc("id", "name") 等价于 order by id DESC,name DESC
orderBy(boolean condition, boolean isAsc, R... columns) // 例如:orderBy(true, true, "id", "name") 等价于 order by id ASC,name ASC
// HAVING ( sql语句 )
having(String sqlHaving, Object... params) // 例如:having("sum(age) > 10") 等价于 having sum(age) > 10
// 函数方法,主要方便在出现if...else下调用不同方法能不断链
func(Consumer<Children> consumer) // 例如: func(i -> if(true) {i.eq("id", 1)} else {i.ne("id", 1)})
常用注解
@TableName:表名注解
属性 | 类型 | 描述 | 默认值 | 是否必须指定 |
---|---|---|---|---|
value | String | 表名 | "" | 否 |
schema | String | 模式 | "" | 否 |
resultMap | String | xml 中 resultMap 的 id | "" | 否 |
autoResultMap | Boolean | 是否自动构建 resultMap 并使用,与 resultMap 互斥 | false | 否 |
excludeProperty | String[] | 需要排除的属性名(@since 3.3.1) | {} | 否 |
keepGlobalPrefix | Boolean | 是否保持使用全局的 tablePrefix 的值 | false | 否 |
关于autoResultMap
的说明:
MP 会自动构建一个 ResultMap 并注入到 MyBatis 里,因为 MP 底层是 MyBatis,只会注入常用的 CRUD 到 MyBatis 里,注入之前可以说是动态的(根据你entity的字段以及注解变化而变化),但是注入之后是静态的(等于你写在xml的东西), 而对于直接指定 typeHandler,MyBatis 只支持你写在2个地方:
-
定义在 resultMap 里,只作用于 select 查询的返回结果封装。
-
定义在 insert 和 update SQL 中 #{property} 里的 property 后面(例:#{property,typehandler=xxx.xxx.xxx}),只作用于设置值。而除了这两种直接指定 typeHandler,MyBatis 有一个全局的扫描你自己的 typeHandler 包的配置,这是根据你的 property 的类型去找 typeHandler 并使用。
@TableId:主键注解
属性 | 类型 | 描述 | 默认值 | 是否必须指定 |
---|---|---|---|---|
value | String | 主键字段名 | "" | 否 |
type | Enum | 主键类型 | IdType.NON | 否 |
IdType
值 | 描述 |
---|---|
AUTO | 数据库自增ID |
NONE | 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT) |
INPUT | insert 前自行 set 主键值 |
ASSIGN_ID | 雪花ID,主键类型 Number 或 String,使用接口 IdentifierGenerator 的方法 nextId 实现 |
ASSIGN_UUID | UUID,主键类型为 String,使用接口 IdentifierGenerator 的默认方法 nextUUID 实现 |
@TableField:非主键字段注解
属性 | 类型 | 描述 | 默认值 | 是否必须指定 |
---|---|---|---|---|
value | String | 数据库字段名 | "" | 否 |
exist | boolean | 是否为数据库表字段 | true | 否 |
fill | Enum | 字段自动填充策略 | FieldFill.DEFAULT | 否 |
select | boolean | 是否进行 select 查询 | true | 否 |
FieldFill
值 | 描述 |
---|---|
DEFAULT | 默认不处理 |
INSERT | 插入时填充字段 |
UPDATE | 更新时填充字段 |
INSERT_UPDATE | 插入和更新时填充字段 |
@TableLogic:表字段逻辑删除,@Version:乐观锁注解
注解示例
@TableName("sys_table")
public class Entity {
/**
* 实体主键
*/
@TableId(value = "table_id", type = IdType.AUTO)
private Long configId;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/**
* 乐观锁
*/
@Version
private Integer version;
/**
* 逻辑删除
*/
@TableLogic
private Long delFlag;
}