Mybatis-Plus学习笔记

目录

一、MyBatis-Plus简介

二、MyBatisPlus使用的基本流程:

(1)引入MybatisPlus依赖,代替MyBatis依赖

(2)自定义Mapper继承BaseMapper

​编辑(3)在实体类上添加注解声明表信息

(4)在application.yml中根据需要添加配置

三、核心功能

1、条件构造器

(1)QueryWrapper

(2)UpdateWrapper

(3)基于Lambda的Wrapper

2、自定义SQL(擅长处理where更新条件)

3、IService接口

4、LambdaQuery和LambdaUpdate

lambdaUpdate实现

5、批量新增 & 批处理方案性能测试

拓展:rewriteBatchedStatements=true 和 allowMultiQueries=true 的区别

@RequiredArgsConstructor注解

四、扩展功能

1、代码生成

(1)安装插件

(2)使用步骤

(3)代码生成器配置

2、静态工具

3、逻辑删除

(1)介绍

注意:只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。

(2)@TableLogic

4、枚举处理器

(1)定义枚举,标记@EnumValue

(2)配置枚举处理器

5、JSON类型处理器

(1)定义接收Json的实体类

(2)指定类型处理器

6、yaml配置加密

(1)生成密钥        

(2)修改配置

(3)配置密钥运行参数

(4)实现原理

7、自动填充字段

(1)配置自动填充处理器

(2)添加@TableField的fill属性

五、插件功能

1、分页插件

(1)引入依赖

(2)配置分页内置拦截器

(3)分页API

2、通用分页实体

(1)实体类设计

(2)开发接口

(3)改造PageDTO实体

(4)改造PageResult实体


一、MyBatis-Plus简介


MyBatis-Plus 是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

  • 支持的数据库

  • 框架结构


MyBatis-Plus官网:https://baomidou.com/

参考文档:https://mybatis.plus/ (网站访问速度稍慢,建议直接看官网文档)

二、MyBatisPlus使用的基本流程:
 

(1)引入MybatisPlus依赖,代替MyBatis依赖


MyBatisPlus官方提供了starter,其中集成了Mybatis和MybatisPlus的所有功能,并且实现了自动装配效果。

<!-- springboot2的mybatis-plus依赖 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.9</version>
</dependency>



注意:如果是springboot3,引入的是mybatis-plus-spring-boot3-starter依赖。

<!-- springboot3的mybatis-plus依赖 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.9</version>
</dependency>


(2)自定义Mapper继承BaseMapper


(3)在实体类上添加注解声明表信息

MybatisPlus底层通过反射,根据PO实体的信息来推断出表的信息,从而生成SQL的。默认情况下(约定):

  • MybatisPlus会把PO实体的类名驼峰转下划线作为表名
  • MybatisPlus会把PO实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型
  • MybatisPlus会把名为id的字段作为主键

但很多情况下,默认的实现与实际场景不符(实际情况与MP的约定不符合时使用),因此MybatisPlus提供了一些注解便于我们声明表信息。

  • @TableName:用来指定表名
  • @Tableld:用来指定表中的主键字段信息
  • @TableField:用来指定表中的普通字段信息

常见注解:

(4)在application.yml中根据需要添加配置

常见配置:

三、核心功能

1、条件构造器

除了新增以外,修改、删除、查询的SQL语句都需要指定where条件。因此BaseMapper中提供的相关方法除了以id作为where条件以外,还支持更加复杂的where条件。

参数中的Wrapper就是条件构造的抽象类,其下有很多默认实现,继承关系如图:

Wrapper的子类AbstractWrapper提供了where中包含的所有条件构造方法:

而QueryWrapper在AbstractWrapper的基础上拓展了一个select方法,允许指定查询字段:

而UpdateWrapper在AbstractWrapper的基础上拓展了一个set方法,允许指定SQL中的SET部分:

(1)QueryWrapper

无论是修改、删除、查询,都可以使用QueryWrapper来构建查询条件。

示例:

// 查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段
@Test
void testQueryWrapper() {// 1.构建查询条件 where username like "%o%" AND balance >= 1000QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.select("id", "username", "info", "balance").like("username", "o").ge("balance", 1000);// 2.查询数据List<User> userList = userMapper.selectList(queryWrapper);userList.forEach(System.out::println);
}
// 更新用户名为jack的用户的余额为2000。
@Test
void testUpdateByQueryWrapper() {// 1.设置要更新的数据User user = new User();user.setBalance(2000);// 2.构建更新条件 where username = "Jack"QueryWrapper<User> queryWrapper = new QueryWrapper<User>().eq("username", "Jack");// 3.执行更新,user中非null字段都会作为set语句System.out.println(userMapper.update(user, queryWrapper) > 0);
}

(2)UpdateWrapper

基于BaseMapper中的update方法更新时只能直接赋值,对于一些复杂的需求就难以实现。

示例:

@Test
void testUpdateWrapper() {List<Long> ids = List.of(1L, 2L, 4L);// 1.生成SQLUpdateWrapper<User> updateWrapper = new UpdateWrapper<User>().setSql("balance = balance - 200")  // SET balance = balance - 200.in("id", ids); // WHERE id in (1, 2, 4)// 2.基于UpdateWrapper中的setSql来更新System.out.println(userMapper.update(updateWrapper) > 0);
}

(3)基于Lambda的Wrapper
  • LambdaQueryWrapper,对应QueryWrapper
  • LambdaUpdateWrapper,对应UpdateWrapper

示例:

@Test
void testLambdaUpdateWrapper() {List<Long> ids = List.of(1L, 2L, 4L);// 1.生成SQLLambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<User>().setSql("balance = balance - 200")  // SET balance = balance - 200.in(User::getId, ids); // WHERE id in (1, 2, 4)// 2.基于UpdateWrapper中的setSql来更新System.out.println(userMapper.update(lambdaUpdateWrapper) > 0);
}@Test
void testLambdaQueryWrapper() {// 1.构建查询条件 where username like "%o%" AND balance >= 1000LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.select(User::getId, User::getUsername, User::getInfo, User::getBalance).like(User::getUsername, "o").ge(User::getBalance, 1000);// 2.查询数据List<User> userList = userMapper.selectList(queryWrapper);userList.forEach(System.out::println);
}

总结:

  • QueryWrapper和LambdaQueryWrapper通常用来构建select、delete、update的where条件部分
  • UpdateWrapper和LambdaUpdateWrapper通常只有在set语句比较特殊的情况才使用
  • 尽量使用LambdaQueryWrapper和LambdaUpdateWrapper避免硬编码

