1.开发步骤
前提: 使用了mybatis作为持久层框架
- 步骤一:引入依赖
注意: mybatis-plus-boot-starter的版本号需要与SpringBoot、mybatis版本对应, 不然会抛异常
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
- 步骤二: 创建实体类, 提供get、set方法
@Data
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
}
- 步骤三: mapper继承BaseMapper接口, 并在泛型中指定实体类
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
我们并没有通过注解指定实体类对应的表名, 也没有通过注解指定实体属性对应列名, 那么MyBatis-Plus是怎么匹配到表和字段的
1.1 表名、字段映射规则
建议手动配置Mybatis-Plus的驼峰匹配
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
-
表名映射规则: 会将实体类的类名按照驼峰匹配规则转换为下划线风格的表名
- User > user
- TbUser > tb_user
-
字段名映射规则: 会将实体类的属性名按照驼峰匹配规则转换为下划线风格的字段名
- name > name
- userName > user_name
1.2 通过注解指定表名、列名
- @TableName注解用于指定实体类对应的数据库表名。
- @TableField注解用于指定实体类属性对应的数据库字段名。
- @TableId注解用于指定实体类属性对应的数据库表的主键字段。
1.表的字段和实体类的属性名不对应
模型类属性上方,使用@TableField属性注解,通过value属性,设置当前属性对应的数据库表中的字段关系。
2.对象中添加了数据库中未定义的属性
在模型类属性上方,使用@TableField注解,通过exist属性,设置属性在数据库表字段中是否存在,默认为true。此属性无法与value合并使用
3.采用默认查询开放了更多的字段查看权限
在模型类属性上方,使用@TableField注解,通过select属性:设置该属性是否参与查询。此属性与select()映射配置不冲突。
4.表名与编码开发设计不同步
@Data
@TableName("tbl_user")
public class User {
/*
id为Long类型,因为数据库中id为bigint类型,
并且mybatis有自己的一套id生成方案,生成出来的id必须是Long类型
*/
private Long id;
private String name;
@TableField(value = "pwd",select = false)
private String password;
private Integer age;
private String tel;
@TableField(exist = false) //表示online字段不参与CRUD操作
private Boolean online;
}
2.BaseMapper的通用操作
2.1 插入操作
int insert(T entity)
: 插入一条记录, 会将实体对象对应的数据插入到数据库中
实体类的属性为null,则在数据库中对应的字段会被插入为 null 值。这与数据库的字段允许为 null 的情况一致。
2.2 查询操作
-
T selectById(Serializable id)
: 根据主键id查询记录, 并返回对应的实体对象。
会根据实体类中被@TableId注解标注的属性来确定主键。如果没有显示添加注解, 仍然可以通过一定的规则来确定主键字段 -
List<T> selectBatchIds(Collection<? extends Serializable> idList)
: 根据主键 id 的集合批量查询记录,并返回对应的实体对象集合。 -
List<T> selectByMap(Map<String, Object> columnMap)
: 根据 columnMap 中的条件查询记录,并返回对应的实体对象集合 -
T selectOne(Wrapper<T> queryWrapper)
: 根据条件查询一条记录,并返回对应的实体对象。若查询结果不唯一或者不存在,则返回 null。 -
Integer selectCount(Wrapper<T> queryWrapper)
: 根据条件查询记录数量,并返回符合条件的记录数。 -
List<T> selectList(Wrapper<T> queryWrapper)
: 根据条件查询记录,并返回符合条件的实体对象集合。 -
List<Map<String, Object>> selectMaps(Wrapper<T> queryWrapper)
: 根据条件查询记录,并返回符合条件的 Map 对象集合。每个 Map 对象表示一条记录,key 是数据库字段名,value 是对应字段的值。 -
List<Object> selectObjs(Wrapper<T> queryWrapper)
: 根据条件查询记录,并返回符合条件的字段值集合。 -
IPage<T> selectPage(IPage<T> page, Wrapper<T> queryWrapper)
: 分页查询记录,并返回符合条件的实体对象分页结果。
2.3 更新操作
int updateById(T entity)
: 根据主键 id 更新记录,将实体对象对应的数据更新到数据库中。int update(T entity, Wrapper<T> updateWrapper)
: 根据条件更新记录,将实体对象对应的数据更新到数据库中。updateWrapper 为更新条件。
3.属性配置
3.1 开启MyBatisPlus日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3.2 取消SpringBoot启动banner图标
mybatis-plus:
global-config:
banner: off # 关闭mybatisplus启动图标
3.3 解决日志打印过多问题
做法:在resources下新建一个logback.xml文件,名称固定,内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
</configuration>
4.查询操作
4.1 构建查询条件
- QueryWrapper: 基本查询, 提供eq、ne、like、in等方法
QueryWrapper<User> qw=new QueryWrapper<>();
qw.lt("age", 18);
List<User> userList = userDao.selectList(qw);
- LambdaQueryWrapper: 对QueryWrapper的增强, 可以使用Lambda表达式来构建查询条件, 优点是可以避免硬编码
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.lt(User::getAge, 18);
List<User> users = userMapper.selectList(lqw);
4.2 并且(and)
查询年龄小于30, 大于10的
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.lt(User::getAge, 30);
lqw.gt(User::getAge, 10);
List<User> users = userMapper.selectList(lqw);
4.3 或者(or)
查询年龄小于10或者大于30的
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>();
lqw.lt(User::getAge, 10).or().gt(User::getAge, 30);
List<User> users = userMapper.selectList(lqw);
4.4 or、and的执行顺序
在sql中, and运算符的优先级是高于or运算符的
SELECT * FROM table_name WHERE condition1 AND condition2 OR condition3 AND condition4 OR condition5;
- condition1与condition2的连接
首先, condition1与condition2之间使用AND运算符连接, 这意味着数据库引擎会首先计算这两个条件. 只有当 condition1和condition2都为真时,整个条件为真 - condition3与condition4的连接
然后, condition3和condition4之间使用AND运算符连接. 这意味着数据库引擎会计算这两个条件. 只有当 condition3和condition4都为真时, 整个条件为真. - 前面两个连接的结果与condition5的连接
接下来, 前面两个连接的结果与condition5使用OR运算符连接. 这意味着只要前面的两个连接的结果中有一个为真, 或者condition5为真, 整个条件就为真。
4.5 构建包含and、or的查询条件
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>()
.and(qw -> qw.gt(User::getAge, 18).lt(User::getAge, 35))
.or(qw -> qw.gt(User::getAge, 18).like(User::getName, "张"))
.or(qw -> qw.eq(User::getName, "tom"));
return userMapper.selectList(lqw);
4.6 查询条件
- eq: 等于
- ne: 不等于
- gt: 大于
- ge: 大于等于
- lt: 小于
- le: 小于等于
- between: 在某个范围内
- like: 模糊查询
- in: 在某个集合内
- isNull: 为空
- isNotNull: 不为空
- orderBy: 排序
- groupBy: 分组
- having: 分组后的条件
- or: 或者
- and: 并且
- nested: 嵌套条件, 相当于()
4.7 条件参数控制
1.if语句控制条件追加
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
if(minAge != null){
lqw.gt(User::getAge, 10);
}
if(maxAge != null){
lqw.lt(User::getAge, 35);
}
List<User> userList = userDao.selectList(lqw);
2.条件参数控制(可链式编程)
Integer minAge = 18;
Integer maxAge = 35;
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
//参数1:如果表达式为true,那么查询才使用该条件
lqw.gt(minAge != null,User::getAge, minAge).lt(maxAge != null,User::getAge, maxAge);
List<User> userList = userDao.selectList(lqw);
4.8 分页查询
如果想要实现分页查询, 使用selectPage是不够的, 需要添加分页拦截器
- 步骤一: 设置分页拦截器(交给Spring管理)
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
//1 创建MybatisPlusInterceptor拦截器对象
MybatisPlusInterceptor mpInterceptor=new MybatisPlusInterceptor();
//2 添加分页拦截器
mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mpInterceptor;
}
}
- 步骤二: 执行分页查询
//分页查询
@Test
void testSelectPage(){
//1 创建IPage分页对象,设置分页参数
IPage<User> page=new Page<>(1,3);
//2 执行分页查询
userMapper.selectPage(page, null);
//3 获取分页结果
System.out.println("当前页码值:"+page.getCurrent());
System.out.println("每页显示数:"+page.getSize());
System.out.println("总页数:"+page.getPages());
System.out.println("总条数:"+page.getTotal());
System.out.println("当前页数据:"+page.getRecords());
}
4.9 查询字段
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
lqw.select(User::getId, User::getName);
List<User> userList = userMapper.selectList(lqw);
4.10 查询字段(未定义的属性)
这种情况下, 使用QueryWrapper更加方便
QueryWrapper<User> lqw = new QueryWrapper<User>();
lqw.select("count(*) as count, tel");
lqw.groupBy("tel");
List<Map<String, Object>> userList = userDao.selectMaps(lqw);
5.增、删、改操作
5.1 新增
5.1.1 id生成策略
- 自增: 例如日志
- 特殊规则: 例如订单编号(FQ23948AK3843)
- 关联地区日期等信息: 例如外卖单(10 04 20200314 34 91)
- …
5.1.2 自增ID、UUID、雪花算法的适用场景
适用场景:
- 自增ID
- 适用场景: 适用于单机应用或者不需要全局唯一性的情况
- 优点
- 简单易用,数据库支持的自增字段可以轻松实现
- 查询效率高,主键值连续递增,适合于分页查询等操作
- 缺点:
- 不能在插入数据前预先知道主键值
- 不适合分布式系统,无法在分布式环境中保证唯一性
- 在分布式环境中,可能存在多个数据库节点同时生成主键值的情况,而自增 ID 的生成通常是基于数据库中的自增长字段,每个数据库节点都维护着自己的自增长计数器。这样就会导致不同节点生成的主键值可能会重复,造成主键冲突
- 在分布式系统中进行数据迁移或分片时,如果使用自增 ID,可能会出现跨节点的主键冲突。因为不同节点上的自增计数器可能是独立的,而迁移或分片过程中可能无法保证主键的连续性
- UUID
- 适用场景: 适合于需要在分布式系统中保证全局唯一性的情况,或者需要在多个系统之间共享数据的情况
- 优点:
- 全局唯一性,不受限于单个数据库,适用于分布式环境
- 不依赖于数据库自增字段,可以在插入数据前预先生成唯一的主键值
- 缺点:
- 主键值较长,可能会增加索引存储和查询的成本
- 生成的主键值是随机的,无法根据时间戳排序
- 雪花算法
- 适用场景: 适合于需要在分布式系统中保证全局唯一性的情况,需要趋势递增的主键值的情况
- 优点:
- 具有趋势递增的特点,可以根据时间戳排序。
- 适用于分布式系统,生成的主键值可以在全局范围内保持唯一性。
- 缺点:
- 生成的主键值可能会存在一定的冲突风险,尤其在同一毫秒内生成的 ID 可能会重复。
- 实现相对复杂,需要考虑时间戳、机器标识和序列号等因素。
总的来说,选择合适的主键生成策略需要根据具体的业务需求和系统架构来综合考虑。如果需要简单的、连续递增的主键值,可以使用自增ID;如果需要全局唯一性且不依赖于特定数据库,可以使用UUID;如果需要在分布式系统中保证全局唯一性且需要趋势递增的主键值,可以使用雪花算法。
5.1.3 @TableId设置id生成策略
- ASSIGN_ID: 雪花算法生成id(默认)
- AUTO:数据库自增
- INPUT:用户输入
- ASSIGN_UUID:以UUID生成算法作为id生成策略
- NONE:不设置主键
1.ASSIGN_ID
当你使用ASSIGN_ID策略时,MyBatis-Plus会在进行插入操作时自动生成主键值, 而不依赖于数据库, 生成的主键值通常是基于一些算法生成的, 比如雪花算法. 而不是依赖于数据库的自增长字段
当前案例: id是数据库自增字段, 且实体类的id属性并没有添加@TableId注解
- 会根据实体类中被@TableId注解标注的属性来确定主键。如果没有显示添加注解, 仍然可以通过一定的规则来确定主键字段
- 使用ASSIGN_ID策略时, 主键生成策略是后端程序自动生成, 不受数据库自增长字段影响
2.AUTO
使用数据库id自增策略控制Id生成
在执行insert语句时, 不会给主键设置值, 主键的值执行数据库自增策略, 如果数据库没有设置主键自增, 则主键为null
3.INPUT
用户手动输入主键值, 而不是由数据库或程序自动生成, 需要用户在业务中主动设置主键值, 如果不设置, 则为null
5.1.4 全局策略配置
mybatis-plus:
global-config:
db-config:
# 使用雪花算法生成id
id-type: assign_id
# 表名前置全局配置
table-prefix: tbl_
相当于
5.2 批量Delete、批量Select
- 按主键删除多条记录
//删除指定多条数据
List<Long> list = new ArrayList<>();
list.add(1402551342481838081L);
list.add(1402553134049501186L);
list.add(1402553619611430913L);
userDao.deleteBatchIds(list);
- 根据主键查询多条记录
//查询指定多条数据
List<Long> list = new ArrayList<>();
list.add(1L);
list.add(3L);
list.add(4L);
userDao.selectBatchIds(list);
5.3 逻辑删除
我们平常进行逻辑删除,是手动来设置deleted的值(0存活、1删除), 而mybatisPlus已经帮我们把这个步骤封装了
- 步骤一: 数据库表中添加逻辑删除标记字段
- 步骤二: 实体类中添加对应字段,并设定当前字段为逻辑删除标记字段@TableLogic
注意:需要提供get/set方法
package com.itheima.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
@Data
public class User {
private Long id;
//逻辑删除字段,标记当前记录是否被删除
@TableLogic
private Integer deleted;
}
- 步骤三: 配置逻辑删除字面值
mybatis-plus:
global-config:
db-config:
table-prefix: tbl_
# 逻辑删除字段名
logic-delete-field: deleted
# 逻辑删除字面值:未删除为0
logic-not-delete-value: 0
# 逻辑删除字面值:删除为1
logic-delete-value: 1
逻辑删除本质:逻辑删除的本质其实是修改操作。如果加了逻辑删除字段,查询数据时也会自动带上逻辑删除字段。
- 步骤四 : 查询记录
注意: 一旦设置了logic-not-delete-value: 0,此后我们所有的select语句,只能查询deleted=0的数据
@Test
void selectListByCondition() {
QueryWrapper<User> query = new QueryWrapper<>();
query.eq("age", 12);
List<User> users_2 = userDao.selectList(query);
System.out.println(users_2);
}
- 步骤五: 删除记录
@Test
void deleteById() {
userDao.deleteById(1625801212297658377L);
}
5.4 更新Null字段的解决方案
updateById
和update
默认只更新不为null的字段
User user = new User();
user.setId(1L);
user.setName("tom");
user.setPassWord("");
user.setTel(null);
userMapper.updateById(user);
生成的sql中忽略了SET tel = null
解决方案:
- 添加@TableField(strategy = FieldStrategy.IGNORED)
- 全局配置
- 条件构造器组装sql
1.FieldStrategy.IGNORED
- FieldStrategy.IGNORED: 忽略判断,无论实体对象的字段值是否为空,都会进行更新操作
- FieldStrategy.NOT_NULL: 非NULL判断,只有不为NULL的字段才会参与数据处理
- FieldStrategy.NOT_EMPTY: 对字符串进行空值判断,只有非空字符串的字段才会参与数据处理
- FieldStrategy.DEFAULT: 跟随全局配置
@TableField(updateStrategy = FieldStrategy.IGNORED)
private String tel;
缺点: 其他更新的sql也会跳过null值检查, 所有update语句, 如果修改tel值时, 没有给该字段赋值也会更新为null,导致很严重的问题。
2.全局配置
不推荐的理由如上
mybatis-plus:
global-config:
db-config:
# 更新字段为空时同样进行修改
update-strategy: ignored
3.条件构造器组装sql
LambdaUpdateWrapper<User> uw = new LambdaUpdateWrapper<>();
uw.eq(User::getId, 1L)
.set(User::getName, "tom")
.set(User::getPassWord, "")
.set(User::getTel, null);
userMapper.update(null, uw);
update方法的参数: entity和updateWrapper是二选一的, 在调用 update 方法时只能选择其中一个来指定更新操作的条件。
- 如果你想基于实体对象的属性值来更新数据库记录,你应该传入一个非空的 entity 参数,这将会以实体对象的属性值作为更新的内容。
- 如果你想使用更加复杂的条件来指定更新的范围,比如基于某些条件来进行批量更新,你应该传入一个非空的 updateWrapper 参数,这将会根据 updateWrapper 提供的条件来执行更新操作。
6.乐观锁
mybatisPuls也帮我们封装了配置乐观锁的功能
- 步骤一: 数据库表中添加锁标记字段, 并提供初始值0或1
- 步骤二: 实体类中添加对应字段,并设定当前字段为逻辑删除标记字段
package com.itheima.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.Version;
import lombok.Data;
@Data
public class User {
private Long id;
@Version
private Integer version;
}
- 步骤三: 配置乐观锁拦截器实现锁机制对应的动态SQL语句拼装
package com.itheima.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MpConfig {
@Bean
public MybatisPlusInterceptor mpInterceptor() {
//1.定义Mp拦截器
MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
//2.添加乐观锁拦截器
mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return mpInterceptor;
}
}
- 步骤四: 使用乐观锁机制在修改前必须先获取到对应数据的verion方可正常进行
@Test
public void testUpdate() {
// 先通过要修改的数据id将当前数据查询出来
User user = userDao.selectById(3L); //version=3
User user2 = userDao.selectById(3L); //version=3
user2.setName("Jock aaa");
userDao.updateById(user2); //version=>4
user.setName("Jock bbb");
userDao.updateById(user); //verion=3?条件还成立吗?
}
执行结果:
- 第一次selectById
SELECT version FROM user => 3 - 第二次selectById
SELECT version FROM user => 3 - 第一次updateById
UPDATE user SET version = 4 WHERE version = 3, 执行成功, version为4 - 第二次updateById
UPDATE user SET version = 4 WHERE version = 3, 条件不成立, 更新记录不成功
乐观锁的本质:在执行update操作时,对version的版本进行校验和更新
7.代码生成器(基于数据库)
如果只给一张表的字段信息,能够推演出Domain、Dao层的代码?
- 步骤一: 添加依赖
<!--代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<!--velocity模板引擎-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
- 步骤二: 编写代码生成器类
package com.itheima;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
public class Generator {
public static void main(String[] args) {
//1. 创建代码生成器对象,执行生成代码操作
AutoGenerator autoGenerator = new AutoGenerator();
//2. 数据源相关配置:读取数据库中的信息,根据数据库表结构生成代码
DataSourceConfig dataSource = new DataSourceConfig();
dataSource.setDriverName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=UTC");
dataSource.setUsername("root");
dataSource.setPassword("root");
autoGenerator.setDataSource(dataSource);
//3. 执行生成操作
autoGenerator.execute();
}
}
- 步骤三: 运行方法后,会打开一个文件夹,你在文件夹下找到com/baomidou
7.1 自定义配置
- 设置全局配置
//设置全局配置
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setOutputDir(System.getProperty("user.dir")+"/mybatisplus_04_generator/src/main/java"); //设置代码生成位置
globalConfig.setOpen(false); //设置生成完毕后是否打开生成代码所在的目录
globalConfig.setAuthor("黑马程序员"); //设置作者
globalConfig.setFileOverride(true); //设置是否覆盖原始生成的文件
globalConfig.setMapperName("%sDao"); //设置数据层接口名,%s为占位符,指代模块名称
globalConfig.setIdType(IdType.ASSIGN_ID); //设置Id生成策略
autoGenerator.setGlobalConfig(globalConfig);
- 设置包名相关配置
//设置包名相关配置
PackageConfig packageInfo = new PackageConfig();
packageInfo.setParent("com.aaa"); //设置生成的包名,与代码所在位置不冲突,二者叠加组成完整路径
packageInfo.setEntity("domain"); //设置实体类包名
packageInfo.setMapper("dao"); //设置数据层包名
autoGenerator.setPackageInfo(packageInfo);
- 策略设置
//策略设置
StrategyConfig strategyConfig = new StrategyConfig();
strategyConfig.setInclude("tbl_user"); //设置当前参与生成的表名,参数为可变参数
strategyConfig.setTablePrefix("tbl_"); //设置数据库表的前缀名称,模块名 = 数据库表名 - 前缀名 例如: User = tbl_user - tbl_
strategyConfig.setRestControllerStyle(true); //设置是否启用Rest风格
strategyConfig.setVersionFieldName("version"); //设置乐观锁字段名
strategyConfig.setLogicDeleteFieldName("deleted"); //设置逻辑删除字段名
strategyConfig.setEntityLombokModel(true); //设置是否启用lombok
autoGenerator.setStrategy(strategyConfig);
8.通用Service
MyBatisPlus除了提供通用Mapper, 还提供通用的Service层, 帮助我们简化业务层的开发, 提高开发效率
- 步骤一: 业务层接口继承IService接口
public interface IUserService extends IService<User>{
}
- 步骤二: 业务实现类, 实现接口的同时, 继承ServiceImpl类
@Service("userService")
public class UserServiceImpl extends ServiceImpl<UserMapper , User> implements IUserService{
}
此处的UserMapper需要继承BaseMapper(即通用mapper)