文章目录
1. 概念
是MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,简化开发提高效率。
特性:
无侵入,损耗小,强大的 CRUD 操作,支持 Lambda 形式调用,
支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强 大的 CRUD 操作
支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用 (MyBatis的逆向工程只能生成Mapper,Medel)
内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询,分页插件支持多种数据库
内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作
2. 入门案例
-
建立测试表user
CREATE DATABASE `mybatis_plus` use `mybatis_plus`; CREATE TABLE `user` ( `id` bigint(20) NOT NULL COMMENT '主键ID', # 注意是bigint `name` varchar(30) DEFAULT NULL COMMENT '姓名', `age` int(11) DEFAULT NULL COMMENT '年龄', `email` varchar(50) DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-
创建SpringBoot项目
mysql驱动,lombok,mybatis-plus
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.25</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!-- 引入 MyBatis-Plus 之后请不要再次引入 MyBatis 以及 MyBatis-Spring,以避免因版本差异导致的问题--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency>
-
配置数据源
spring: datasource: type: com.zaxxer.hikari.HikariDataSource # Spring默认的数据源 driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/mybatis_plus username: root password: 9527
-
创建实体类User
@Data @AllArgsConstructor @NoArgsConstructor public class User { private Long id; // 注意是Long,MyBtisPlus使用雪花算法算的id private String name; private Integer age; private String email; }
-
创建Mapper接口
// BaseMapper是MyBatisPlus提供的(里面对单表的增删查改方法),泛型是要操作的实体类的类型 public interface UserMapper extends BaseMapper<User> { }
-
在SpringBoot启动类上面添加Mapper扫描
@MapperScan("com.sutong.mapper") // 用于扫描Mapper接口,或者在每个Mapper上加上@Mapper注解
-
测试
@SpringBootTest public class MyBatisPlusTest { @Resource UserMapper userMapper; @Test public void test01() { // 参数里面是查询条件,没有则填null就是查询全部 List<User> users = userMapper.selectList(null); System.out.println(users); } }
我们没有写sql,也没给MyBatisPlus说操作那个表,也没有说字段和那个属性映射,只是指定了一个泛型就可以使用!
如果想要看sql语句则下面这样配置(日志功能):
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 输出sql信息
上面测试的sql语句:SELECT id,name,age,email FROM user,则默认操作表是实体类的名称,查询字段则是属性名
MySQL新老版本区别⭐
1、驱动类
spring boot 2.0(内置jdbc5驱动), spring boot 2.1及以上(内置jdbc8驱动)
driver-class-name:
com.mysql.jdbc.Driver
,driver-class-name:
com.mysql.cj.jdbc.Driver
否则运行测试用例的时候会有 WARN 信息2、连接地址url
MySQL5.7版本的url:
jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&useSSL=false
MySQL8.0版本的url:
jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false
如果上面设置还是相差八小时的话使用:serverTimezone=Asia/Shanghai
3. CRUD
Ⅰ BaseMapper
方法:
int insert(T entity);
int deleteById(Serializable id);
int deleteById(T entity);
int deleteByMap(@Param("cm") Map<String, Object> columnMap);
int delete(@Param("ew") Wrapper<T> queryWrapper);
int deleteBatchIds(@Param("coll") Collection<?> idList);
int updateById(@Param("et") T entity);
int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);
T selectById(Serializable id);
List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);
List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);
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("...", 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;
}
Long selectCount(@Param("ew") Wrapper<T> queryWrapper); // 查询符合条件的数据数量
List<T> selectList(@Param("ew") Wrapper<T> queryWrapper); // 根据条件构造器查询数据,封装为List
// 根据条件查询数据,每条封装为Map,整体是个List
List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);
List<Object> selectObjs(@Param("ew") Wrapper<T> queryWrapper); // 查询多个
// 分页的
<P extends IPage<T>> P selectPage(P page, @Param("ew") Wrapper<T> queryWrapper);
<P extends IPage<Map<String, Object>>> P selectMapsPage(P page, @Param("ew") Wrapper<T> queryWrapper);
Ⅱ 插入 删除 修改
@Test
public void test02() {
// sql:INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? )
User su = new User(null, "su", 18, "su@qq.com");
int res = userMapper.insert(su);
System.out.println(res + " " + su.getId()); // id很长(1499732145958776834)
}
@Test
public void test03() {
userMapper.deleteById(1L); // 如果很大的话需要1499732145958776834L这样!Long类型
userMapper.deleteById(new User(1499732145958776834L, null, null, null));
userMapper.deleteBatchIds(Arrays.asList(1L, 2L)); // sql:... WHERE id IN (?, ?)
Map<String, Object> map = new HashMap<>();
map.put("name", "su");
map.put("age", 18);
userMapper.deleteByMap(map); // 删除名字是su的,年龄是18的
// 带条件的下面讲!!
}
@Test
public void test04() {
// userMapper.update(); // 第一个是数据,第二是条件Wrapper(后面讲)
userMapper.updateById(new User(1L, "tong", null, "tong@qq.com")); // 通过id修改, age不修改
}
Ⅲ 查询
@Test
public void test05() {
User user = userMapper.selectById(1L);
List<User> users01 = userMapper.selectBatchIds(Arrays.asList(1L, 2L)); // sql: ... WHERE id INT(?,?)
Map<String, Object> map = new HashMap<>();
map.put("name", "su");
map.put("age", 18);
List<User> users02 = userMapper.selectByMap(map); // map的k-v作为条件查询,和delete一样
List<User> users03 = userMapper.selectList(null);
}
Ⅳ 拓展
如果BaseMapper的方法不满意,我们可以自定义,和MyBatis的用法一样的
指定Mapper映射文件的路径:(Mapper映射文件的名字要和Mapper接口名一样)
mybatis-plus:
mapper-locations: classpath:mapper/**/*.xml # 默认就是classpath*:/mapper/**/*.xml,mapper目录下还可以有目录
接口:
public interface UserMapper extends BaseMapper<User> {
// 根据id查询用户信息为一个Map集合 k-v 列名-值
Map<String, Object> selectMapById(Long id);
}
映射文件:
<mapper namespace="com.sutong.mapper.UserMapper">
<select id="selectMapById" resultMap="map">
select id,name,age,email from user where id = #{id}
</select>
</mapper>
测试:
@Test
public void test06() {
System.out.println(userMapper.selectMapById(1L));
// {name=Jone, id=1, age=18, email=test1@baomidou.com}
}
Ⅴ 通用Service
- 通用 Service CRUD 封装
IService
接口,进一步封装 CRUD 采用save
保存,get
查询单行,remove
删除,list
查询集合,page
分页 前缀命名方式区分 Mapper 层避免混淆- 泛型
T
为任意实体对象,对象Wrapper
为 条件构造- 建议如果存在自定义通用 Service 方法的可能,请创建自己的
IBaseService
继承Mybatis-Plus
提供的基类
IService
-> ServiceImp
(实际中不建议直接使用IServiceImp因为无法满足我们的业务)
public interface IService<T> {}
public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {}
使用⭐:
// 建议自己创建一个Service,这样既可以使用MyBatisPlus的,也可以自定义方法!!
public interface UserService extends IService<User> {
}
// ServiceImpl第一个泛型是要操作Mapper接口的类型,第二个实体类的类型
// 这样UserServiceImp就能用MyBatisPlus提供的通过Service方法了!!!
@Service
public class UserServiceImp extends ServiceImpl<UserMapper, User> implements UserService {
}
测试:
@SpringBootTest
public class ServiceTest {
@Autowired
private UserService userService;
@Test
public void test01() {
long count = userService.count(); // 查询总记录数 SELECT COUNT( * ) FROM user
List<User> users = new ArrayList<>();
// INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? )
// 是根据单条添加进行批量添加的,每1000条提交一次
users.add(new User(null, "11", 20, "11@qq.com"));
users.add(new User(null, "22", 10, "22@qq.com"));
userService.saveBatch(users); // BaseMapper里面没有批量添加,在Service才有
// saveOrUpdateBatch 是根据id有没有,进行添加或者修改
}
}
4. 注解
Ⅰ @TableName
如果数据库的表名和实体类名不一样,怎么办?
-
使用
@TableName
@TableName("t_user") // 设置实体类所对应的表名 public class User { private Long id; private String name; private Integer age; private String email; }
-
全局配置
可以配置实体类对应数据表名的前缀(就不用每个实体类都加注解的)
mybatis-plus: global-config: db-config: table-prefix: t_
Ⅱ @TableId
MyBatisPlus默认是以id作为主键,如果数据库中uid是主键怎么办? @TableId
public class User {
@TableId //将当前属性对应的字段指定为主键
private Long uid;
private String name;
private Integer age;
private String email;
}
-
value属性,如果实体类中叫id,数据库中叫uid,怎么映射?
public class User { @TableId(value = "uid") // 用于指定主键的字段 private Long id; ... }
-
type属性(表示主键生成的策略,默认是雪花算法)
如果想要使用MySql是主键自动递增(首先要在数据库设置主键自动递增):
public class User { @TableId(type = IdType.AUTO) // 再指定主键自动生成策略 (ASSIGN_ID代表雪花算法,与数据库递增无关) private Long id; ... }
递增添加的时候则不为id赋值。而雪花算法是先生成id,再为id字段赋值!
全局配置
mybatis-plus: global-config: db-config: id-type: auto
Ⅲ @TableField
其他字段的字段名和实体类的属性名不对应时,怎么办?
当数据库是下划线命名法,属性是驼峰命名法,我们不需要使用这个,自动帮我们映射!!!
当命名完全没关系时,则可以使用下面的注解:
@TableField("user_name") // 指定属性对应的字段名
private String name;
@TableField(exist = false) // 这个字段在表中不存在,否则就会报错了!
private String password;
Ⅳ @TableLogic
- 物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除的数据
- 逻辑删除:假删除,将对应数据中代表是否被删除字段的状态修改为“被删除状态”,之后在数据库 中仍旧能看到此条数据记录
- 使用场景:可以进行数据恢复
1.表中添加一个字段,is_delete
默认0,没被删除
2.实体类中也添加上
public class User {
...
@TableLogic // 代表是逻辑删除的字段
private Integer isDelete;
}
3.再次执行通用service的删除方法就是逻辑删除
@Test
public void test02() {
// UPDATE user SET is_delete=1 WHERE id=? AND is_delete=0
// 将@TableLogic标注的字段修改为1
System.out.println(userService.removeById(1)); // 不再是删除,而是修改sql(MyBatisPlus自动帮我们转化操作)
// sql: SELECT id,name,age,email,is_delete FROM user WHERE id=? AND is_delete=0
User user = userService.getById(1); // null,是查不到的
}
雪花算法
数据库分表(需要选择合适的方案去应对数据规模的增长)
-
垂直拆分:将不重要的字段和占了大量空间的拆分掉
-
水平拆分:适合表行数特别大的表,例如超过 5000 万就要分表
水平分表相比垂直分表,会引入更多的复杂性,例如要求全局唯一的数据id该如何处理
-
主键递增
1 ~ 999999 放到表 1中, 1000000 ~ 1999999 放到表2中,以此类推。
优点:可以随着数据的增加平滑地扩充新的表。 缺点:分布不均匀。复杂点:分段大小的选取。
-
取模
假如有 10 个数据库表,用 user_id % 10 的值来表示数据所属的数据库表编号,ID 为 985 的用户放到编号为 5 的子表中。
优点:表分布比较均匀。 缺点:扩充新的表很麻烦,所有数据都要重分布。复杂点:初始表数量的确定。
-
雪花算法
雪花算法是由Twitter公布的分布式主键生成算法,它能够保证不同表的主键的不重复性,以及相同表的主键的有序性。
优点:整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞,并且效率较高!!
-
雪花算法核心思想(了解):
长度共64bit(一个long型)。
首先是一个符号位,1bit标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负 数是1,所以id一般是正数,最高位是0。
41bit时间截(毫秒级),存储的是时间截的差值(当前时间截 - 开始时间截),结果约等于69.73年。
10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID,可以部署在1024个节点)。
12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID)。
5. 条件构造器
继承逻辑:
Wrapper : 条件构造抽象类,最顶端父类
- AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
- QueryWrapper : 查询条件封装
- UpdateWrapper : Update 条件封装
- AbstractLambdaWrapper : 使用Lambda 语法
- LambdaQueryWrapper :用于Lambda语法使用的查询Wrapper
- LambdaUpdateWrapper : Lambda 更新封装Wrapper
QueryWrapper 查询和删除(修改也能用)
UpdateWrapper 修改
Ⅰ QueryWrapper
测试BaseMapper的方法:
简单的:
// 组装查询条件
@Test
public void test01() {
// 查询用户名包含t,年龄再20-30,emil不为空的信息 (column参数是字段名,并不是属性名)
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like("name", "t").between("age", 20, 30).isNotNull("email");
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
// SELECT id,name,age,email,is_delete FROM user
// WHERE is_delete=0 AND (name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL)
// Parameters: %t%(String), 20(Integer), 30(Integer)
}
// 组装排序条件
@Test
public void test02() {
// 查询用户,按照age降序排序,age相同则按照id升序
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc("age").orderByAsc("id");
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
// SELECT id,name,age,email,is_delete FROM user WHERE is_delete=0 ORDER BY age DESC,id ASC
}
// 组装删除条件
@Test
public void test03() {
// 删除email为null的用户
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.isNull("email");
int line = userMapper.delete(queryWrapper);
}
复杂一点:
// 组装修改条件!!!!
@Test
public void test04() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 将 (name包含a并且age大于20) 或者 email为空的信息进行修改 (不调用or则默认为使用and连接)
queryWrapper.like("name", "a")
.gt("age", 20)
.or()
.isNull("email");
// 将 name包含a 并且 (age大于20或者email为空) 的信息进行修改
// and()可传入一个Consumer(or也有),消费者模型,消费者的参数就是条件构造器,Lambda表达式中的条件优先执行
queryWrapper.like("name", "a")
.and(i -> {
i.gt("age", 20).or().isNull("email");
});
User user = new User(null, "小明", null, "ming@qq.com", 0); // 修改user不为空的字段
int line = userMapper.update(user, queryWrapper); // 第一个参数用户填充,第二个是要修改的条件
// 第一个:UPDATE user SET name=?, email=?
// WHERE is_delete=0 AND (age > ? AND name LIKE ? OR email IS NULL)
// 第二个:UPDATE user SET name=?, email=?
// WHERE is_delete=0 AND (name LIKE ? AND (age > ? OR email IS NULL))
}
查询部分字段:
// 组装select字句
@Test
public void test05() {
// 查询age大于20用户的的name,email信息
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.gt("age", 20).select("name", "email");
List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
maps.forEach(System.out::println);
// SELECT name,email FROM user WHERE is_delete=0 AND (age > ?)
}
子查询:
@Test
public void test06() {
// 查询id小于100的的用户信息(可以不使用子查询)
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.inSql("id", "select id from user where id <= 100");
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
// SELECT id,name,age,email,is_delete FROM user
// WHERE is_delete=0 AND (id IN (select id from user where id <= 100))
}
com.baomidou.mybatisplus.core.toolkit.StringUtils; 中isNotBlank()判断字符串是否为null,空字符串,空白符。Spring中也有StringUtils工具类
组装前判断,condition
参数:
在实际中,我们要根据用户浏览器发来的参数,进行组装条件,组装前要对每个参数进行条件判断(下面是判断简单的方法)
@Test
public void test07() {
String username = "a"; Integer ageBegin = 30; Integer ageEnd = 50; // 用户请求参数信息
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like(StringUtils.isNotBlank(username), "name", username) // 第一个参数是condition布尔类型
.ge(ageBegin != null, "age", ageBegin)
.le(ageEnd != null, "age", ageEnd);
List<User> users = userMapper.selectList(queryWrapper);
}
Ⅱ UpdateWrapper
@Test
public void test08() {
// 将 name包含a 并且 (age大于20或者email为空) 的信息进行修改
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>(); // UpdateWrapper!!
// 修改条件
updateWrapper.like("name", "a")
.and(i -> {
i.gt("age", 20).or().isNull("email");
});
// 修改数据
updateWrapper.set("name", "小白").set("email", "bai@qq.com");
userMapper.update(null, updateWrapper); // 修改不使用实体类对象了,更简单一点
// UPDATE user SET name=?,email=?
// WHERE is_delete=0 AND (name LIKE ? AND (age > ? OR email IS NULL))
}
Ⅲ LambdaQueryWrapper
上面我们的字段名也容易写错,所以有了LambdaQueryWrapper
和上面不同的是,字段那个字符串参数,变成了一个函数式接口,SFunction
public interface Function<T, R> { R apply(T t); } public interface SFunction<T, R> extends Function<T, R>, Serializable { }
T就是实体类的类型,通过这个就可以直接拿到实体类属性所对应的字段名!!
自动获取属性所对应的字段名来作为组装sql的字段!!(使用@TableField注解时会自动去映射字段名)
测试:
@Test
public void test09() {
String username = "a"; Integer ageBegin = 30; Integer ageEnd = 50;
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
// 方法引用:第一个参数作为调用者, 类::非静态方法 !!!
lambdaQueryWrapper.like(StringUtils.isNotBlank(username), User::getName, username)
.ge(ageBegin != null, User::getAge, ageBegin)
.le(ageEnd != null, User::getAge, ageEnd);
List<User> users = userMapper.selectList(lambdaQueryWrapper);
// SELECT id,name,age,email,is_delete FROM user
// WHERE is_delete=0 AND (name LIKE ? AND age >= ? AND age <= ?)
}
Ⅳ LambdaUpdateWrapper
@Test
public void test10() {
// 将 (name包含a并且age大于20) 或者 email为空的信息进行修改
LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
// 修改条件
lambdaUpdateWrapper.like(User::getName, "a")
.and(i -> {
i.gt(User::getAge, 20).or().isNull(User::getEmail);
});
// 设置数据也能用Lambda方式
lambdaUpdateWrapper.set(User::getName, "小白").set(User::getEmail, "bai@qq.com");
userMapper.update(null, lambdaUpdateWrapper);
// UPDATE user SET name=?,email=?
// WHERE is_delete=0 AND (name LIKE ? AND (age > ? OR email IS NULL))
}
6. 插件
Ⅰ 分页插件
MyBatis Plus自带分页插件,只要简单的配置即可实现分页功能!!
-
配置类
@Configuration public class MyBatisPlusConfig { // @return MybatisPlusInterceptor是配置MyBatis中的插件的 @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加插件。分页插件的有参构造最后加上数据库类型,因为不同的数据库分页功能实现不同! interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }
-
测试
@Test public void test01() { // 泛型是实体类类型,Page<T> implements IPage<T>。参数:current是当前第几页,size是每页显示的数据数 Page<User> page = new Page<>(2, 3); userMapper.selectPage(page, null); // 查询所有数据的分页数据,该方法返回还是Page,数据都在这里面 List<User> users = page.getRecords(); // 当前页的数据 page.getCurrent(); page.getSize(); long pageNum = page.getPages(); // 总页数 long total = page.getTotal(); // 总记录数 page.hasNext(); page.hasPrevious(); // 是否有下一条/上一条数据 // SELECT id,name,age,email,is_delete FROM user WHERE is_delete=0 LIMIT ?,? // Parameters: 3(Long), 3(Long) // limit后面第一个占位是当前页的起始索引 = (current - 1) * size,所以两个参数都是3 }
自定义分页功能
Mapper接口:
public interface UserMapper extends BaseMapper<User> {
// 参考selectPage()方法,返回值必须是Page对象。想要分页插件作用我们自己的方法,第一个参数必须是Page类型!!!
// 根据年龄查询用户信息,进行分页
Page<User> selectPageVo(@Param("page") Page<User> page, @Param("age") Integer age);
}
映射文件:
<!-- resultType是实体类对象类型就行,select标签里面的sql不需要我们自己写分页的逻辑-->
<select id="selectPageVo" resultType="com.sutong.bean.User">
select id,name,age,email,is_delete from user where age > #{age}
</select>
测试:
@Test
public void test02() {
Page<User> page = new Page<>(2, 3);
userMapper.selectPageVo(page, 10); // 当然这个方法可以不用自己写,可以传入条件构造器就行
List<User> list = page.getRecords(); // 年龄大于10分页后的数据
list.forEach(System.out::println);
// SELECT COUNT(*) AS total FROM user WHERE age > ?
// select id,name,age,email,is_delete from user where age > ? LIMIT ?,? 在我们写的sqsl后面加上limit
}
Ⅱ 乐观锁
一件商品,成本价是80元,售价是100元。如果小李和小王同时操作商品后台系统。小李操作的时候,系统先取出商品价格100元;小王也在操作,取出的商品价格也是100元。小李将价格加了50元,并将100+50=150元存入了数据库;小王将商品减了30元,并将100-30=70元存入了数据库。是的,如果没有锁,小李的操作就 完全被小王的覆盖了。
-
悲观锁
小李取出数据后,小王只能等小李操作完之后,才能对价格进行操作,也会保证 最终的价格是120元。
-
乐观锁
小王保存价格前,会检查下价格是否被人修改过了(查看版本号字段)。如果被修改过了,则重新取出的被修改后的价格,150元,这样他会将120元存入数据库。
模拟修改的冲突:
-
创建商品表
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 t_product (id, NAME, price) VALUES (1, '外星人笔记本', 100);
-
创建实体类Product,ProductMapper
-
测试(最终价格会变为70)
@Test public void test01() { Product productLi = productMapper.selectById(1); Integer priceLi = productLi.getPrice(); // 100 Product productWang = productMapper.selectById(1); Integer priceWang = productWang.getPrice(); // 100 // 修改 productLi.setPrice(priceLi + 50); productMapper.updateById(productLi); // 150 productWang.setPrice(priceWang - 30); productMapper.updateById(productWang); // 70 }
解决上面修改冲突:(使用MyBatisPlus提供的乐观锁插件)
乐观锁实现流程:
在表中添加version字段
取出记录时,获取当前version
SELECT id,name,price,version FROM product WHERE id=1
更新时,version + 1,如果where语句中的version版本不对,则更新失败
UPDATE product SET price=price+50, version=version + 1 WHERE id=1 AND version=1
-
配置类中配置插件
@Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 添加乐观锁插件 return interceptor; } }
-
在实体类表示版本号的属性上面加上
@Version
注解public class Product { ... @Version // 标识乐观锁版本号字段 private Integer version; }
-
运行上面的测试
@Test public void test01() { Product productLi = productMapper.selectById(1); Integer priceLi = productLi.getPrice(); // 100 Product productWang = productMapper.selectById(1); Integer priceWang = productWang.getPrice(); // 100 productLi.setPrice(priceLi + 50); productMapper.updateById(productLi); // 150 // UPDATE product SET name=?, price=?, version=? WHERE id=? AND version=? // 参数:外星人笔记本(String), 150(Integer), 1(Integer), 1(Long), 0(Integer) productWang.setPrice(priceWang - 30); productMapper.updateById(productWang); // 版本号不对,更新失败!!! // UPDATE product SET name=?, price=?, version=? WHERE id=? AND version=? // 参数: 外星人笔记本(String), 70(Integer), 1(Integer), 1(Long), 0(Integer) Product productBoss = productMapper.selectById(1); System.out.println(productBoss.getPrice()); // 最终会是150 }
但这样并没有实现我们想要的价格!!
-
优化
@Test public void test01() { Product productLi = productMapper.selectById(1); Integer priceLi = productLi.getPrice(); // 100 Product productWang = productMapper.selectById(1); Integer priceWang = productWang.getPrice(); // 100 productLi.setPrice(priceLi + 50); productMapper.updateById(productLi); // 150 productWang.setPrice(priceWang - 30); int res = productMapper.updateById(productWang); if (res == 0) { // 操作失败重试,获取新的版本号,进行修改 Product productNew = productMapper.selectById(1); Integer priceNew = productNew.getPrice(); productNew.setPrice(priceNew - 30); productMapper.updateById(productNew); // 再次进行修改!! } Product productBoss = productMapper.selectById(1); System.out.println(productBoss.getPrice()); // 最终就是120了!! }
7. 通用枚举
表中的有些字段值是固定的,例如性别(男或女),此时我们可以使用MyBatis-Plus的通用枚举来实现
-
在user表上添加sex字段,是int型的(默认是把枚举的名放到数据库的,而我们想要放01,数据库的sex字段是int类型的!)
-
创建SexEnum枚举类
@Getter // 只设置get进行了 public enum SexEnum { MALE(1, "男"), FEMALE(0, "女"); @EnumValue // 将注解所标志属性的值存储到数据库中 private Integer sex; private String sexName; SexEnum(Integer sex, String sexName) { this.sex = sex; this.sexName = sexName; } }
-
在User实体类加上sex属性
public class User { ... private SexEnum sex; // 枚举类型的sex @TableLogic private Integer isDelete; }
-
扫描通用枚举!!
mybatis-plus: type-enums-package: com.sutong.enums
-
测试
@Test public void test01() { User user = new User(null, "admin", 20, "admin@qq.com", SexEnum.MALE, 0); userMapper.insert(user); // 这时数据库中存的标识MALE字符串,而是1数字!! }
很简单就两步
1.@EnumValue
2.扫描通用枚举
8. 代码生成器
逆向工程:根据表逆向生成实体类和Mapper接口和映射文件
这个和逆向工程差不多,但要比MyBatis生成的要多一点!!
-
依赖
<dependency> <groupId>com.baomidou</groupId> <!-- 代码生成器的依赖--> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.1</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <!-- 生成过程中需要使用FreeMarker引擎模板--> <artifactId>freemarker</artifactId> <version>2.3.31</version> </dependency>
-
创建测试类,直接执行下面代码就行
public class FastAutoGeneratorTest { public static void main(String[] args) { FastAutoGenerator.create("jdbc:mysql://localhost:3306/mybatis_plus", "root", "9527") .globalConfig(builder -> { builder.author("sutong") // 设置作者 .enableSwagger() // 开启 swagger 模式 (可关) .fileOverride() // 覆盖已生成文件 .outputDir(System.getProperty("user.dir") + "/src/main/java"); // 指定输出目录 }) .packageConfig(builder -> { builder.parent("com.sutong") // 设置父包名 .moduleName("mybatisplus") // 设置父包模块名(可不设置) .pathInfo(Collections.singletonMap(OutputFile.mapperXml, System.getProperty("user.dir") + "/src/main/resources/mapper")); // 设置mapperXml生成路径 }) .strategyConfig(builder -> { builder.addInclude("t_dept") // 设置需要生成的表名 .addTablePrefix("t_", "c_") // 设置过滤表前缀 .entityBuilder().enableLombok() // 使用lombok .controllerBuilder().enableRestStyle(); // 开启生成@RestController控制器 }) .templateEngine(new FreemarkerTemplateEngine()) //使用Freemarker引擎模板,默认的是Velocity引擎模板 .execute(); } }
-
controller,entity,mapper,service,xxxMapper.xml 都自动生成了!!
命名会自动转化,下划线命名自动转化为驼峰命名!
9. 多数据源
适用于多种场景:纯粹多库、 读写分离、 一主多从、 混合模式等
我们创建两个库,分别为:mybatis_plus(以前的库不动)与mybatis_plus_1(新建),将 mybatis_plus库的product表移动到mybatis_plus_1库,这样每个库一张表,通过一个测试用例 分别获取用户数据与商品数据,如果获取到说明多库模拟成功
-
创建数据库和表
-
引入多数据源场景!!starter
<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.5.0</version> </dependency>
-
配置多数据源
spring: datasource: dynamic: primary: master #设置默认的数据源或者数据源组,默认值即为master strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源 datasource: master: url: jdbc:mysql://localhost:3306/mybatis_plus username: root password: zgq20020820 driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置 slave_1: url: jdbc:mysql://localhost:3306/mybatis_plus_1 username: root password: zgq20020820 driver-class-name: com.mysql.jdbc.Driver slave_2: url: ENC(xxxxx) # 内置加密,使用请查看详细文档 username: ENC(xxxxx) password: ENC(xxxxx) driver-class-name: com.mysql.jdbc.Driver #......省略 #以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
# 多主多从 纯粹多库(记得设置primary) 混合配置 spring: spring: spring: datasource: datasource: datasource: dynamic: dynamic: dynamic: datasource: datasource: datasource: master_1: mysql: master: master_2: oracle: slave_1: slave_1: sqlserver: slave_2: slave_2: postgresql: oracle_1: slave_3: h2: oracle_2:
-
创建UserService
public interface UserService extends IService<User> { } @DS("master") //指定所操作的数据源!!! 没有@DS使用默认数据源 @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { }
-
创建ProductService
public interface ProductService extends IService<Product> { } @DS("slave_1") @Service public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService { }
@DS注意:方法上的注解优先于类上注解。强烈建议只在service的类和方法上添加注解,不建议在mapper上添加注解。配置文件所有以下划线
_
分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。如果我们实现读写分离,将写操作方法加上主库数据源,读操作方法加上从库数据源,自动切换,就能实现读写分离!!
10. MyBatisX
MyBatis-Plus为我们提供了强大的mapper和service模板,能够大大的提高开发效率 但是在真正开发过程中,MyBatis-Plus并不能为我们解决所有问题,例如一些复杂的SQL,多表联查,我们就需要自己去编写代码和SQL语句,我们该如何快速的解决这个问题呢,这个时候可 以使用MyBatisX插件 MyBatisX一款基于 IDEA 的快速开发插件,为效率而生!
使用文档:
https://baomidou.com/pages/ba5b24/
-
IDEA中安装MyBatisX插件
-
想要使用MyBatisX的代码生成需要使用IDEA右边的DataBase连接上数据库
-
连上后选择表,右键MyBatisX Generator
-
设置一些生成的包/路径,字段忽略的前后缀,表明忽略的前后缀,然后next
-
annotation选择MyBatisPlus 3
options按需选择就行,comment是注释,lombok是使用它
template是生成的模板,选择MyBatisPlus 3
然后下面有Mapper接口,MapperXML,serviceImpl,service接口的生成位置,和我们自己写的一样
-
生成的东西和MyBatis的代码生成器差不多!
MapperXML里面还给我们把字段抽取出来了,还提供了一个基础的resultMap
快速生成CRUD⭐,我们不需要写返回值,只需要写方法名(insert, select, delete, update开头…),插件就给我们提示了
然后按alt+enter,选择第二个MyBatisX-Generator sql,帮我们自动补全方法和映射文件中的sql
例如:
public interface DeptMapper extends BaseMapper<Dept> {
// 有选择性的添加
int insertSelective(Dept dept);
// 通过各个字段删除都可以,输入And后面也自动提示!!
int deleteByIdAndDeptName(@Param("id") Long id, @Param("deptName") String deptName);
// 修改那个字段都行,直接And拼接,条件通过By拼接!!
int updateAddressById(@Param("address") String address, @Param("id") Long id);
//selectAllById 通过id查所有字段
//selectAddressByAgeBetween 通过年龄区间查地址字段,参数会有beginAge和endAge
//selectAllOrderByAgeDesc 年龄升序
List<Dept> selectDeptNameAndAddressById(@Param("id") Long id); //通过id查询名字和地址字段
}
<insert id="insertSelective">
insert into t_dept
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
<if test="deptName != null">dept_name,</if>
<if test="address != null">address,</if>
</trim>
values
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">#{id,jdbcType=BIGINT},</if>
<if test="deptName != null">#{deptName,jdbcType=VARCHAR},</if>
<if test="address != null">#{address,jdbcType=VARCHAR},</if>
</trim>
</insert>
<delete id="deleteByIdAndDeptName">
delete from t_dept
where
id = #{id,jdbcType=NUMERIC}
AND dept_name = #{deptName,jdbcType=VARCHAR}
</delete>
<update id="updateAddressById">
update t_dept
set address = #{address,jdbcType=VARCHAR}
where
id = #{id,jdbcType=NUMERIC}
</update>
<select id="selectDeptNameAndAddressById" resultMap="BaseResultMap">
select dept_name, address
from t_dept
where
id = #{id,jdbcType=NUMERIC}
</select>
命名推荐:
Dao 接口命名
- insert
- batchInsert
- selectOne (selectByXXX)
- count
- list
- listPage
- update
- delete
Service 接口命名
- add
- findOne (findByXXX)
- findAll
- modify
- remove
阿里的规范Dao / Service:
获取单个对象,get
获取多个对象,listUsers
统计,count
插入,insert / save
删除,delete / remove
修改,update