2、自定义SQL(擅长处理where更新条件)

  • 问题引出

把Mapper层的sql语句写在Service层了,这在某些企业也是不允许的,因为SQL语句最好都维护在持久层,而不是业务层。

示例:

@Test
void testCustomSQLUpdate() {// 更新条件List<Long> ids = List.of(1L, 2L, 4L);int amount = 200;// 定义条件LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().in(User::getId, ids);// 调用自定义SQL方法userMapper.updateBalanceByIds(wrapper, amount);
}
  • ${ew.customSqlSegment}:为自定义SQL片段,@Param("ew")其中参数ew必须叫这个,如果忘记了也可以用baomidou包下的常量类Constants.WRAPPER,其值等于"ew"

3、IService接口

MybatisPlus不仅提供了BaseMapper,还提供了通用的Service接口及默认实现,封装了一些常用的service模板方法。

继承+实现

由于Service中经常需要定义与业务有关的自定义方法,因此我们不能直接使用IService,而是自定义Service接口,然后继承MP的IService接口以拓展方法。让自定义的ServiceImpl实现类实现自定义的Service接口,同时继承MP的默认实现类 ServiceImpl,同时,这样就不用自己实现IService接口中的方法了。

代码示例:

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;public interface IUserService extends IService<User> {
}import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import org.springframework.stereotype.Service;@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
}

注意:ServiceImpl<M, T>接口的泛型参数中,M是继承了BaseMapper的Mapper接口,T是PO实体类。

4、LambdaQuery和LambdaUpdate

IService中还提供了Lambda功能来简化我们的复杂查询及更新功能。

代码示例:

public interface IUserService extends IService<User> {List<User> queryUserByCondition(UserQueryDTO queryDTO);
}// 基于Lambda查询
@Override
public List<User> queryUserByCondition(UserQueryDTO queryDTO) {String name = queryDTO.getName();Integer status = queryDTO.getStatus();Integer minBalance = queryDTO.getMinBalance();Integer maxBalance = queryDTO.getMaxBalance();return lambdaQuery().like(name != null, User::getUsername, name).eq(status != null, User::getStatus, status).ge(minBalance != null, User::getBalance, minBalance).le(maxBalance != null, User::getBalance, maxBalance).list();
}

MP对LambdaQueryWrapper和LambdaUpdateWrapper的用法进一步做了简化。我们无需自己通过new的方式来创建Wrapper,而是直接调用lambdaQuery和lambdaUpdate方法。在组织查询条件的时候,我们加入了name != null这样的参数,意思就是当条件成立时才会添加这个查询条件,类似Mybatis的mapper.xml文件中的<if>标签。这样就实现了动态查询条件效果了。

MybatisPlus会根据链式编程的最后一个方法来判断最终的返回结果。lambdaQuery方法中除了可以构建条件,还需要在链式编程的最后添加一个查询结果,list()表示查询结果返回一个List集合。可选的常用方法有:

  • one():最多1个结果
  • list():返回集合结果
  • count():返回计数结果
  • exist():返回查询的结果是否存在

lambdaUpdate实现

代码示例:

@Override
@Transactional
public void deductBalanceById(Long id, Integer money) {// 查询用户User user = getById(id);// 校验用户状态if (user == null || user.getStatus() == 2) {throw new RuntimeException("用户状态异常!");}// 校验余额是否充足if (user.getBalance() < money) {throw new RuntimeException("用户余额不足!");}// 扣减余额//baseMapper.deductBalance(id, money);int remainBalance = user.getBalance() - money;lambdaUpdate().set(User::getBalance, remainBalance)   // 更新余额.set(remainBalance == 0, User::getStatus, 2)    // 动态判断是否更新status.eq(User::getBalance, user.getBalance())    // CAS乐观锁.eq(User::getId, id)   // 根据id扣减对应用户的余额.update();  // 注意:LambdaUpdate做复杂更新时,最后必须记得加上.update()进行更新操作
}

5、批量新增 & 批处理方案性能测试

需求:批量插入10万条用户数据,并作出对比。

  • 方式一:普通for循环逐条插入
  • 方式二:IService的批量插入(默认不开启 jdbc 批处理参数)
  • 方式三:开启rewriteBatchedStatements=true参数

方式一:执行结果耗时大约为551.9秒

/*** 10w次插入意味着10w次网络请求,耗时最慢*/
@Test
void testSaveOneByOne() {long b = System.currentTimeMillis();for (int i = 1; i <= 100000; i++) {userService.save(buildUser(i));}long e = System.currentTimeMillis();System.out.println("耗时:" + (e - b));
}private User buildUser(int i) {User user = new User();user.setUsername("user_" + i);user.setPassword("123");user.setPhone("" + (18688190000L + i));user.setBalance(2000);user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");user.setCreateTime(LocalDateTime.now());user.setUpdateTime(user.getCreateTime());return user;
}

方式二:执行结果耗时大约为27.6秒,打包逐条插入,从网络请求层面大大减少了耗时。

/*** MP批处理采用的是JDBC底层的预编译方案PreparedStatement,将1000条数据统一打包执行save一并提交到MySQL,每1000条发送一次网络请求,插入100次共发送100次网络请求* MP如果不加JDBC连接参数rewriteBatchedStatements=true,底层还是打包逐条插入,只不过是从网络请求数量上减少了耗时* 而加上了MySQL的这个开启批处理参数后,MP调用的JDBC底层的批处理才能真正变成一次性批量插入多条数据*/
@Test
void testSaveBatch() {// 因为一次性new 10万条数据占用内存太多,并且向数据库请求的数据包有上限大小限制(一次网络传输的数据量是有限的)// 所以我们每次批量插入1000条件,插入100次即10万条数据// 准备一个容量为1000的集合List<User> list = new ArrayList<>(1000);long b = System.currentTimeMillis();for (int i = 1; i <= 100000; i++) {// 添加一个userlist.add(buildUser(i));// 每1000条批量插入一次if (i % 1000 == 0) {// 批量插入userService.saveBatch(list);// 清空集合,准备下一批数据list.clear();}}long e = System.currentTimeMillis();System.out.println("耗时:" + (e - b));
}

方式三:开启参数后,测试耗时大约为6.5秒

利用MP的jdbc批处理,只不过MySQL本身默认没有开启这个批处理参数rewriteBatchedStatements=true,该参数在MySQL 3.1.13版本开始引入,默认值为false不开启。

