MyBatis-Plus学习笔记
1. 简介
官网:https://baomidou.com/pages/24112f/
简称为MP,是一个Mybatis的增强工具,在Mybatis的基础上只做增强不做改变,为简化开发,提高效率而生。
对比mybatis,MP减少了sql的书写,之前的mybatis是需要将sql语句写在xml文件中,涉及的操作比较繁琐,而MP基本不用书写简单SQL,开发起来比较方便,而且MP拥有的特性也很受用。
2. 特性
-
无侵入
只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
-
损耗小
启动即会自动注入基本CRUD,性能基本无损耗,直接面向对象操作
-
强大的CRUD操作
内置通用Mapper,通用Service,仅仅通过少量配置即可实现单表大部分CRUD操作,更有强大的条件构造器,满足各类使用需求
-
支持Lambda形式调用
通过Lambda表达式,方便的编写各类查询条件,无需担心字段写错
-
支持主键自动生成
支持多达4种主键策略(内含分布式唯一ID生成器- Sequence),可自由配置,完美解决主键问题
-
支持ActiveRecord模式
支持ActiveRecord形式调用,实体类只需继承Model类即可进行强大的CRUD操作
-
支持自定义全局通用操作
支持全局通用方法注入
-
内置代码生成器
采用代码或者Maven插件可快速生成Mapper,Model,Service,Controller层代码,支持模板引擎,更有超多自定义配置
-
内置分页插件
基于Mybatis物理分页,开发者无需关心具体操作,配置好插件后,写分页等同于普通List查询
-
分页插件支持多种数据库
支持MySQL,Oracle,DB2,H2,HSQL,SQLite,Postgre,SQLServer等多种数据库
-
内置性能分析插件
可输出SQL语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
-
内置全局拦截插件
提供全表delete,update操作只能分析阻断,也可以自定义拦截规则,预防误操作
3. 快速开始
-
首先创建数据表,假设有一张用户表user
CREATE TABLE `user` ( `id` bigint(20) NOT NULL COMMENT '主键ID', `name` varchar(32) DEFAULT NULL COMMENT '姓名', `age` int(11) DEFAULT NULL COMMENT '年龄', `email` varchar(32) DEFAULT NULL COMMENT '邮箱', PRIMARY KEY(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
这里id字段的类型为bigint,是因为mybatis默认的主键是雪花算法计算出来的,比较长超出范围,需要用bigint
-
插入一些预置的数据
INSERT INTO user (id, name, age, email) VALUES (1, 'Jone', 18, '[email protected]'), (2, 'Jack', 20, '[email protected]'), (3, 'Tom', 28, '[email protected]'), (4, 'Sandy', 21, '[email protected]'), (5, 'Billie', 24, '[email protected]');
-
创建Springboot工程,引入依赖
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.13</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.48</version> </dependency>
-
编写配置(数据库使用MySQL,数据源使用Druid)
spring: # 配置数据源信息 datasource: # 配置数据源类型 type: com.alibaba.druid.pool.DruidDataSource # 配置连接数据库信息 driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/mybatisplus?characterEncoding=utf-8&useSSL=false username: root password: root
这里有个要注意的:
-
驱动类(
driver-class-name
)的值- Springboot 2.0或使用的数据库是5.X版本,
driver-class-name
的值为:com.mysql.jdbc.Driver
- Springboot 2.1及以上或数据库是8.x版本,
driver-class-name
的值为:com.mysql.cj.jdbc.Driver
- Springboot 2.0或使用的数据库是5.X版本,
-
连接地址(
url
)的值-
mysql5.x版本的url
jdbc:mysql://localhost:3306/mybatisplus?characterEncoding=utf8&useSSL=false
-
mysql8.x版本的url
jdbc:mysql://localhost:3306/mybatisplus?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false
mysql8多了一个时区的设置
-
-
-
添加实体类User
@Data @NoArgsConstructor @AllArgsConstructor public class User { private Long id; private String name; private Integer age; private String email; }
这里的字段类型,尽量用包装类。
在插入数据时,由于ID是自动生成的,在设置数据时,如果用了基本数据类型long,就得设置为0,就会变成了插入数据的ID了,而包装类,可以设置为null。(个人理解)
-
添加对应UserMapper
public interface UserMapper extends BaseMapper<User> { }
这里UserMapper继承了BaseMapper,这个BaseMapper是MybatisPlus提供的模板Mapper,其中包含了基本的CRUD方法,泛型就是要操作的实体类型。
Mapper 继承该接口后,无需编写 mapper.xml 文件,即可获得CRUD功能
-
启动类加上
MapperScan
@SpringBootApplication @MapperScan("com.zzy.mybatisplus_test.mapper") public class MybatisplusTestApplication { public static void main(String[] args) { SpringApplication.run(MybatisplusTestApplication.class, args); } }
-
编写测试方法
@Autowired private UserMapper userMapper; @Test public void testSelectList() { userMapper.selectList(null).forEach(System.out::println); }
UserMapper中的selectList方法需要一个参数,是MP内置的条件封装器,这里填写null,就是指不需要条件,选择所有
-
测试结果
控制台会输出前面插入的数据
User(id=1, name=Jone, age=18, email=test1@baomidou.com) User(id=2, name=Jack, age=20, email=test2@baomidou.com) User(id=3, name=Tom, age=28, email=test3@baomidou.com) User(id=4, name=Sandy, age=21, email=test4@baomidou.com) User(id=5, name=Billie, age=24, email=test5@baomidou.com)
4. MP的CRUD操作
在操作之前,我们可以配置MP的日志输出,能在控制台显示MP执行的SQL语句,方便Debug与学习。
# 配置Mybatis日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
4.1 使用BaseMapper
在MP中,基本的CRUD在内置的BaseMapper中都已经得到了实现了,我们可以直接使用。
-
Insert
/** * 插入一条记录 * * @param entity 实体对象 */ int insert(T entity);
@Test public void testInsert() { User user = new User(null, "zhangsan", 23, "[email protected]"); int result = userMapper.insert(user); System.out.println("result: " + result); System.out.println("用户id为:" + user.getId()); }
输出数据:
==> Preparing: INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? )
> Parameters: 1546431457684246529(Long), zhangsan(String), 23(Integer), [email protected](String)
< Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2fd9fb34]
result: 1
用户id为:1546431457684246529用户id是MP默认使用雪花算法生成的。
-
Delete
// 根据 entity 条件,删除记录 // wrapper: 实体对象封装操作类,可以为null int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper); // 删除(根据ID 批量删除) // idList:主键ID列表,不能为null以及empty int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList); // 根据 ID 删除 int deleteById(Serializable id); // 根据 columnMap 条件,删除记录 // columnMap: 表字段map对象 int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
// 通过Id删除记录 @Test public void testDeleteById() { int result = userMapper.deleteById(1546431457684246529L); System.out.println("result = " + result); } // 输出结果 ==> Preparing: DELETE FROM user WHERE id=? ==> Parameters: 1546431457684246529(Long) <== Updates: 1 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@687389a6] result = 1
// 根据ID批量删除 @Test public void testDeleteByBatchId() { List<Long> list = Arrays.asList(1L, 2L, 3L); int result = userMapper.deleteBatchIds(list); System.out.println("result = " + result); } // 输出结果 ==> Preparing: DELETE FROM user WHERE id IN ( ? , ? , ? ) ==> Parameters: 1(Long), 2(Long), 3(Long) <== Updates: 3 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4ffced4e] result = 3
// 通过map条件删除 @Test public void testDeleteByMap() { Map<String, Object> map = new HashMap<>(); map.put("age", 24); map.put("name", "Billie"); int result = userMapper.deleteByMap(map); System.out.println("result = " + result); } // 输出结果 ==> Preparing: DELETE FROM user WHERE name = ? AND age = ? ==> Parameters: Billie(String), 24(Integer) <== Updates: 1 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6094de13] result = 1
-
Update
// 根据 whereWrapper 条件,更新记录 int update(@Param(Constants.ENTITY) T updateEntity, @Param(Constants.WRAPPER) Wrapper<T> whereWrapper); // 根据 ID 修改 int updateById(@Param(Constants.ENTITY) T entity);
前面演示了删除的方法,把数据几乎都删光了,这里重新将原始数据添加到数据库,再测试。
// 通过Id修改 @Test public void testUpdateById() { User user = new User(3L, "zhangsan", 20, null); int result = userMapper.updateById(user); System.out.println("result = " + result); } // 输出结果 ==> Preparing: UPDATE user SET name=?, age=? WHERE id=? ==> Parameters: zhangsan(String), 20(Integer), 3(Long) <== Updates: 1 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@bbb6f0] result = 1 // 注意,email设置为null,在执行时并没有去修改email的值的。
-
Select
// 根据 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);
// 根据Id查询 @Test public void testSelectById() { User user = userMapper.selectById(2L); System.out.println("user = " + user); } // 输出结果 ==> Preparing: SELECT id,name,age,email FROM user WHERE id=? ==> Parameters: 2(Long) <== Columns: id, name, age, email <== Row: 2, Jack, 20, test2@baomidou.com <== Total: 1 user = User(id=2, name=Jack, age=20, email=test2@baomidou.com)
// 根据多个Id批量查询 @Test public void testSelectByBatchId() { List<Long> list = Arrays.asList(4L, 5L); List<User> users = userMapper.selectBatchIds(list); System.out.println("users = " + users); } // 输出结果 ==> Preparing: SELECT id,name,age,email FROM user WHERE id IN ( ? , ? ) ==> Parameters: 4(Long), 5(Long) <== Columns: id, name, age, email <== Row: 4, Sandy, 21, test4@baomidou.com <== Row: 5, Billie, 24, test5@baomidou.com <== Total: 2 users = [User(id=4, name=Sandy, age=21, email=test4@baomidou.com), User(id=5, name=Billie, age=24, email=test5@baomidou.com)]
// 通过map条件查询 @Test public void testSelectByMap() { Map<String, Object> map = new HashMap<>(); map.put("age", 21); List<User> users = userMapper.selectByMap(map); System.out.println(users); } // 输出结果 ==> Preparing: SELECT id,name,age,email FROM user WHERE age = ? ==> Parameters: 21(Integer) <== Columns: id, name, age, email <== Row: 4, Sandy, 21, test4@baomidou.com <== Total: 1 [User(id=4, name=Sandy, age=21, email=test4@baomidou.com)]
4.2 使用通用Service
说明:
- 通用Service CRUD封装了
IService
接口,进一步封装CRUD采用get 查询单行
,remove 删除
,list 查询集合
,page 分页
前缀命名的方式区分Mapper
层,避免混淆 - 泛型T为任意实体对象
- 建议如果存在自定义通用Service方法的可能,请创建自己的
IBaseService
继承MP提供的基类
-
IService
MP中有一个接口
IService
和其实现类ServiceImpl
,封装了常见的业务层逻辑(具体翻看源码)Save
// 插入一条记录(选择字段,策略插入) boolean save(T entity); // 插入(批量) boolean saveBatch(Collection<T> entityList); // 插入(批量) boolean saveBatch(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);
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);
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);
-
创建Service接口和实现类
public interface UserService extends IService<User> { } @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { }
-
测试查询记录数
@Test public void testGetCount() { long count = userService.count(); System.out.println("count = " + count); }
-
测试批量插入
@Test public void testSaveBatch() { // SQL 长度有限制,海量数据插入单条SQL无法实行 // 因此MP将批量插入放在了通用Service中实现,而不是通用Mapper ArrayList<User> users = new ArrayList<>(); for (int i = 0; i < 5; i++ ){ User user = new User(); user.setName("abc" + 1); user.setAge(20); users.add(user); } // INSERT INTO t_user1 ( name, age ) VALUES ( ?, ? ) userService.saveBatch(users); }
4.3 条件构造器
在BaseMapper类中的方法,大多数方法中都有带Wrapper
类型的形参,这个Wrapper
就是条件构造器,能够针对SQL语句设置不同的条件。如果没有条件,则可以将该形参赋值为null,即为查询(删除/修改)所有数据
Wrapper的关系树:
Wrapper
:条件构造抽闲类,最顶端父类AbstractWrapper
:用于查询条件封装,生成sql的where条件QueryWrapper
:查询条件封装UpdateWrapper
: 更新条件封装AbstractLambdaWrapper
:使用Lambda语法的条件封装LambdaQueryWrapper
:用于Lambda语法使用的查询WrapperLambdaUpdateWrapper
:用于Lambda语法使用的更新Wrapper
-
QueryWrapper
-
组装查询条件
@Test public void test01() { // 查询用户名包含a,年龄在20-30之间,并且邮箱不为null的用户信息 QueryWrapper<User> wrapper = new QueryWrapper<>(); // 构造的sql:SELECT id,name,age,email FROM user WHERE (name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL) wrapper.like("name", "a") .between("age", 20, 30) .isNotNull("email"); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } // 输出结果 User(id=2, name=Jack, age=20, email=test2@baomidou.com) User(id=3, name=zhangsan, age=20, email=test3@baomidou.com) User(id=4, name=Sandy, age=21, email=test4@baomidou.com)
-
组装排序条件
@Test public void test02() { // 按年龄降序查询用户,如果年龄相同则按id升序排列 QueryWrapper<User> wrapper = new QueryWrapper<>(); // 构造的sql:SELECT id,name,age,email FROM user ORDER BY age DESC,id ASC wrapper.orderByDesc("age") .orderByAsc("id"); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } // 输出结果 User(id=5, name=Billie, age=24, email=test5@baomidou.com) User(id=4, name=Sandy, age=21, email=test4@baomidou.com) User(id=2, name=Jack, age=20, email=test2@baomidou.com) User(id=3, name=zhangsan, age=20, email=test3@baomidou.com) User(id=1, name=Jone, age=18, email=test1@baomidou.com)
-
组装删除条件
@Test public void test03() { // 删除email为空的用户 QueryWrapper<User> wrapper = new QueryWrapper<>(); // 构造的sql:DELETE FROM user WHERE (email IS NULL) wrapper.isNull("email"); int result = userMapper.delete(wrapper); System.out.println("result = " + result); } // 输出结果 result = 0,因为没有用户email是null的
-
条件的优先级
@Test public void test04() { // 将(年龄大于20并且用户名中包含a)或者邮箱为null的用户信息修改 QueryWrapper<User> wrapper = new QueryWrapper<>(); // 构造的sql:UPDATE user SET age=?, email=? WHERE (age >= ? AND name LIKE ? OR email IS NULL) wrapper.ge("age", 20) .like("name", "a") .or() .isNull("email"); User user = new User(); user.setAge(33); user.setEmail("[email protected]"); int update = userMapper.update(user, wrapper); System.out.println("update = " + update); } // 输出结果 update = 3
@Test public void test05() { // 将用户名中包含a并且(年龄大于20或者邮箱为null)的用户信息修改 QueryWrapper<User> wrapper = new QueryWrapper<>(); // 构造的SQL:UPDATE user SET age=?, email=? WHERE (name LIKE ? AND (age >= ? OR email IS NULL)) wrapper.like("name", "a") .and(i -> i.ge("age", 20).or().isNull("email")); User user = new User(); user.setAge(10); user.setEmail("[email protected]"); int update = userMapper.update(user, wrapper); System.out.println("update = " + update); }
主动调用
or
表示接着的下一个方法不是用and
连接(默认是使用and
连接的) -
组装select子句
@Test public void test06() { // 查询用户信息的name,age字段 QueryWrapper<User> wrapper = new QueryWrapper<>(); // 构造的sql:SELECT name,age FROM user wrapper.select("name", "age"); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } // 输出结果 User(id=null, name=Jone, age=18, email=null) User(id=null, name=Jack, age=10, email=null) User(id=null, name=zhangsan, age=10, email=null) User(id=null, name=Sandy, age=10, email=null) User(id=null, name=Billie, age=24, email=null)
-
实现子查询
@Test public void test07() { // 查询id小于等于3的用户信息 QueryWrapper<User> wrapper = new QueryWrapper<>(); // 构建的sql:SELECT id,name,age,email FROM user WHERE (id IN (select id from user where id <= 3)) wrapper.inSql("id", "select id from user where id <= 3"); List<User> list = userMapper.selectList(wrapper); list.forEach(System.out::println); } // 输出结果 User(id=1, name=Jone, age=18, email=test1@baomidou.com) User(id=2, name=Jack, age=10, email=abc@qq.com) User(id=3, name=zhangsan, age=10, email=abc@qq.com)
这里只是测试效果,实际应该不会这样select id
-
-
LambdaQueryWrapper
上面使用
QueryWrapper
中,我们查询时使用的是数据库列名字符串,容易出现写错或与类的属性名不一致的导致写错,MP提供了LambdaQueryWrapper,可以通过获取Lambda数据库列名。@Test public void test08() { // 查询用户名包含a,年龄在10-20之间,并且邮箱不为null的用户信息 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); // SELECT id,name,age,email FROM user WHERE (name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL) wrapper.like(User::getName, "a") .between(User::getAge, 10, 20) .isNotNull(User::getEmail); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); }
使用LambdaQueryWrapper后,不用直接写列名字符串形式,可以通过类获取
-
UpdateWrapper
前面的测试例子中也有用QueryWrapper实现update的,使用
UpdateWrapper
可以使用set
方法直接设置,不需要创建User对象,如果创建了User对象,并且设置属性,传递给了update方法,那么设置列会加入到sql中。@Test public void test09() { // 将(年龄大于20或邮箱为null)并且用户名中包含有a的用户信息修改 UpdateWrapper<User> updateWrapper = new UpdateWrapper<>(); // UPDATE user SET age=?,email=? WHERE (name LIKE ? AND (age >= ? OR email IS NULL)) updateWrapper.set("age", 18) .set("email", "[email protected]") .like("name", "a") .and(i -> i.ge("age", 20).or().isNull("email")); int update = userMapper.update(null, updateWrapper); System.out.println("update = " + update); }
-
LambdaUpdateWrapper
将上面的测试例子用Lambda的方式来编写
@Test public void test09() { // 将(年龄大于20或邮箱为null)并且用户名中包含有a的用户信息修改 LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.set(User::getAge, 38) .set(User::getEmail, "[email protected]") .like(User::getName, "a") .and(i -> i.ge(User::getAge, 10).or().isNull(User::getEmail)); User user = new User(); user.setName("Billiea"); int update = userMapper.update(user, updateWrapper); System.out.println("update = " + update); }
-
总结
QueryWrapper
、QueryChainWrapper
只能指定需要的数据库列名LambdaQueryWrapper
、LambdaQueryChainWrapper
可以通过Lambda获取数据库列名QueryWrapper
、LambdaQueryWrapper
不能使用链式查询的方式,必须借助BaseMapper来执行QueryChainWrapper
、LambdaQueryChainWrapper
可以使用链式查询的方式,如list(),one()
4.4 condition
在真正开发过程中,组装条件时,有些数据是来源于用户输入的,是可选的,因此在组装条件之前是需要先判断条件是否成立的,成立才组装条件到SQL执行。
MP在条件构造器的方法中提供了带condition参数的方法
未使用带condition参数的方法时的操作:
@Test
public void test10() {
// 定义查询条件,有可能为null,假设输入数据如下
String name = null;
Integer ageBegin = 10;
Integer ageEnd = 24;
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(name)) {
queryWrapper.like(User::getName, "a");
}
if (ageBegin != null) {
queryWrapper.ge(User::getAge, ageBegin);
}
if (ageEnd != null) {
queryWrapper.le(User::getAge, ageEnd);
}
// SELECT id,name,age,email FROM user WHERE (age >= ? AND age <= ?)
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
}
使用带condition参数的方法:
@Test
public void test11() {
// 定义查询条件,有可能为null,假设输入数据如下
String name = null;
Integer ageBegin = 10;
Integer ageEnd = 24;
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(StringUtils.isNotBlank(name), User::getName, name)
.ge(ageBegin != null, User::getAge, ageBegin)
.le(ageEnd != null, User::getAge, ageEnd);
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
}
5. 常用注解
-
@TableName
在以上的测试中,我们使用MP进行CRUD时,并没有指定要操作的表,只是在UserMapper接口继承BaseMapper时,设置了泛型为User,但实际操作的表是User表。
因此可以得出结论,MP在确定操作的表时,是由BaseMapper的泛型决定的,即实体类型决定,而且默认操作的表名与实体类的类名一致。
@TableName
的作用是标识实体类对应的表,作用在实体类上。可以做如下测试:将数据库的表user改名为t_user,运行前面的测试程序,报错如下
org.springframework.jdbc.BadSqlGrammarException: ### Error querying database. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'mybatisplus.user' doesn't exist ### The error may exist in com/zzy/mybatisplus_test/mapper/UserMapper.java (best guess) ### The error may involve defaultParameterMap ### The error occurred while setting parameters ### SQL: SELECT id,name,age,email FROM user WHERE (age >= ? AND age <= ?) ### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'mybatisplus.user' doesn't exist ; bad SQL grammar []; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'mybatisplus.user' doesn't exist
解决方法:因为当前实体类为User,数据库表为t_user,二者名字不匹配,因此报错,可以在User类上添加注解
@TableName("t_user")
标识User类与表t_user匹配@Data @NoArgsConstructor @AllArgsConstructor @TableName("t_user") public class User { private Long id; private String name; private Integer age; private String email; }
上面我们只是改了一个表,如果所有的实体类对应的表都有固定的前缀,比如t_,我们总不能手动一个个的添加@TableName注解吧,那得累死
为了解决这种情况,我们可以通过MP全局配置,为实体类所对应的表名设置默认的前缀:
# 配置Mybatis日志 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: # 配置MP操作表的默认前缀 table-prefix: t_
-
@TableId
经过以上测试,MP在实现CRUD时,会默认将id作为主键列,并在插入数据时,默认基于雪花算法的策略生成id
问题:如果实体类和表中表示主键的不是id,而是其他字段,比如uid,那MP还会自动识别uid为主键列吗?
测试:将实体类中属性id改为uid,表中字段id也改为uid,测试添加功能
### SQL: INSERT INTO t_user1 ( name, age, email ) VALUES ( ?, ?, ? ) ### Cause: java.sql.SQLException: Field 'uid' doesn't have a default value ; Field 'uid' doesn't have a default value; nested exception is java.sql.SQLException: Field 'uid' doesn't have a default value
解决方法:通过
@TableId
将uid属性标识为主键public class User { @TableId private Long uid; private String name; private Integer age; private String email; }
若实体类中主键对应的属性为id,而表中表示主键的字段为uid,此时如果只在属性上添加注解
@TableId
,则会抛出异常Unknown column 'id' in 'field list'
,也就是MP仍然会将id作为表的主键操作,而表中表示主键的是字段uid。此时就需要通过@TableId的value属性了,指定表中主键字段:
@TableId("uid")
或@TableId(value="uid")
@TableId的type属性
常用的主键策略:
-
IdType.ASSIGN_ID
(默认)基于雪花算法的策略生成数据id,与数据库id是否设置自增无关
-
IdType.AUTO
使用数据库的自增策略,注意,该类型请确保数据库设置了id自增,否则无效
配置全局主键策略:
# 配置Mybatis日志 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: # 配置MP操作表的默认前缀 table-prefix: t_ # 配置MP的主键策略 id-type: auto
-
-
@TableField
经过以上的测试,我们可以发现,MP在执行SQL语句时,要保证实体类中的属性名和表中的字段名一致
如果实体类中的属性名和字段名不一致,会产生什么问题?
-
如果实体类中属性使用的是驼峰命名风格,而表中字段使用的是下划线命名风格,比如实体类属性为userName,表中字段为user_name.
这种情况MP会自动将下划线命名的风格转化为驼峰命名风格
-
如果实体类中的属性和表中的字段不满足第一个情况,比如实体类属性是name,表中字段为username
这种情况需要在实体类上添加注解
@TableField("username")
设置属性对应的字段名
-
-
@TableLogic
逻辑删除:假删除,就是新增一个字段用来表示删除状态,在数据库中仍旧可以看到这条记录,不是真正的删除,但是对于MP来说像是删除了的,无法使用CRUD再操作这条数据。
实现逻辑删除:
- 数据库中创建逻辑删除状态列,设置默认值为0
- 实体类中添加逻辑删除属性,添加注解@TableLogic
6.插件
-
分页插件
MP自带分页插件,只需要简单配置就可以实现分页功能了。
-
添加配置类
@Configuration @MapperScan("com.zzy.mybatisplus_test.mapper") // 可以将主类中Mapper的注解移到这里 public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); // 数据类型为DbType.MYSQL mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return mybatisPlusInterceptor; } }
-
测试
@Test public void testPage() { // 设置分页参数,通过分页参数来确定数据 Page<User> page = new Page<User>(1, 3); userMapper.selectPage(page, null); // 获取分页数据 List<User> list = page.getRecords(); list.forEach(System.out::println); System.out.println("当前页: " + page.getCurrent()); System.out.println("每页显示的条数: " + page.getSize()); System.out.println("总记录数: " + page.getTotal()); System.out.println("总页数: " + page.getPages()); System.out.println("是否有上一页:" + page.hasPrevious()); System.out.println("是否有下一页: " + page.hasNext()); }
使用XML自定义分页
-
UserMapper中定义接口方法
/** * 根据年龄查询用户列表,分页显示 * @param page 分页对象,xml中可以从里面进行取值,传递参数Page即自动分页,必须放在第一位 * @param age 年龄 * @return */ Page<User> selectPageVo(@Param("page") Page<User> page, @Param("age") Integer age);
-
UserMapper.xml中编写SQL
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.zzy.mybatisplus_test.mapper.UserMapper"> <sql id="BaseColumns">uid, name, age, email</sql> <select id="selectPageVo" resultType="User"> select <include refid="BaseColumns"/> from t_user1 where age > #{age} </select> </mapper>
-
测试
@Test public void testSelectPageVo() { // 设置分页参数 Page<User> page = new Page<>(1, 3); userMapper.selectPageVo(page, 20); // 获取分页数据 List<User> list = page.getRecords(); list.forEach(System.out::println); System.out.println("当前页: " + page.getCurrent()); System.out.println("每页显示的条数: " + page.getSize()); System.out.println("总记录数: " + page.getTotal()); System.out.println("总页数: " + page.getPages()); System.out.println("是否有上一页:" + page.hasPrevious()); System.out.println("是否有下一页: " + page.hasNext()); }
-
-
乐观锁插件
-
悲观锁与乐观锁的理解可以看看这个文章:https://my.oschina.net/hanchao/blog/3057429
-
乐观锁实现方式:(当要更新一条记录的时候,希望这条记录没有被别人更新)
-
取出记录时,获取当前version
-
更新时,带上这个version
-
执行更新时,set version = newVersion where version = oldVersion
-
如果version不对,就更新失败
-
-
模拟冲突场景
有一件商品成本80元,售价100元,老板先让小李将商品价格提高50元,小李没有及时操作,一小时后,老板觉得商品售价150元太高,就又让小王去将价格降低30元。
老板的想法是100 -> 150 -> 120
但是这时候小李和小王同时操作系统,取出的商品价格都是100元,然后小李加了50元,将150存到了数据库;小王减掉30元,将70存入数据库.
如果没有锁,小李的操作就会被小王覆盖了,最终可能价格变成了70元,导致老板亏钱
新增一张商品表
CREATE TABLE t_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 t_product (id, NAME, price) VALUES (1, '笔记本', 100);
测试(未使用乐观锁):
@Test public void testConCurrentUpdate() { // 1. 小李 Product p1 = productMapper.selectById(1L); System.out.println("小李取出的价格:" + p1.getPrice()); // 2. 小王 Product p2 = productMapper.selectById(1L); System.out.println("小王取出的价格: " + p2.getPrice()); // 3. 小李将价格增加50元,存入了数据库 p1.setPrice(p1.getPrice() + 50); int ret1 = productMapper.updateById(p1); System.out.println("小李修改结果:" + ret1); // 4. 小王将价格减了30元,存入了数据库 p2.setPrice(p2.getPrice() - 30); int ret2 = productMapper.updateById(p2); System.out.println("小王修改结果:" + ret2); // 最后的结果 Product p3 = productMapper.selectById(1L); System.out.println("最后的价格是:" + p3.getPrice()); } // 最终输出的价格变为70
-
乐观锁配置:
-
配置插件
@Configuration @MapperScan("com.zzy.mybatisplus_test.mapper") public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); // 数据类型为DbType.MYSQL mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 乐观锁插件 mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return mybatisPlusInterceptor; } }
-
实体类字段加上
@Version
注解模拟冲突:
@Data @NoArgsConstructor @AllArgsConstructor public class Product { private Integer id; private String name; private Integer price; @Version private Integer version; }
-
-
测试(使用乐观锁后的测试)
@Test public void testConcurrentVesionUpdate() { // 小李取数据 Product p1 = productMapper.selectById(1L); // 小王取数据 Product p2 = productMapper.selectById(1L); // 小李修改 + 50 p1.setPrice(p1.getPrice() + 50); int result = productMapper.updateById(p1); System.out.println("小李修改的结果:" + result); // 小王修改 - 30 p2.setPrice(p2.getPrice() - 30); int ret = productMapper.updateById(p2); System.out.println("小王修改的结果:" + ret); if (ret == 0) { // 失败重试,重新获取version并更新 p2 = productMapper.selectById(1L); p2.setPrice(p2.getPrice() - 30); ret = productMapper.updateById(p2); } System.out.println("小王修改重试的结果:" + ret); // 老板看价格 Product p3 = productMapper.selectById(1L); System.out.println("老板看价格:" + p3.getPrice()); }
-
7.通用枚举
表中有些字段值是固定的,比如性别,只有男或女,此时我们可以用MP的通用枚举来实现
-
数据库表增加一个字段sex
-
创建通用枚举类型
@Getter public enum SexEnum { MALE(1, "男"), FEMALE(2, "女"); @EnumValue private Integer sex; private String sexName; SexEnum(Integer sex, String sexName) { this.sex = sex; this.sexName = sexName; } }
-
配置扫描通用枚举
# 配置Mybatis日志 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: # 配置MP操作表的默认前缀 table-prefix: t_ # 配置MP的主键策略 id-type: auto type-aliases-package: com.zzy.mybatisplus_test.pojo # 配置扫描通用枚举 type-enums-package: com.zzy.mybatisplus_test.enums
-
测试
@Test public void testSexEnum() { User user = new User(); user.setName("Enum"); user.setAge(20); // 设置性别信息为枚举项,会将@EnumValue注解所标注的属性值存储到数据库 user.setSex(SexEnum.FEMALE); userMapper.insert(user); }
8.多数据源
适用于多种场景:纯粹多库,读写分离,一主多从,混合模式等
接下来模拟一个纯粹多库的场景:
创建两个库,分别为:mybatisplus(上面的库不动)与mybatisplus_1(新建),将mybatis_plus库的t_product表移动到mybatisplus_1库,这样每个库一张表,通过一个测试用例分别获取用户数据与商品数据,如果获取到说明多库模拟成功。
-
创建数据库及表
CREATE DATABASE `mybatisplus_1`; use `mybatisplus_1`; CREATE TABLE t_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 t_product (id, NAME, price) VALUES (1, '笔记本', 100); # 删除mybatisplus库的t_product表 use mybatisplus; DROP TABLE IF EXISTS t_product;
-
引入依赖
<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.5.1</version> </dependency>
-
配置多数据源
将之前的数据库连接配置注释掉,添加新的配置
spring: # 配置数据源信息 datasource: dynamic: # 设置默认的数据源或者数据源组,默认值即为master primary: master # 严格匹配数据源,默认false.true未匹配到指定数据源时抛异常,false使用默认数据源 strict: false datasource: master: url: jdbc:mysql://localhost:3306/mybatisplus?characterEncoding=utf-8&useSSL=false driver-class-name: com.mysql.jdbc.Driver username: root password: root slave_1: url: jdbc:mysql://localhost:3306/mybatisplus_1?characterEncoding=utf-8&useSSL=false driver-class-name: com.mysql.jdbc.Driver username: root password: root
-
创建用户Service
在Service上添加注解
@DS
,指定数据源public interface UserService extends IService<User> { } @Service @DS("master") public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { }
-
创建商品Service
public interface ProductService extends IService<Product> { } @DS("slave_1") @Service public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService { }
-
测试
@Test public void testDynamicDataSource() { // User user = userService.getById(1L); User user = userMapper.selectById(2L); System.out.println(user); System.out.println(productService.getById(1L)); }
注意:
这里在测试时候遇到了一点bug,使用Mybatis-plus 3.5.2版本的时候,从数据库查询得到的枚举不会转换,一直报错:
org.springframework.jdbc.UncategorizedSQLException: Error attempting to get column ‘sex’ from result set. Cause: java.sql.SQLException: Error
; uncategorized SQLException; SQL state [null]; error code [0]; Error; nested exception is java.sql.SQLException: Error一直查找问题,最后查看了这个issue:https://github.com/baomidou/mybatis-plus/issues/4338,使用3.4.3,3.5.1,3.5.3版本都报错,只有将mybatis-plus版本退回3.4.2才可以正常运行。