通用pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.13</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
前提回顾
resultMap比resultType更灵活。
<resultMap id = "xxxMap" type="brand">
// 主键列使用 id标签
<id column = "表列名" property="实体类属性名"/>
// 普通列使用 result标签
<result column = "数据库表列名" property="属性名"/>
...
</resultMap>
使用:
<select id = "selectAll" resultMap="xxxMap">
// * 可以自动映射到xxxMap中的数据
select * from tb_brand
</select>
BaseMapper功能
@SpringBootTest
class Atguigu01MybatisplusApplicationTests {
@Autowired
private UserMapper userMapper;
@Test
void contextLoads() {
List<User> list = userMapper.selectList(null);
list.forEach(System.out::println);
}
// 新增功能
@Test
public void testInsert(){
// insert into user(id,name,age,email) values (?,?,?,?)
User user = new User();
user.setAge(18);
user.setEmail("zs@qq.com");
user.setName("张三");
// 返回新增的数据量,默认mybatis-plus生成的主键id用的是雪花算法
int result = userMapper.insert(user);
System.out.println(result);
}
// 删除功能:返回删除的数量
@Test
public void testDelete(){
// 如果不加一个L,数值就会超过int型
// int result = userMapper.deleteById(1584762459243786242L);// 根据id来删除
/* 根据map集合中设置的条件来删除
Map<String,Object> map = new HashMap<>();
map.put("name","张三");
map.put("age",18);
int result = userMapper.deleteByMap(map);
*/
/* 通过多个id,批量删除表数据,传入的参数是一个集合
将数组转化成List集合的方法
List<Long> list = Arrays.asList(1L, 2L);
int result = userMapper.deleteBatchIds(list);
*/
// System.out.println(result);
}
// 修改功能:
@Test
public void testUpdate(){
User user = new User();
user.setId(3L);
user.setName("Xhan");
user.setEmail("666@qq.com");
int result = userMapper.updateById(user);
System.out.println(result);
}
// 查询功能:
@Test
public void testQuery(){
// User user = userMapper.selectById(3L);
/*
List<Long> list1 = Arrays.asList(3L, 4L);
List<User> list = userMapper.selectBatchIds(list1);
list.forEach(System.out::println);
*/
/*
Map<String,Object> map = new HashMap<>();
map.put("name","Xhan");
map.put("age",28);
List<User> list = userMapper.selectByMap(map);
*/
// 删除和修改也有类似这种方法,可以不写参数,默认就是所有,如果是所有,除了查询,修改和删除不推荐默认所有
List<User> list = userMapper.selectList(null);
list.forEach(System.out::println);
}
}
自定义sql
- 先在MyBatisPlus的
Mapper.xml
中定义:
@Repository
public interface UserMapper extends BaseMapper<User> {
// 自定义数据库语句,需要自定义Mapper,并且要存在mapper文件夹中
Map<String,Object> selectMapById(Long id);
}
- 然后需要在resources文件夹中再次创建一个mapper文件夹,里面存储
XXXMapper.xml
,该文件中的自定义sql语句和MyBatis的写法是一致的。
<?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.example.atguigu_01_mybatisplus.mapper.UserMapper">
<!-- Map<String,Object> selectMapById(Long id); -->
<select id="selectMapById" resultType="map">
select id,name,age,email from user where id = #{id}
</select>
</mapper>
- 使用:
@Test
public void testMySql(){
Map<String, Object> map = userMapper.selectMapById(3L);
System.out.println(map);
}
Service接口中的CRUD
CRUD 接口 | MyBatis-Plus (baomidou.com)
@SpringBootTest
public class MyBatisPlusServiceTest {
@Autowired
private UserService userService;
// 查询总数据数方法:SELECT COUNT( * ) FROM user
@Test
void testGetCount(){
long count = userService.count();
System.out.println("总记录数:"+count);
}
// 批量添加:本质上是单个sql语句,循环添加
// INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? )
@Test
void testInsertMore(){
List<User> list = new ArrayList<>();
for(int i=0;i<10;i++){
User user = new User();
user.setName("Xhan"+i);
user.setEmail("123@qq.com");
user.setAge(i+1);
list.add(user);
}
boolean b = userService.saveBatch(list); // 返回批量是否成功
System.out.println(b);
}
}
注解@TableName
我们在项目中并没有指定对哪张表操作,MyBatisPlus是通过继承BaseMapper时,指定了类型为<User>
也就是实体类类型,才匹配到数据中的User表,一旦表名发生改变,那么就不会匹配上了。
第一种方法:
TableName("user")
指定对应的表名
如果表名具有相同的前缀,比如果
t_
,我们可以在yml
中进行设置:
global-config:
db-config:
table-prefix: t_
注解@TableId
MyBatisPlus默认会将id设置为主键,并通过雪花算法生成唯一的主键值。如果我们id不是主键,或者说主键名不叫id,那么就会存在问题。
-
在实体类中,对实体类中匹配字段主键的属性,设置
@TableId
指定主键(字段和实体类的名字要相同,只不过不叫id)。 -
那如果字段和实体类的名字不同呢?比如字段是uid,而实体类中是id:通过
@TableId(value="字段名")
来指定主键的字段
@TableId(value = "uid")
private Long id;
- 不想使用MyBatisPlus中的主键雪花算法,想要自动递增,首先要把主键字段设置成递增,然后设置
@TableId(type=IdType.AUTO)
,因为默认type是雪花算法类型(IdType.ASSIGN_ID)。
@TableId(value="uid" type=IdType.AUTO)
private Long id;
- 如果在修改操作时,我们指定id的值,那么id的值还会是雪花算法吗?不会的。和主键自增一样,手动优先。
我们也可以全局设置自增的策略:
global-config:
db-config:
id-type:auto;
雪花算法
-
垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。
- 例如,前面示意图中的 nickname 和 description 字段,假设我们是一个婚恋网站,用户在筛选其他用户的时候,主要是用 age 和 sex 两个字段进行查询,而 nickname 和 description 两个字段主要用于展示,一般不会在业务查询中用到。description 本身又比较长,因此我们可以将这两个字段独立到另外 一张表中,这样在查询 age 和 sex 时,就能带来一定的性能提升。
-
水平分表适合表行数特别大的表,有的公司要求单表行数超过 5000 万就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能。
- 对于一些比较复杂的表,可能超过 1000万就要分表了;而对于一些简单的表,即使存储数据超过 1 亿行,也可以不分表。但不管怎样,当看到表的数据量达到千万级别时,作为架构师就要警觉起来,因为这很可能是架构的性 能瓶颈或者隐患。
水平分表相比垂直分表,会引入更多的复杂性,例如要求全局唯一的数据id该如何处理。
注解@TableField
MyBatisPlus可以将字段中下划线的名字,替换成驼峰,但是如果名字整个就不同,就需要我们使用注解来解决。
@TableFiled
用来解决除了主键外的字段名和实体类属性不匹配的问题。
@TableField(user_name)
private String name;
注解@TableLogic
- 在表中添加一个字段:isDeleted,int型,默认是0。
- 然后在实体类中定义isDeleted。
- 对这个属性加上注解:
@TableLogic
表示逻辑删除字段。
此时删除操作就会变成修改操作:is_deleted默认是0,表示未删除状态,如果为1,表示已删除状态(逻辑删除)。
测试删除功能,真正执行的是修改is_deleted
的值,但是查询的话,还是查不到的,因为被逻辑删除了,只能查询到id_deleted=0
未删除的数据。但是打开数据表,还是存在的,只不过is_deleted=1
UPDATE t_user SET is_deleted=1 WHERE id=? AND is_deleted=0
测试查询功能,被逻辑删除的数据默认不会被查询
SELECT id,username AS name,age,email,is_deleted FROM t_user WHERE is_deleted=0
条件构造器
在BaseMapper中,老是能看到一个参数:Wrapper
,他实质上是一个条件构造器,用来封装条件。
组装各种条件
默认就是and条件
@SpringBootTest
public class MyBatisPlusWrapperTest {
@Autowired
private UserMapper userMapper;
// 查询 条件
@Test
public void testWrapperQuery(){
// 查询用户名包含a,年龄在20~30间,邮箱信息不为空
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// SELECT id,name,age,email FROM user WHERE (name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL)
queryWrapper.like("name","a")
.between("age",20,30)
.isNotNull("email");
List<User> list = userMapper.selectList(queryWrapper);
list.forEach(System.out::println);
}
// 排序 条件
@Test
public void testWrapperOrder(){
// 查询用户信息,按照年龄的降序排序(Desc),若年龄相同,则按照id升序排序。
// SELECT id,name,age,email FROM user ORDER BY age DESC,id ASC
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc("age")
.orderByAsc("id");
List<User> list = userMapper.selectList(queryWrapper);
list.forEach(System.out::println);
}
// 删除 条件
@Test
public void testWrapperDelete(){
// 删除邮箱地址为:null
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.isNull("email");
int result = userMapper.delete(queryWrapper);
System.out.println(result);
}
// 修改 条件
@Test
public void testWrapperUpdate(){
// UPDATE user SET name=?, email=? WHERE (age > ? AND name LIKE ? OR email IS NULL)
// 将年龄大于20,并且用户名中包含a 或者 邮箱为null的用户信息修改
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.gt("age",20)
.like("name","a")
.or()
.isNull("email");
User user = new User();
user.setName("小猪");
user.setEmail("xhan@qq.com");
// update要求传入要修改的数据,第二个要求传入条件
int update = userMapper.update(user,queryWrapper);
System.out.println(update);
}
}
条件的优先级
lambda中的条件优先执行。
and和or方法,都可以使用lambda表达式。
// lambda表达式
@Test
public void testLambda(){
// 将用户名中含有a,并且年龄大于20 或者 邮箱为null 的用户信息修改
// lambda中的条件优先执行
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// i是条件构造器
queryWrapper.like("name","a")
.and(i->i.gt("age",20).or().isNull("email"));
User user = new User();
user.setName("小鹏");
user.setEmail("666@qq.com");
int update = userMapper.update(user, queryWrapper);
System.out.println(update);
}
组装select
按照我们设置的字段,来进行查询
// 组装select
@Test void testWrapperSelect(){
// 查询用户的用户名、年龄、邮箱
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.select("name","age","email");
List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
maps.forEach(System.out::println);
}
组装子查询
// 组装子查询
@Test
void testWrapper_Select(){
// 查询id小于等于100的数据:
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// inSql的第二个字段是语句
queryWrapper.inSql("id","select id from user where id<=100");
List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
maps.forEach(System.out::println);
}
UpdateWrapper
之前我们都是使用queryWrapper
来修改,现在我们通过UpdateWrapper
也可以进行修改。
// updateWrapper
@Test
void testUpdateWrapper(){
// 将用户名中包含a,并且年龄大于20 或者 邮箱为null 的用户信息修改
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.like("name","a")
.and(i->i.gt("age",20).or().isNull("email"));
updateWrapper.set("name","小黑")
.set("email","shabi@qq.com");
// 这里不需要定义实体类,因为已经将User添加到UpdateWrapper中了
int update = userMapper.update(null, updateWrapper);
System.out.println(update);
}
条件组装
在真正开发的过程中,组装条件是常见的功能,而这些条件数据来源于用户输入,是可选的,因此我们在组装这些条件时,必须先判断用户是否选择了这些条件,若选择则需要组装该条件,若没有选择则一定不能组装,以免影响SQL执行的结果。
低配写法:
// 模拟企业组装条件
@Test
void testAllWrapper(){
String name = "";
Integer ageBegin = 20;
Integer ageEnd = 30;
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if(StringUtils.isNotBlank(name)) {
// isNotBlank判断某个字符是否不为空、不为null、不为空白
queryWrapper.like("name",name);
}
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组装条件
@Test
void testCondition(){
//定义查询条件,有可能为null(用户未输入或未选择)
String name = "a";
Integer ageBegin = 10;
Integer ageEnd = 24;
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like(StringUtils.isNotBlank(name),"username", "a")
.ge(ageBegin != null, "age", ageBegin)
.le(ageEnd != null, "age", ageEnd);
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
}
LambdaQueryWrapper
Lambda表达式需要用函数式接口来指定字段名,不可以使用字符串。
// Lambda
@Test
void testLambdaQueryWrapper(){
String name = "a";
Integer ageBegin = null;
Integer ageEnd = 30;
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
// 第二个参数是 数据表列名(这里使用的写法是函数式接口写法,属性所对应的字段名),第三个参数是实体类名
lambdaQueryWrapper.like(StringUtils.isNotBlank(name),User::getName,name)
.ge(ageBegin!=null,User::getAge,ageBegin)
.le(ageEnd!=null,User::getAge,ageEnd);
List<User> list = userMapper.selectList(lambdaQueryWrapper);
list.forEach(System.out::println);
}
LambdaUpdateWrapper
@Test
void testLambdaUpdateWrapper(){
LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
// 第二个参数是 数据表列名(这里使用的写法是函数式接口写法,属性所对应的字段名),第三个参数是实体类名
lambdaUpdateWrapper.like(User::getName,"a")
.and(i->i.gt(User::getAge,20).or().isNull(User::getEmail));
lambdaUpdateWrapper.set(User::getName,"张伟")
.set(User::getAge,24)
.set(User::getEmail,"888@qq.com");
int result = userMapper.update(null,lambdaUpdateWrapper);
System.out.println(result);
}
分页插件
配置类配置插件:
@Configuration
@MapperScan("xxx.mapper") // 扫描mapper接口所在的包
public class MyBatisPlusConfig {
// 配置插件:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件:并指定数据库类型(因为每个数据库的分页类型不同)
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
测试类进行测试:
@SpringBootTest
public class MyBatisPlusPluginsTest {
@Autowired
private UserMapper userMapper;
@Test
void testPage(){
// 当前页的索引,每页显示的条数。
Page<User> page = new Page<>(1,3);
// 分页对象,条件构造器。返回值也是一个page
userMapper.selectPage(page, null);
System.out.println(page);
}
}
System.out.println(page.getRecords()); // 获取当前页数据
System.out.println(page.getCurrent()); // 获取当前页码
System.out.println(page.getSize()); // 获取每页显示的条数
System.out.println(page.getPages()); // 获取总页数
System.out.println(page.getTotal()); // 获取总记录数
System.out.println(page.hasNext()); // 是否有下一页
System.out.println(page.hasPrevious()); // 是否有上一页
自定义分页功能
查询语句是自定义的,对我们查询出来的结果实现分页功能:自定义查询语句,就要在.xml中自己手写:
yml中配置类型别名
mybatis-plus:
# 配置类型别名所对应的包
type-aliases-package: com.example.atguigu_01_mybatisplus.pojo
<!-- resultType使用的User是类型别名,需要在yml中设置 -->
<select id="selectPageVo" resultType="User">
select id,name,age,email from user where age > #{age}
</select>
// 第一个参数必须是一个page对象,第二个可以是自定义分页条件
// 通过年龄查询用户信息,并分页
Page<User> selectPageVo(@Param("page") Page<User> page, @Param("age") Integer age);
@Test
void testMyPage(){
Page<User> page = new Page<>(1,3);
// 查询年龄大于20的,并进行分页
userMapper.selectPageVo(page,20);
System.out.println(page.getRecords()); // 获取当前页数据
System.out.println(page.getCurrent()); // 获取当前页码
System.out.println(page.getSize()); // 获取每页显示的条数
System.out.println(page.getPages()); // 获取总页数
System.out.println(page.getTotal()); // 获取总记录数
System.out.println(page.hasNext()); // 是否有下一页
System.out.println(page.hasPrevious()); // 是否有上一页
}
乐观锁
乐观锁会先在数据库中添加一个version字段,每次更新时,都会将version+1,如果后续的version匹配不上,则不允许操作。
@Test
void test_01_product(){
// 小李查询商品价格
Product productLi = productMapper.selectById(1);
System.out.println("小李查询商品价格:"+productLi.getPrice());
// 小王查询商品价格
Product productWang = productMapper.selectById(1);
System.out.println("小王查询商品价格:"+productWang.getPrice());
// 小李将商品+50
productLi.setPrice(productLi.getPrice()+50);
productMapper.updateById(productLi);
// 小王将商品-30,此时小王的操作会将小李的操作覆盖掉。
productWang.setPrice(productWang.getPrice()-30);
productMapper.updateById(productWang);
// 老板查询价格:最终是70,但是预期是小李+50,150;然后-30,120,
Product productBoss = productMapper.selectById(1);
System.out.println("老板查询价格:"+productBoss);
}
乐观锁插件:
先给实体类中version属性加上@Version
标识
@Version // 标识乐观锁版本号字段
private Integer version;
配置插件:
// 添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
此时得到的结果就是150了,因为小李修改后,version+1,等到小王再次操作时,version已经对不上了,所以小王的操作就会失效。所以老板查到的结果就是+50的结果。
@Test
void test_01_product(){
// 小李查询商品价格
Product productLi = productMapper.selectById(1);
System.out.println("小李查询商品价格:"+productLi.getPrice());
// 小王查询商品价格
Product productWang = productMapper.selectById(1);
System.out.println("小王查询商品价格:"+productWang.getPrice());
// 小李将商品+50
productLi.setPrice(productLi.getPrice()+50);
productMapper.updateById(productLi);
// 小王将商品-30,此时小王的操作会将小李的操作覆盖掉。
productWang.setPrice(productWang.getPrice()-30);
productMapper.updateById(productWang);
// 老板查询价格:最终是70,但是预期是小李+50,150;然后-30,120,
Product productBoss = productMapper.selectById(1);
System.out.println("老板查询价格:"+productBoss);
}
优化流程
// 小王将商品-30,此时小王的操作会将小李的操作覆盖掉。
...
// 说明没有更新
if(result == 0){
// 操作失败,重试。也就是需要重新获取新的Product
Product productNew = productMapper.selectById(1);
productNew.setPrice(productNew.getPrice()-30);
productMapper.updateById(productNew);
}
// 老板查询价格:
...
多数据源
多数据源应用于:多张库,多张表共同工作,或者读写分离等。
spring:
datasource:
dynamic:
# 设置默认的数据源或者数据源组,默认值是master;
primary: master
# 严格匹配数据源,默认false,true未匹配到指定数据源时,抛异常,false使用默认数据源
strict: false
datasource:
# primary中书写的是什么,这里就得写什么
master:
url: jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&&useSSL=false&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
# 从数据源
slave_1:
url: jdbc:mysql://localhost:3306/mybatis_plus1?characterEncoding=utf-8&&useSSL=false&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
创建:
@Repository
public interface UserMapper extends BaseMapper<User> {
}
@Repository
public interface ProductMapper extends BaseMapper<Product> {
}
public interface UserService extends IService<User> {
}
@Service
// 指定数据源
@DS("master")
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
public interface ProductService extends IService<Product> {
}
@Service
// 指定数据源
@DS("slave_1")
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
}
测试:
@Autowired
private UserService userService;
@Autowired
private ProductService productService;
@Test
void testMoreData() {
System.out.println(userService.getById(4));
System.out.println(productService.getById(1));
}
MyBatisX插件
这个插件是基于IDEA
而生,其他编译器需要自行查找。
// 实现自定义sql,这里只需要写方法名即可,会帮我们自动生成对应的sql语句
int insertSelective(User user);
int deleteByIdAndName(@Param("id") Long id, @Param("name") String name);
int updateAgeAndNameAndSexById(@Param("age") Integer age, @Param("name") String name, @Param("sex") Integer sex, @Param("id") Long id);
List<User> selectAgeAndSexByAgeBetween(@Param("beginAge") Integer beginAge, @Param("endAge") Integer endAge);
List<User> selectAllOrderByAgeDes();