spring:datasource:url: jdbc:mysql://127.0.0.1:3307/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=truedriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: 123456

批处理方案总结:

  • 普通for循环逐条插入速度极差,不推荐
  • MP的批量新增,基于预编译的批处理,性能不错
  • 配置jdbc参数,开启rewriteBatchedStatements,性能最好
拓展:rewriteBatchedStatements=true 和 allowMultiQueries=true 的区别
  • rewriteBatchedStatements是重写sql语句达到发送一次sql的请求效果;allowMultiQueries是在mapper.xml中使用;分号分隔多条sql,允许多条语句一起发送执行的。
  • 对于insert批处理操作,开启rewriteBatchedStatements=true,驱动则会把多条sql语句重写成一条sql语句然后再发出去;而对于update和delete批处理操作,开启allowMultiQueries=true,驱动所做的事就是把多条sql语句累积起来再一次性发出去。

@RequiredArgsConstructor注解

@RequiredArgsConstructor 是 Project Lombok 库提供的一个注解,它用于在编译时自动生成一个包含所有 “必需”字段 的构造函数。

哪些字段被认为是“必需的”?

  1. 所有被 final 关键字修饰的字段:这些字段必须在对象创建时被初始化。

  2. 所有被 @NonNull 注解标记且未初始化的字段:这些字段不能为 null,因此也必须在构造函数中初始化。

对于普通的、非 final 且没有 @NonNull 约束的字段,不会被包含在这个生成的构造函数中。

四、扩展功能

1、代码生成

(1)安装插件

在idea的plugins市场中搜索并安装MyBatisPlus插件:

(2)使用步骤

我们利用插件生成一下。 首先需要配置数据库地址,在Idea顶部菜单中,找到other,选择Config Database,在弹出的窗口中填写数据库连接的基本信息:

点击OK保存。然后再次点击Idea顶部菜单中的other,然后选择Code Generator,在弹出的表单中填写信息:

最终,代码自动生成到指定的位置了

(3)代码生成器配置

如果不想用图形化界面方式配置生成代码,使用MyBatis-Plus官网提供的代码生成器模板也是可以的,但需要自己填写配置信息。

因为MP代码生成更新迭代速度很快,若本文的API被弃用,请以官网最新版本API为准:

MyBatis-Plus新代码生成器:https://baomidou.com/guides/new-code-generator/

代码生成器配置:https://baomidou.com/reference/new-code-generator-configuration/

  • 引入依赖
<!-- MP代码生成器 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.5.9</version>
</dependency>
  • 代码生成模板配置示例
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;import java.util.Collections;
import java.util.List;public class CodeGenerator {/*** 数据库链接地址**/private static final String JDBC_URL_MAN = "jdbc:mysql://xxxxx:3306/xxx?useUnicode=true&characterEncoding=UTF-8";/*** 数据库登录账号**/private static final String JDBC_USER_NAME = "xx";/*** 数据库登录密码**/private static final String JDBC_PASSWORD = "xxxx";public static void main(String[] args) {String dir = "\\xx\\xxx";String tablePrefix = "tb_";List<String> tables = List.of("tb_user");FastAutoGenerator.create(JDBC_URL_MAN, JDBC_USER_NAME, JDBC_PASSWORD).globalConfig(builder -> {builder.author("Aizen")                                                           // 作者.outputDir(System.getProperty("user.dir") + dir + "\\src\\main\\java")    // 输出路径(写到java目录).enableSwagger()                                                          // 开启swagger.commentDate("yyyy-MM-dd");                                       // 设置注释日期格式,默认值: yyyy-MM-dd}).packageConfig(builder -> {builder.parent("com.{company}")     // 设置父包名.moduleName("{model}")      // 设置父包模块名.entity("domain")           // 设置实体类包名.service("service")         // 设置Service接口包名.serviceImpl("service.impl")// 设置Service实现类包名.controller("controller")   // 设置Controller包名.mapper("mapper")           // 设置Mapper接口文件包名.xml("mappers")             // 设置Mapper XML文件包名.pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + dir + "\\src\\main\\resources\\mapper"));}).strategyConfig(builder -> {builder.addInclude(tables)                              // 设置需要生成的表名.addTablePrefix(tablePrefix)                    // 设置表前缀.serviceBuilder()                               // 设置 Service 层模板.formatServiceFileName("%sService").formatServiceImplFileName("%sServiceImpl").entityBuilder()                                // 设置实体类模板.enableLombok()                                 // 启用 Lombok.logicDeleteColumnName("deleted")               // 逻辑删除字段名(数据库字段).enableTableFieldAnnotation()                   // 开启生成实体时生成字段注解.controllerBuilder().formatFileName("%sController").enableRestStyle()                              // 启用 REST 风格.mapperBuilder()                                // Mapper 策略配置.enableBaseResultMap()                          // 生成通用的resultMap.superClass(BaseMapper.class)                   // 设置父类.formatMapperFileName("%sMapper").enableMapperAnnotation().formatXmlFileName("%sMapper");})//.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板.execute();     // 执行生成}
}

2、静态工具

有的时候Service之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致,也可以帮助我们实现CRUD功能:

因为静态方法无法读取类上的泛型,所以MP在使用静态工具读取表信息时,需要传入PO实体类的Class字节码,MP再通过反射获取到表信息。其中新增和修改的方法由于需要传入实体类对象,因此不用传入实体类的Class字节码。

代码示例:

@Test
void testDbGet() {User user = Db.getById(1L, User.class);System.out.println(user);
}@Test
void testDbList() {// 利用Db实现复杂条件查询List<User> list = Db.lambdaQuery(User.class).like(User::getUsername, "o").ge(User::getBalance, 1000).list();list.forEach(System.out::println);
}@Test
void testDbUpdate() {Db.lambdaUpdate(User.class).set(User::getBalance, 2000).eq(User::getUsername, "Rose");
}

3、逻辑删除

(1)介绍

对于一些比较重要的数据,我们往往会采用逻辑删除的方案,即:

  • 在表中添加一个字段标记数据是否被删除,逻辑删除字段的属性通常是IntegerBoolean类型。
  • 当删除数据时把标记置为1,1表示已删除
  • 查询时只查询标记为0的数据,0表示未删除

同理更新操作也需要加上deleted = 0,所以一旦采用了逻辑删除,所有的查询、删除、更新逻辑都要跟着变化,非常麻烦。

为了解决这个问题,MybatisPlus就添加了对逻辑删除的支持。无需改变方法调用的方式,而是在底层帮我们自动修改CRUD的语句。我们只需要在application.yaml文件中配置逻辑删除的字段名称和值即可。
 

注意:只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。

代码示例:

@SpringBootTest
class IAddressServiceTest {@Autowiredprivate IAddressService addressService;@Testvoid testLogicDelete() {// 删除addressService.removeById(59L);// 查询Address address = addressService.getById(59L);System.out.println("address = " + address);}
}

(2)@TableLogic

  • @TableLogic注解用于标记实体类中的逻辑删除字段。
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("address")
public class Address implements Serializable {private static final long serialVersionUID = 1L;@TableId(value = "id", type = IdType.AUTO)private Long id;// 省略.../*** 逻辑删除*/@TableLogic // @TableLogic注解用于标记实体类中的逻辑删除字段//@TableLogic(value = "0", delval = "1") // value表示默认逻辑未删除值,delval表示默认逻辑删除值 (这两个值可无、会自动获取全局配置)@TableField("deleted")private Boolean deleted;
}

使用这种方式就不用在application.yaml中配置MP逻辑删除字段了,直接在逻辑删除字段属性上加该注解即可。

总结:逻辑删除本身也有自己的问题,比如

  • 会导致数据库表垃圾数据越来越多,从而影响查询效率
  • SQL中全都需要对逻辑删除字段做判断,影响查询效率

因此,不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。

实际业务应该有回收站的逻辑处理数据

4、枚举处理器

当实体类属性是枚举类型,在与数据库的字段类型做转换时,底层默认使用的是MyBatis提供的EnumOrdinalTypeHandler枚举类型处理器。

但是这个并不好用,所以MP对类型处理器做了增强,其中增强后的枚举处理器叫MybatisEnumTypeHandler,JSON处理器叫AbstractJsonTypeHandler

(1)定义枚举,标记@EnumValue

代码示例:

import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;@Getter
public enum UserStatus {NORMAL(1, "正常"),FREEZE(2, "冻结");@EnumValueprivate final int value;private final String desc;UserStatus(int value, String desc) {this.value = value;this.desc = desc;}
}

要让MybatisPlus处理枚举与数据库类型自动转换,我们必须告诉MybatisPlus,枚举中的哪个字段的值作为数据库值。 MybatisPlus提供了@EnumValue注解来标记枚举属性值。因此我们需要给枚举中与数据库字段类型对应的属性值添加@EnumValue注解。

(2)配置枚举处理器

在application.yaml文件中配置枚举处理器

mybatis-plus:configuration:default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler

如果我们想向前端返回指定的枚举属性,例如value状态值desc描述,SpringMVC负责处理响应数据,它在底层处理Json时用的是jackson,所以我们只需要使用jackson提供的@JsonValue注解,来标记JSON序列化后展示的字段。

代码示例:

import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;@Getter
public enum UserStatus {NORMAL(1, "正常"),FREEZE(2, "冻结");@EnumValueprivate final int value;@JsonValueprivate final String desc;UserStatus(int value, String desc) {this.value = value;this.desc = desc;}
}

效果:

5、JSON类型处理器

数据库的user表中有一个info字段,是JSON类型

这样一来,我们要读取info中的属性时就非常不方便。如果要方便获取,info的类型最好是一个Map或者实体类。而一旦我们把info改为对象类型,就需要在写入数据库时手动转为String,再读取数据库时,手动转换为对象,这会非常麻烦。

因此MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler处理器(SpringMVC底层默认也是使用的这个类)。

(1)定义接收Json的实体类

首先定义一个单独实体类UserInfo来与info字段的属性匹配

@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")  // 为了方便构建对象,为有参构造提供静态方法,名为of,UserInfo.of()
public class UserInfo {private Integer age;private String intro;private String gender;
}

(2)指定类型处理器


将User和UserVO类的info字段修改为UserInfo类型,并声明类型处理器@TableField(typeHandler = JacksonTypeHandler.class)。另外,将info改为对象类型后出现对象嵌套,在复杂嵌套查询时需要使用resultMap结果集映射,否则无法映射。所以还需要再@TableName注解中添加autoResultMap=true确保能够正常映射。

/*** 详细信息*/@TableField(typeHandler = JacksonTypeHandler.class)private UserInfo info;

如果启动mapper.xml报错,在info字段后加上, typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler就解决了。

效果:

6、yaml配置加密

目前我们配置文件中的很多参数都是明文,如果开发人员发生流动,很容易导致敏感信息的泄露。所以MybatisPlus支持配置文件的加密和解密功能。

以数据库的用户名和密码为例:

(1)生成密钥        

首先,我们利用MP提供的AES工具生成一个随机秘钥,然后对用户名、密码加密。

import com.baomidou.mybatisplus.core.toolkit.AES;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;import static org.junit.jupiter.api.Assertions.*;@SpringBootTest
class MpDemoApplicationTest {public static final String USERNAME = "root";public static final String PASSWORD = "123456";@Testvoid testEncrypt() {// 生成 16 位随机 AES 密钥String randomKey = AES.generateRandomKey();System.out.println("randomKey = " + randomKey); // randomKey = 7pSEa6F9TnYacTNJ// 利用密钥对用户名加密String username = AES.encrypt(USERNAME, randomKey);System.out.println("username = " + username);   // username = O4Yq+WKYGlPW5t8QvgrhUQ==// 利用密钥对用户名加密String password = AES.encrypt(PASSWORD, randomKey);System.out.println("password = " + password);   // password = cDYHnWysq07zUIAy1tcbRQ==}
}
(2)修改配置

修改application.yaml文件,把jdbc的用户名、密码修改为刚刚加密生成的密文。

spring:datasource:url: jdbc:mysql://127.0.0.1:3307/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=truedriver-class-name: com.mysql.cj.jdbc.Driverusername: mpw:O4Yq+WKYGlPW5t8QvgrhUQ== # 密文要以 mpw:开头password: mpw:cDYHnWysq07zUIAy1tcbRQ== # 密文要以 mpw:开头
(3)配置密钥运行参数

在启动项目的时候,需要把刚才生成的AES秘钥添加到Jar启动参数中,像这样:

--mpw.key=7pSEa6F9TnYacTNJ,新版本idea添加Program arguments中设置,界面如下

单元测试的时候不能添加启动参数,所以要在测试类的注解上配置:@SpringBootTest(args = "--mpw.key=7pSEa6F9TnYacTNJ")

(4)实现原理

SpringBoot提供修改Spring环境后置处理器【EnvironmentPostProcessor】,允许在应用程序之前操作环境属性值,MyBatisPlus对其进行了重写实现。

package com.baomidou.mybatisplus.autoconfigure;import com.baomidou.mybatisplus.core.toolkit.AES;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.SimpleCommandLinePropertySource;import java.util.HashMap;/*** 安全加密处理器** @author hubin* @since 2020-05-23*/
public class SafetyEncryptProcessor implements EnvironmentPostProcessor {@Overridepublic void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {/*** 命令行中获取密钥*/String mpwKey = null;for (PropertySource<?> ps : environment.getPropertySources()) {if (ps instanceof SimpleCommandLinePropertySource) {SimpleCommandLinePropertySource source = (SimpleCommandLinePropertySource) ps;mpwKey = source.getProperty("mpw.key");break;}}/*** 处理加密内容*/if (StringUtils.isNotBlank(mpwKey)) {HashMap<String, Object> map = new HashMap<>();for (PropertySource<?> ps : environment.getPropertySources()) {if (ps instanceof OriginTrackedMapPropertySource) {OriginTrackedMapPropertySource source = (OriginTrackedMapPropertySource) ps;for (String name : source.getPropertyNames()) {Object value = source.getProperty(name);if (value instanceof String) {String str = (String) value;if (str.startsWith("mpw:")) {map.put(name, AES.decrypt(str.substring(4), mpwKey));}}}}}// 将解密的数据放入环境变量,并处于第一优先级上if (CollectionUtils.isNotEmpty(map)) {environment.getPropertySources().addFirst(new MapPropertySource("custom-encrypt", map));}}}
}

7、自动填充字段

MyBatis-Plus提供了一个便捷的自动填充功能,用于在插入或更新数据时自动填充某些字段,如创建时间、更新时间等。

(1)配置自动填充处理器

自动填充功能通过实现 com.baomidou.mybatisplus.core.handlers.MetaObjectHandler 接口来实现。我们需要创建一个类来实现这个接口,并在其中定义插入和更新时的填充逻辑。添加@Component配置自动填充处理器类被Spring管理。

  • MyMetaObjectHandler实现MetaObjectHandler接口
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {log.info("开始插入填充...");// 起始版本 3.3.3(推荐)this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());// Date类型填充//this.strictInsertFill(metaObject, "createTime", () -> new Date(), Date.class);// 起始版本 3.3.0(推荐使用)//this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());// 也可以使用(3.3.0 该方法有bug)//this.fillStrategy(metaObject, "createTime", LocalDateTime.now());}@Overridepublic void updateFill(MetaObject metaObject) {log.info("开始更新填充...");// 起始版本 3.3.3(推荐)this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());// Date类型填充//this.strictUpdateFill(metaObject, "updateTime", Date::new, Date.class);// 起始版本 3.3.0(推荐)//this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());// 也可以使用(3.3.0 该方法有bug)//this.fillStrategy(metaObject, "updateTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug)}
}

(2)添加@TableField的fill属性

在实体类中,你需要使用 @TableField 注解来标记哪些字段需要自动填充,并通过fill属性指定填充的策略。

@Data
@TableName(value = "user", autoResultMap = true)
public class User {// 省略.../*** 创建时间*/@TableField(fill = FieldFill.INSERT)private LocalDateTime createTime;/*** 更新时间*/@TableField(fill = FieldFill.INSERT_UPDATE)private LocalDateTime updateTime;
}

注意事项:

  • 自动填充是直接给实体类的属性设置值,如果属性没有值,入库时会是null。
  • MetaObjectHandler 提供的默认方法策略是:如果属性有值则不覆盖,如果填充值为 null 则不填充。
  • 字段必须声明 @TableField 注解,并设置 fill 属性来选择填充策略。
  • 在 update(T entity, Wrapper<T> updateWrapper) 时,entity 不能为空,否则自动填充失效。
  • 在 update(Wrapper<T> updateWrapper) 时不会自动填充,需要手动赋值字段条件。
  • 使用 strictInsertFill 或 strictUpdateFill 方法可以根据注解 FieldFill.xxx、字段名和字段类型来区分填充逻辑。如果不需区分,可以使用 fillStrategy 方法。

五、插件功能

MybatisPlus提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:

  • PaginationInnerInterceptor:自动分页
  • TenantLineInnerInterceptor:多租户
  • DynamicTableNameInnerInterceptor:动态表名
  • OptimisticLockerInnerInterceptor:乐观锁
  • IllegalSQLInnerInterceptor:sql 性能规范
  • BlockAttackInnerInterceptor:防止全表更新与删除

注意:使用多个分页插件的时候需要注意插件定义顺序,建议使用顺序如下:

  • 多租户,动态表名
  • 分页,乐观锁
  • sql 性能规范,防止全表更新与删除

1、分页插件

在未引入分页插件的情况下,MybatisPlus是不支持分页功能的,IServiceBaseMapper中的分页方法都无法正常起效。 所以,我们必须配置分页插件。

(1)引入依赖

⚠ 注意,MyBatisPlus于 v3.5.9 起,PaginationInnerInterceptor已分离出来。如需使用,则需单独引入mybatis-plus-jsqlparser依赖!

<!-- MP分页插件 jdk 11+ 引入可选模块 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-jsqlparser</artifactId><version>3.5.9</version>
</dependency>
<!-- MP分页插件 jdk 8+ 引入可选模块 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
</dependency>

(2)配置分页内置拦截器

新建一个配置类MyBatisConfig

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** MyBatis配置类*/
@Configuration
public class MybatisConfig {/*** MP拦截器*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {// 初始化核心插件MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加分页插件PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);paginationInnerInterceptor.setMaxLimit(1000L);  // 设置单页分页条数最大限制interceptor.addInnerInterceptor(paginationInnerInterceptor);return interceptor;}
}

(3)分页API

代码示例:

@Test
void testPageQuery() {// 准备分页条件int pageNo = 1, pageSize = 2;   // 页码、每页查询条数// 查询条件:无// 分页条件Page<User> page = Page.of(pageNo, pageSize);// 排序条件//page.addOrder(new OrderItem("id", true)); // MP老版本排序条件写法,true为升序,false为降序page.addOrder(OrderItem.desc("balance"));   // 新版MP直接用OrderItem的静态方法page.addOrder(OrderItem.asc("id")); // 可以添加多个排序条件// 分页查询page = userService.page(page);  // 这里返回的page对象其实和上面是同一个地址,只不过是封装好分页查询结果的page对象// 获取page中的查询结果long total = page.getTotal();   // 总条数System.out.println("total = " + total);long pages = page.getPages();   // 总页数System.out.println("pages = " + pages);List<User> records = page.getRecords(); // 分页数据records.forEach(System.out::println);
}

效果:

2、通用分页实体

现在要实现一个用户分页查询的接口,接口规范如下:

这里需要用到4个实体:

  • PageDTO:通用分页查询条件的实体,包含分页页码、每页查询条数、排序字段、是否升序
  • UserQueryDTO:接收用户查询条件实体,为了实现分页功能去继承PageDTO
  • PageResult:分页结果实体,包含总条数、总页数、当前页数据
  • UserVO:响应用户页面视图实体,将用户VO集合封装到PageResult中返回

(1)实体类设计

分页条件不仅仅用户分页查询需要,以后其它业务也都有分页查询的需求。因此建议将分页查询条件单独定义为一个PageDTO实体

  • PageDTO 通用分页查询条件的实体
@Data
@ApiModel(description = "通用分页查询实体")
public class PageDTO {@ApiModelProperty("页码")private Integer pageNo;@ApiModelProperty("每页查询条数")private Integer pageSize;@ApiModelProperty("排序字段")private String sortBy;@ApiModelProperty("是否升序")private Boolean isAsc;
}

  • UserQueryDTO 接收用户查询条件实体,继承PageDTO
@EqualsAndHashCode(callSuper = true)    // 当判断相等时先考虑父类属性再考虑子类属性,就是分页的时候把分页条件作为数据请求的的前提,然后再考虑查到了哪些匹配的数据
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQueryDTO extends PageDTO {@ApiModelProperty("用户名关键字")private String name;@ApiModelProperty("用户状态:1-正常,2-冻结")private Integer status;@ApiModelProperty("余额最小值")private Integer minBalance;@ApiModelProperty("余额最大值")private Integer maxBalance;
}
  • PageResult 分页结果实体
@Data
@ApiModel(description = "分页结果")
public class PageResult<T> {@ApiModelProperty("总条数")private Long total;@ApiModelProperty("总页数")private Long pages;@ApiModelProperty("结果集合")private List<T> list;
}
  • UserVO 响应用户页面视图实体,将用户VO集合封装到PageResult中返回,之前已经定义过了
@Data
@ApiModel(description = "用户VO实体")
public class UserVO {@ApiModelProperty("用户id")private Long id;@ApiModelProperty("用户名")private String username;@ApiModelProperty("详细信息")private UserInfo info;@ApiModelProperty("使用状态(1正常 2冻结)")private UserStatus status;@ApiModelProperty("账户余额")private Integer balance;@ApiModelProperty("用户的收获地址")private List<AddressVO> addresses;
}

(2)开发接口

Controller层:UserController

// in UserController
@ApiOperation("根据条件分页查询用户接口")
@GetMapping("/condition/page")
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {return userService.queryUserByConditionAndPage(queryDTO);
}

Service层:IUserService

// in IUserService
PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO);// in UserServiceImpl
@Override
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {// 构建分页条件Page<User> page = Page.of(queryDTO.getPageNo(), queryDTO.getPageSize());// 构建排序条件if (StrUtil.isNotBlank(queryDTO.getSortBy())) { // 如果排序字段不为空page.addOrder(new OrderItem().setColumn(queryDTO.getSortBy()).setAsc(queryDTO.getIsAsc()));}else { // 如果排序字段为空,默认按照更新时间排序page.addOrder(new OrderItem().setColumn("update_time").setAsc(false));}// 分页查询String name = queryDTO.getName();Integer status = queryDTO.getStatus();Page<User> p = lambdaQuery().like(name != null, User::getUsername, name).eq(status != null, User::getStatus, status).page(page);// 封装VO结果PageResult<UserVO> result = new PageResult<>();result.setTotal(p.getTotal());  // 总条数result.setPages(p.getPages());  // 总页数// 当前页数据List<User> records = p.getRecords();// 其实也可以不用判断,因为如果查到的是空集合,转换完还是空集合,不影响最后的结果if (CollUtil.isEmpty(records)) {result.setList(Collections.emptyList());return result;}// 将用户集合拷贝为用户VO集合List<UserVO> userVOList = BeanUtil.copyToList(records, UserVO.class);result.setList(userVOList);return result;
}

(3)改造PageDTO实体

在刚才的代码中,从PageDTOMybatisPlusPage之间转换的过程还是比较麻烦的。

对于PageDTO构建为MP的分页对象的部分,我们完全可以在PageDTO这个实体内部中定义一个转换方法,简化开发。

一般是封装成工具类进行操作

import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "通用分页查询实体")
public class PageDTO {@ApiModelProperty("页码")private Integer pageNo;@ApiModelProperty("每页查询条数")private Integer pageSize;@ApiModelProperty("排序字段")private String sortBy;@ApiModelProperty("是否升序")private Boolean isAsc;/*** 在PageDTO内部将PageDTO构建为MP的分页对象,并设置排序条件* @param items 排序条件(可以有一个或多个)* @return MP的分页对象* @param <T> MP的分页对象的泛型*/public <T> Page<T> toMpPage(OrderItem... items) {// 构建分页条件Page<T> page = Page.of(pageNo, pageSize);// 构建排序条件if (sortBy != null && sortBy.trim().length() > 0) { // 如果排序字段不为空page.addOrder(new OrderItem().setColumn(sortBy).setAsc(isAsc));}else if (items != null) { // 如果排序字段为空,且传入的默认OrderItem不为空,按照调用者传入的默认OrderItem排序page.addOrder(items);}return page;}/*** 将PageDTO构建为MP的分页对象* 如果调用者不想new OrderItem对象,可以调用该方法,传入默认的排序字段和排序规则即可* @param defaultSortBy 排序字段* @param defaultAsc 排序规则,true为asc,false为desc* @return MP的分页对象* @param <T> MP的分页对象的泛型*/public <T> Page<T> toMpPage(String defaultSortBy, Boolean defaultAsc) {return toMpPage(new OrderItem().setColumn(defaultSortBy).setAsc(defaultAsc));}/*** 将PageDTO构建为MP的分页对象,排序条件按update_time更新时间降序* @return MP的分页对象* @param <T> MP的分页对象的泛型*/public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {return toMpPage("update_time", false);}/*** 将PageDTO构建为MP的分页对象,排序条件按create_time创建时间降序* @return MP的分页对象* @param <T> MP的分页对象的泛型*/public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {return toMpPage("create_time", false);}
}

(4)改造PageResult实体

在查询出分页结果后,数据的非空校验,数据的VO转换都是模板代码,编写起来很麻烦。

我们完全可以将 PO分页对象转换为VO分页结果对象 的逻辑,封装到 PageResult 的内部方法中,简化整个过程。

相当于定义通用业务工具类

@Data
@ApiModel(description = "分页结果")
public class PageResult<T> {@ApiModelProperty("总条数")private Long total;@ApiModelProperty("总页数")private Long pages;@ApiModelProperty("结果集合")private List<T> list;/*** 将PO分页对象转换为VO分页结果对象* @param page PO分页对象* @param clazz 目标VO的字节码对象* @return VO分页结果对象* @param <PO> PO实体* @param <VO> VO实体*/public static <PO, VO> PageResult<VO> of(Page<PO> page, Class<VO> clazz) {// PO分页对象封装为VO结果PageResult<VO> result = new PageResult<>();result.setTotal(page.getTotal());  // 总条数result.setPages(page.getPages());  // 总页数List<PO> records = page.getRecords();    // 当前页数据// 其实也可以不用判断,因为如果查到的是空集合,转换完还是空集合,不影响最后的结果if (CollUtil.isEmpty(records)) {result.setList(Collections.emptyList());return result;}// 将PO集合拷贝为VO集合List<VO> userVOList = BeanUtil.copyToList(records, clazz);result.setList(userVOList);return result;}/*** 将PO分页对象转换为VO分页结果对象* @param page PO分页对象* @param convertor 自定义规则转换器* @return VO分页结果对象* @param <PO> PO实体* @param <VO> VO实体*/public static <PO, VO> PageResult<VO> of(Page<PO> page, Function<PO, VO> convertor) {// PO分页对象封装为VO结果PageResult<VO> result = new PageResult<>();result.setTotal(page.getTotal());  // 总条数result.setPages(page.getPages());  // 总页数List<PO> records = page.getRecords();    // 当前页数据// 其实也可以不用判断,因为如果查到的是空集合,转换完还是空集合,不影响最后的结果if (CollUtil.isEmpty(records)) {result.setList(Collections.emptyList());return result;}// 将PO集合转换为VO集合,转换动作由调用者来传递List<VO> voList = records.stream().map(convertor).collect(Collectors.toList());result.setList(voList);return result;}
}

最终Service层代码:

@Override
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {// 构建分页条件对象Page<User> page = queryDTO.toMpPageDefaultSortByUpdateTimeDesc();// 分页查询String name = queryDTO.getName();Integer status = queryDTO.getStatus();Page<User> p = lambdaQuery().like(name != null, User::getUsername, name).eq(status != null, User::getStatus, status).page(page);// PO分页对象封装为VO结果return PageResult.of(p, UserVO.class);
}

如果是希望自定义PO到VO的转换过程,可以调用重载方法of(Page<PO> page, Function<PO, VO> convertor),convertor的转换器逻辑由调用者去编写传递:

@Override
public PageResult<UserVO> queryUserByConditionAndPage(UserQueryDTO queryDTO) {// 构建分页条件对象Page<User> page = queryDTO.toMpPageDefaultSortByUpdateTimeDesc();// 分页查询String name = queryDTO.getName();Integer status = queryDTO.getStatus();Page<User> p = lambdaQuery().like(name != null, User::getUsername, name).eq(status != null, User::getStatus, status).page(page);// PO分页对象封装为VO结果return PageResult.of(p, user -> {// PO拷贝基础属性得到VOUserVO userVO = BeanUtil.copyProperties(user, UserVO.class);// 对VO进行处理特殊逻辑String username = userVO.getUsername();// 例如用户名脱敏处理userVO.setUsername(username.length() > 2 ? StrUtil.fillAfter(username.substring(0, 2), '*', username.length()) : username.charAt(0) + "*");return userVO;});
}

自定义转换规则的场景,例如:

  • ① PO字段和VO字段不是包含关系,出现字段不一致。
  • ② 对VO中的属性做一些过滤、数据脱敏、加密等操作。
  • ③ 将VO中的属性继续设置数据,例如VO中的address属性,可以查询出用户所属的收获地址,设置后一并返回。

效果:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:
http://www.pswp.cn/web/96602.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/96602.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Day22 用C语言编译应用程序

文章目录1. 保护操作系统5&#xff08;harib19a&#xff09;2. 帮助发现bug&#xff08;harib19b&#xff09;3. 强制结束应用程序&#xff08;harib19c&#xff09;4. 用C语言显示字符串&#xff08;harib19e&#xff09;5. 显示窗口&#xff08;harib19f&#xff09;1. 保护操…

简单学习HTML+CSS+JavaScript

一、HTML HTML被称为 超文本标记语言&#xff0c;是由一系列标签构成的语言。 下面介绍HTML中的标签&#xff1a; &#xff08;一&#xff09;HTML文件基本结构 <!DOCTYPE html><html><head><title>Document</title></head> <body&…

强化学习中重要性采样

PPO 中重要性采样 https://github.com/modelscope/ms-swift/blob/main/docs/source/Instruction/GRPO/GetStarted/GRPO.md乐&#xff0c;这个网页中是的groundtruth是错误的&#xff08;可能是为了防止抄袭&#xff09;。一些例子 0. 池塘养鱼的一个例子 想象一下&#xff0c;你…

《树与二叉树详解:概念、结构及应用》

目录 一. 树的概念和结构 1.1 树的基本概念 1.2 树的结构特点 二. 树的表示方法和实际运用 2.1 孩子 - 兄弟表示法&#xff08;Child-Sibling Representation&#xff09; 2.2 树的实际应用场景 三. 二叉树的概念 3.1 二叉树的核心定义 3.2 二叉树的基本分类 四. 二叉…

Qt/C++,windows多进程demo

1. 项目概述 最近研究了一下Qt/C框架下&#xff0c;windows版本的多进程编写方法&#xff0c;实现了一个小demo。下面详细介绍一下。 MultiProcessDemo是一个基于Qt框架实现的多进程应用程序示例&#xff0c;展示了如何在Windows平台上通过共享内存和事件机制实现进程间通信。该…

Android SystemServer 系列专题【篇五:UserController用户状态控制】

本篇接着SystemServer的启动流程&#xff0c;围绕SystemServer最后阶段关于主用户的启动和解锁的流程&#xff0c;作为切入点&#xff0c;来看看SystemServer是如何讲用户状态同步到所有的系统级服务中。ssm.onStartUserssm.onUnlockingUserssm.onUnlockedUser本篇先介绍UserCo…

推荐使用 pnpm 而不是 npm

npm 的局限性 磁盘空间浪费在 npm 早期版本中&#xff0c;每个项目的node_modules目录都会完整复制所有依赖包&#xff0c;即使多个项目依赖同一个包的相同版本&#xff0c;也会重复存储。这导致磁盘空间被大量占用&#xff0c;随着项目数量的增加&#xff0c;存储成本显著上升…

Transformer实战(18)——微调Transformer语言模型进行回归分析

Transformer实战&#xff08;18&#xff09;——微调Transformer语言模型进行回归分析0. 前言1. 回归模型2. 数据处理3. 模型构建与训练4. 模型推理小结系列链接0. 前言 在自然语言处理领域中&#xff0c;预训练 Transformer 模型不仅能胜任离散类别预测&#xff0c;也可用于连…

【Linux】【实战向】Linux 进程替换避坑指南:从理解 bash 阻塞等待,到亲手实现能执行 ls/cd 的 Shell

前言&#xff1a;欢迎各位光临本博客&#xff0c;这里小编带你直接手撕&#xff0c;文章并不复杂&#xff0c;愿诸君耐其心性&#xff0c;忘却杂尘&#xff0c;道有所长&#xff01;&#xff01;&#xff01;&#xff01; IF’Maxue&#xff1a;个人主页&#x1f525; 个人专栏…

linux常用命令 (3)——系统包管理

博客主页&#xff1a;christine-rr-CSDN博客 ​​​​​ ​​ hi&#xff0c;大家好&#xff0c;我是christine-rr ! 今天来分享一下linux常用命令——系统包管理 目录linux常用命令---系统包管理&#xff08;一&#xff09;Debian 系发行版&#xff08;Ubuntu、Debian、Linux …

YOLOv8 mac-intel芯片 部署指南

&#x1f680; 在 Jupyter Notebook 和 PyCharm 中使用 Conda 虚拟环境&#xff08;YOLOv8 部署指南&#xff0c;Python 3.9&#xff09; YOLOv8 是 Ultralytics 开源的最新目标检测模型&#xff0c;轻量高效&#xff0c;支持分类、检测、分割等多种任务。 在 Mac&#xff08;…

【高等数学】第十一章 曲线积分与曲面积分——第六节 高斯公式 通量与散度

上一节&#xff1a;【高等数学】第十一章 曲线积分与曲面积分——第五节 对坐标的曲面积分 总目录&#xff1a;【高等数学】 目录 文章目录1. 高斯公式2. 沿任意闭曲面的曲面积分为零的条件3. 通量与散度1. 高斯公式 设空间区域ΩΩΩ是由分片光滑的闭曲面ΣΣΣ所围成&#x…

IDEA试用过期,无法登录,重置方法

IDEA过期&#xff0c;重置方法: IntelliJ IDEA 2024.2.0.2 (亲测有效) 最新Idea重置办法!&#xff1a; 方法一&#xff1a; 1、删除C:\Users\{用户名}\AppData\Local\JetBrains\IntelliJIdea2024.2 下所有文件(注意&#xff1a;是子目录全部删除) 2、删除C:\Users\{用户名}\App…

创建用户自定义桥接网络并连接容器

1.创建用户自定义的 alpine-net 网络[roothost1 ~]# docker network create --driver bridge alpine-net 9f6d634e6bd7327163a9d83023e435da6d61bc6cf04c9d96001d1b64eefe4a712.列出 Docker 主机上的网络[roothost1 ~]# docker network ls NETWORK ID NAME DRIVER …

Vue3 + Vite + Element Plus web转为 Electron 应用,解决无法登录、隐藏自定义导航栏

如何在vue3 Vite Element Plus搭好的架构下转为 electron应用呢&#xff1f; https://www.electronjs.org/zh/docs/latest/官方文档 https://www.electronjs.org/zh/docs/latest/ 第一步&#xff1a;安装 electron相关依赖 npm install electron electron-builder concurr…

qt QAreaLegendMarker详解

1. 概述QAreaLegendMarker 是 Qt Charts 模块中的一部分&#xff0c;用于在图例&#xff08;Legend&#xff09;中表示 QAreaSeries 的标记。它负责显示区域图的图例项&#xff0c;通常包含区域颜色样例和对应的描述文字。图例标记和对应的区域图关联&#xff0c;显示区域的名称…

linux 函数 kstrtoul

kstrtoul 函数概述 kstrtoul 是 Linux 内核中的一个函数&#xff0c;用于将字符串转换为无符号长整型&#xff08;unsigned long&#xff09;。该函数定义在 <linux/kernel.h> 头文件中&#xff0c;常用于内核模块中解析用户空间传递的字符串参数。 函数原型 int kstrtou…

LLM(三)

一、人类反馈的强化学习&#xff08;RLHF&#xff09;微调的目标是通过指令&#xff0c;包括路径方法&#xff0c;进一步训练你的模型&#xff0c;使他们更好地理解人类的提示&#xff0c;并生成更像人类的回应。RLHF&#xff1a;使用人类反馈微调型语言模型&#xff0c;使用强…

DPO vs PPO,偏好优化的两条技术路径

1. 背景在大模型对齐&#xff08;alignment&#xff09;里&#xff0c;常见的两类方法是&#xff1a;PPO&#xff1a;强化学习经典算法&#xff0c;OpenAI 在 RLHF 里用它来“用奖励模型更新策略”。DPO&#xff1a;2023 年提出的新方法&#xff08;参考论文《Direct Preferenc…

BLE6.0信道探测,如何重构物联网设备的距离感知逻辑?

在物联网&#xff08;IoT&#xff09;无线通信技术快速渗透的当下&#xff0c;实现人与物、物与物之间对物理距离的感知响应能力已成为提升设备智能高度与人们交互体验的关键所在。当智能冰箱感知用户靠近而主动亮屏显示内部果蔬时、当门禁系统感知到授权人士靠近而主动开门时、…