目录
(3)LambdaQueryWrapper(Lambda表达式查询构造器)
(4)LambdaUpdateWrapper(Lambda表达式更新构造器)
[如何使用 Mp 处理枚举Enums与数据库字段的映射](#如何使用 Mp 处理枚举Enums与数据库字段的映射)
一、Mybatis-plus
1、快速入门
从资料包内导入demo代码
(1)pom文件引入MybatisPlus的起步依赖
MyBatisPlus官方提供了starter,其中集成了Mybatis和MybatisPlus的所有功能,并且实现了自动装配效果
因此我们可以用MybatisPlus的starter代替Mybatis的starter
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency>(2)继承BaseMapper类
- 继承
BaseMapper<User>是为了获得一组功能强大的、通用的数据库操作方法<User>的作用是:为这个泛型接口BaseMapper指定一个具体的类型BaseMapper<User>的意思是:这个 Mapper 是专门用于操作User实体类的(3)把原有的方法替换
测试一下,成功实现!
2、原理解析
原理: Mybatis-plus通过扫描实体类,并基于反射获取【实体类信息】作为【数据库表信息】
反射是 Java 语言的一种机制,它允许程序在运行时获取自身的信息,并能动态操作类或对象的属性、方法。
反射的作用: 在程序运行起来之后 ,通过一个类的完整路径名(如
com.example.User),可以动态地:
获取这个类的所有信息(类名、修饰符、父类、接口等)。
获取这个类中声明的所有字段(字段名、类型、修饰符等)。
获取这个类中声明的所有方法(方法名、参数类型、返回类型等)。
实例化这个类的对象。
读取或修改一个对象的字段值(即使该字段是
private的)。调用一个对象的方法。
📍Mybatis-plus如何利用反射获取表/字段信息?
扫描类路径: MP 会扫描你配置的包路径(如
com.example.mapper),找到所有继承了BaseMapper<T>的接口(如UserMapper)。提取泛型参数: 通过反射 API(如
JavaType),MP 从UserMapper接口上提取出它的泛型参数<User>。这样 MP 就确定了这个 Mapper 要操作的实体类是com.example.entity.User。加载并分析实体类: MP 使用
Class.forName("com.example.entity.User")或类似方法,将User类加载到内存中,并获取一个Class<User>对象。这个Class对象是反射的入口,它包含了User类的完整元数据。获取表名信息:
MP 通过
Class对象的getSimpleName()方法获取类的简单名称"User"。MP 检查该类上是否有
@TableName注解。如果有,则使用注解指定的值作为表名;如果没有,则应用其默认命名策略(例如,++将 类名驼峰"BaseUser"转换为下划线格式"base_user"作为表名++)。获取字段信息:
MP 通过
Class对象的getDeclaredFields()方法,获取User类中声明的所有字段(如id,userName,password)对应的Field对象数组。遍历每个
Field对象:
通过
field.getName()获取字段名(如"userName")。检查字段上是否有
@TableField或@TableId注解,以确定它对应数据库的普通列还是主键列,以及自定义的列名是什么。如果没有注解,则应用默认变量命名策略(例如,++将 变量名驼峰
"userName"转换为"user_name"作为字段名++)。通过
field.getType()获取字段的 Java 类型(如String.class),用于在 SQL 操作时进行类型转换。构建映射关系缓存: MP 将以上分析结果(类
User-> 表user,字段userName-> 列user_name等)缓存起来,形成一个完整的TableInfo对象。之后所有通过UserMapper进行的数据库操作,都会查询这个TableInfo来动态生成正确的 SQL。📍通过上述过程我们明白MP的默认转换规则:
- 类名驼峰→下划线作为表名(如:
BaseUser → base_user)- 变量名驼峰→下划线作为字段名(如:
userName → ``user_name)名为id的变量作为主键📍那如果我们想自定义这些名称,需要用到下面的注解:
@TableName:用来指定表名
@TableId:用来指定表中的主键字段信息
IdType.AUTO:数据库自增长
IdType.INPUT:通过set方法自行输入
IdType.ASSIGN_ID:分配 ID,接口IdentifierGenerator的方法nextId来生成id,默认实现类为DefaultIdentifierGenerator雪花算法
@TableField:用来指定表中的普通字段信息
成员变量名与数据库字段名不一致
成员变量名以is开头,且是布尔值(如:isMarried)
成员变量名与数据库关键字冲突(如:order)
成员变量不是数据库字段(exist = false)
3、条件构造器
Mybatis-plus支持各种复杂的where条件
(1)QueryWrapper(查询条件构造器)
① 查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段。
java@Test void testQueryWrapper(){ // 1.构建查询条件 QueryWrapper<User> wrapper = new QueryWrapper<User>() .select("id","username","info","balance") .like("username","o") .ge("balance",1000); // 2.查询 List<User> users = userMapper.selectList(wrapper); }② 更新用户名为jack的用户的余额为2000。
java@Test void testUpdateWrapper(){ // 1.要更新的数据 User user = new User(); user.setBalance(2000); // 2.更新条件 QueryWrapper<User> wrapper = new QueryWrapper<User>() .eq("username","jack"); // 3.执行更新 userMapper.update(user,wrapper); }(2)UpdateWrapper(更新条件构造器)
更新ID为1,2,4的用户余额,扣200。
java@Test void testUpdateWrapper() { List<Long> ids = List.of(1L, 2L, 4L); UpdateWrapper<User> wrapper = new UpdateWrapper<User>() .setSql("balance = balance - 200") .in("id", ids); userMapper.update(null, wrapper); }(3)LambdaQueryWrapper(Lambda表达式查询构造器)
查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段。
java@Test void testLambdaQueryWrapper() { // 1.构建查询条件 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>() .select(User::getId, User::getUsername, User::getInfo, User::getBalance) .like(User::getUsername, "o") .ge(User::getBalance, 1000); // 2.查询 List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); }(4)LambdaUpdateWrapper(Lambda表达式更新构造器)
javaLambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>(); wrapper.set(User::getName, "李四") .set(User::getAge, 20) .eq(User::getId, 1);推荐优先使用Lambda表达式版本,因为它们在编译期就能发现字段名错误,更加安全可靠。
4、自定义SQL
当MyBatis-Plus自带的条件构造器无法满足复杂需求(如多表关联、复杂计算、使用数据库特有功能)时,就必须使用自定义SQL
sql-- 这是一个简单的多表连接,条件构造器无法直接生成 SELECT u.*, o.order_number, o.amount FROM user u LEFT JOIN order o ON u.id = o.user_id WHERE u.status = 1我们可以利用MyBatisPlus的Wrapper来构建复杂的Where条件,然后自己定义SQL语句中剩下的部分。
📍需求:将id在指定范围的用户(1,2,4)的余额扣减指定值
① 基于Wrapper构建where条件
sql@Test void testCustomSqlWrapper(){ // 1.要更新的数据 List<Long> ids = List.of(1L, 2L, 4L); int amount = 200; // 2.定义条件 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>() .in(User::getId,ids); // 3.自定义SQL方法调用 userMapper.updateBalanceByIds(wrapper,amount); }② 在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew
sqlvoid updateBalanceByIds(@Param("ew") LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);③ 自定义SQL,并使用Wrapper条件
sql<update id="updateBalanceByIds"> UPDATE user SET balance = balance - #{amount} ${ew.customSqlSegment} </update>
5、IService接口
(1)简介
📍为什么要使用 IService?
封装常用业务逻辑:将常见的业务操作(如批量操作、链式查询、分页查询等)进行封装,避免在 Service 中重复编写简单代码。
提供更业务友好的方法:相比 Mapper 的数据库导向的方法,Service 层的方法命名更贴近业务。例如:
saveUser()而不仅仅是insert()
getUserById()而不仅仅是selectById()
removeUserById()而不仅仅是deleteById()实现复杂的批量操作 :提供了 Mapper 层没有的批量操作方法,如
saveBatch()、updateBatchById()等。
- UserService继承IService接口,获得丰富的查询接口
- UserServiceImpl继承ServiceImpl实现类,获得接口所需要实现的实现类
📍如何使用 IService?
第一步:创建 Service 接口,继承 IService
java// 继承 IService<User>,并指定泛型为 User public interface UserService extends IService<User> { // 可以在这里定义自定义的业务方法 User getUserWithDetail(Long id); }第二步:创建 Service 实现类,继承 ServiceImpl
java// 继承 ServiceImpl<Mapper, Entity> // 需要指定:1.对应的 Mapper 接口 2.实体类类型 @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Override public User getUserWithDetail(Long id) { // 自定义业务逻辑 return getById(id); } }(2)基础增删改查
【1】在pom文件引入依赖
XML<!-- swagger --> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-openapi2-spring-boot-starter</artifactId> <version>4.1.0</version> </dependency> <!-- web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>【2】在application文件引入swagger配置
XMLknife4j: enable: true openapi: title: 用户管理接口文档 description: "用户管理接口文档" email: zhanghuyi@itcast.cn concat: 虎哥 url: https://www.itcast.cn version: v1.0.0 group: default: | group-name: default api-rule: package api-rule-resources: - com.itheima.mp.controller【3】导入DTO、VO文件
【4】编写controller层
java@RequestMapping("/users") @RestController @Api(tags = "用户管理接口") @RequiredArgsConstructor public class UserController { private final IUserService userService; @ApiOperation("新增用户接口") @PostMapping public void addUser(@RequestBody UserFormDTO userFormDTO){ // 1.把DTO拷贝到PO User user = BeanUtil.copyProperties(userFormDTO, User.class); // 2.新增 userService.save(user); } @ApiOperation("删除用户接口") @DeleteMapping("{id}") public void deleteUseryId(@ApiParam("用户id") @PathVariable("id")Long id){ userService.removeById(id); } @ApiOperation("根据id查询用户接口") @GetMapping("/{id}") public UserVO queryUserById(@ApiParam("用户id") @PathVariable("id") Long id){ // 1.查询用户PO User user = userService.getById(id); // 2.把PO拷贝到VO return BeanUtil.copyProperties(user, UserVO.class); } @ApiOperation("根据id批量查询用户接口") @GetMapping public List<UserVO> queryUserByIds(@ApiParam("用户id集合") @RequestParam("ids") List<Long> ids){ // 1.查询用户PO List<User> users = userService.listByIds(ids); // 2.把PO拷贝到VO return BeanUtil.copyToList(users, UserVO.class); } }
java@RequiredArgsConstructor public class UserController { private final IUserService userService;
- 这里依赖注入与@Autowired类似,但更安全
- 作用:让 Spring 自动通过构造方法,为
UserController注入一个IUserService的实现类实例,并且保证这个依赖在程序运行期间不可改变(final)。@RequiredArgsConstructor
这是 Lombok 库提供的一个注解。
作用 :它在编译时 会自动为类生成一个构造方法。
生成规则 :这个构造方法会包含所有必须初始化 的字段作为参数。哪些是"必须初始化"的字段呢?就是所有被声明为
final的字段(比如上面的userService)以及被@NonNull注解的字段。【5】上述是基础CRUD,而我们实际开发中会遇到复杂的功能开发,会编写service层
java//controller层 @ApiOperation("扣减用户余额") @PutMapping("/{id}/deduction/{money}") public void deductBalance( @ApiParam("用户id") @PathVariable("id") Long id, @ApiParam("扣减的金额") @PathVariable("money") Integer money) { userService.deductBalance(id,money); }
java//serviceImpl层 /** * 根据id扣减用户余额 * @param id * @param money */ @Override public void deductBalance(Long id, Integer money) { // 1.获取用户信息 User user = getById(id); // 2.校验用户状态 if(user == null || user.getStatus() == 2){ throw new RuntimeException("用户状态异常"); } // 3.查询余额是否充足 if(user.getBalance() < money){ throw new RuntimeException("用户余额不足"); } // 4.扣减余额 baseMapper.deductBalance(id,money); // baseMapper的本质就是被自动注入的 UserMapper实例 }
java// mapper层 void deductBalance(Long id, Integer money);
XML<!-- xml文件--> <update id="deductBalance"> UPDATE user set balance = balance - #{money} where id = #{id} </update>登录swagger文档测试一下:http://localhost:8080/doc.html
(3)Lambda查询
将name、status等参数打包成一个类(从资料中导入)
【1】controller层
java@ApiOperation("动态条件查询用户接口") @GetMapping("/list") public List<UserVO> queryUserByIds(UserQuery query){ // 1.查询用户PO List<User> users = userService.queryUsers(query); // 2.把PO拷贝到VO return BeanUtil.copyToList(users, UserVO.class); }【2】serviceImpl层
java@Override public List<User> queryUsers(UserQuery query) { String name = query.getName(); Integer status = query.getStatus(); Integer minBalance = query.getMinBalance(); Integer maxBalance = query.getMaxBalance(); return lambdaQuery() .like(name != null,User::getUsername,name) .eq(status != null,User::getStatus,status) .gt(minBalance != null,User::getBalance,minBalance) .lt(maxBalance != null,User::getBalance,maxBalance) .list(); }(4)Lambda更新
在serviceImpl层修改之前的deductBalance根据id扣减余额代码
java/** * 根据id扣减用户余额 * @param id * @param money */ @Override public void deductBalance(Long id, Integer money) { // 1.获取用户信息 User user = getById(id); // 2.校验用户状态 if(user == null || user.getStatus() == 2){ throw new RuntimeException("用户状态异常"); } // 3.查询余额是否充足 if(user.getBalance() < money){ throw new RuntimeException("用户余额不足"); } // 4.扣减余额 int remainBalance = user.getBalance() - money; lambdaUpdate() .set(User::getBalance,remainBalance) .set(remainBalance == 0,User::getStatus,2) .eq(User::getId,id) .eq(User::getBalance,user.getBalance()) //乐观锁 .update(); }由于查询和修改余额不是原子性操作,因此容易出现并发问题:
- 两个用户同时扣减该账户余额、出现数据覆盖
因此我们加入乐观锁,也就是.eq(User::getBalance,user.getBalance())这行代码:
乐观锁就是通过版本号或数据校验在更新时检查数据是否被修改,避免并发修改导致的数据不一致问题。
在更新时,检查余额的当前值是否与查询时一致
如果余额未被其他线程修改,说明还没扣呢,更新成功
如果余额已被修改,说明已经有人改过了,不需要修改,更新返回0条记录
但这样容易出现aba问题,但我们这里先不细究
其他线程可能先扣款后又退款,余额数值回到原值
虽然余额数值相同,但中间发生了业务变化
(5)批量新增
📍【第一种】普通for循环插入
javavoid 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)); }
性能极差,不推荐
每次插入都单独执行SQL语句
📍【第二种】MyBatis-Plus的批量新增
javavoid testSaveBatch() { // 我们每次批量插入1000条件,插入100次即10万条数据 // 1.准备一个容量为1000的集合 List<User> list = new ArrayList<>(initialCapacity: 1000); long b = System.currentTimeMillis(); for (int i = 1; i <= 100000; i++) { // 2.添加一个user list.add(buildUser(i)); // 3.每1000条批量插入一次 if (i % 1000 == 0) { userService.saveBatch(list); // 4.清空集合,准备下一批数据 list.clear(); } } long e = System.currentTimeMillis(); System.out.println("耗时:" + (e - b)); }
基于预编译的批处理,性能不错
使用MP的
saveBatch()方法📍【第三种】配置rewriteBatchedStatements参数
在application的datasource中url末尾添加配置
javaspring: datasource: url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 12345678
java&rewriteBatchedStatements=true
在JDBC连接字符串中加入
rewriteBatchedStatements=true,性能最好让MySQL真正支持批量处理
rewriteBatchedStatements=true会让JDBC将多个INSERT语句重写为单个多值INSERT语句类似上图,虽然数据很多,但执行的sql语句只有1条
6、扩展功能
(1)代码生成器
然后在【工具栏】内找到【Config Database】进行数据库配置,注意改数据库名称
同样在【工具栏】中打开【Code Generator】(有的版本在Other栏)
按下图填写
然后check field后直接code generatro,成功生成代码框架!
(2)DB静态工具
Db工具类在解决业务逻辑循环依赖方面具有独特优势,因为它不依赖Spring容器管理,是静态方法调用。假设有两个Service:
UserService需要调用OrderService的方法
OrderService也需要调用UserService的方法这会形成典型的Spring Bean循环依赖。
Db的核心优势是:绕过Service层,直接查询数据
Db工具类可以解决技术上的循环依赖,但需要注意:
适用场景:简单的、只读的、无业务逻辑的查询
不适用场景:涉及业务规则、事务管理、复杂逻辑的操作
DB静态工具的接口方法与IService类似
++① 根据id查询用户及其对应的所有地址++
java/** * 根据id查询用户,并查询用户对应的所有地址 * @param id * @return */ @Override public UserVO queryUserAndAdressById(Long id) { // 1.查询用户 User user = getById(id); if(user == null || user.getStatus() == 2){ throw new RuntimeException("用户状态异常"); } // 2.查询地址 List<Address> addresses = Db.lambdaQuery(Address.class).eq(Address::getUserId, id).list(); // 3.封装VO UserVO userVO = BeanUtil.copyProperties(user, UserVO.class); if(CollectionUtil.isNotEmpty(addresses)){ userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class)); } return userVO; }++② 批量查询用户及其对应的地址列表++
逻辑如下:
- listByIds()根据用户ids列表查询用户信息,封装为VO
- 获取用户id集合,并根据id查询出每个用户对应的地址列表
- 将地址列表转换为VO,此时的地址列表为混杂未分类形式
- 为了方便后续按【用户信息:地址列表】形式封装,需要将地址VO列表按userId分组
- 最后,封装VO【用户VO:对应地址列表VO】
java/** * 批量查询用户列表,并查询用户对应的所有地址 * @param ids * @return */ @Override public List<UserVO> queryUserAndAddressByIds(List<Long> ids) { // 1.查询用户 List<User> users = listByIds(ids); if(CollectionUtil.isEmpty(users)){ return Collections.emptyList(); } // 2.查询地址 // 获取用户id集合 List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList()); // 根据id查询对应地址,生成对应地址列表 List<Address> list = Db.lambdaQuery(Address.class).in(Address::getUserId, userIds).list(); // 地址转换为VO List<AddressVO> addressVOS = BeanUtil.copyToList(list, AddressVO.class); // 用户地址集合分组处理,相同用户放入一个组中【userId:地址列表】 Map<Long, List<AddressVO>> addressMap = new HashMap<>(); if(CollectionUtil.isNotEmpty(addressVOS)) { addressMap = addressVOS.stream().collect(Collectors.groupingBy(AddressVO::getUserId)); } // 3.封装VO List<UserVO> res = new ArrayList<>(users.size()); for (User user : users) { UserVO vo = BeanUtil.copyProperties(user, UserVO.class); vo.setAddresses(addressMap.get(user.getId())); res.add(vo); } return res; } }++③ 根据用户id查询收货地址,需要验证用户状态++
java/** * 根据id查询用户,验证用户状态并返回收货地址 * @param id * @return */ @Override public UserVO queryUserAndAdressById(Long id) { // 1.查询用户,如果用户状态异常冻结用户 User user = getById(id); if(user == null || user.getStatus() == 2){ user.setStatus(2); throw new RuntimeException("用户状态异常"); } // 2.查询地址 List<Address> addresses = Db.lambdaQuery(Address.class) .eq(Address::getUserId, id) .eq(Address::getIsDefault,1) .list(); // 3.封装VO UserVO userVO = BeanUtil.copyProperties(user, UserVO.class); if(CollectionUtil.isNotEmpty(addresses)){ userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class)); } return userVO; }
(3)逻辑删除
逻辑删除就是基于代码逻辑模拟删除效果,但并不会真正删除数据。
思路如下:
- 在表中添加一个字段标记数据是否被删除
- 当删除数据时把标记置为1
- 查询时只查询标记为0的数据
而要把一起写的delete语句一个一个改很麻烦,因此我们使用mybatisplus提供的逻辑删除功能。
MybatisPlus提供了逻辑删除功能
无需改变方法调用的方式,而是在底层帮我们自动修改CRUD的语句。我们要做的就是在application.yaml文件中配置逻辑删除的字段名称和值即可:
XMLmybatis-plus: global-config: db-config: logic-delete-field: flag # 全局逻辑删除的实体字段名, 字段类型可以是boolean、integer logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)在application.yaml文件中添加配置
然后我们对IAdressService接口创建单元测试
java@SpringBootTest class IAddressServiceTest { @Autowired private IAddressService addressService; @Test void testLogicDelete() { addressService.removeById(59L); Address address = addressService.getById(59L); System.out.println("address: " + address); } }按原来如果我们写removeById语句,底层执行的应该为delete语句
而经过mp逻辑删除配置,我们可以看到mp将原本的delete语句转换为update语句,将删除数据变为修改数据的逻辑删除字段,从而实现数据不显示但仍并未删除的功能
我们可以看到,查询时数据显示已为null,但实际上这条数据并没有被删除
(4)枚举处理器
问题场景:
数据库的
user表中有一个status字段,类型是INT,用于表示用户状态(1代表正常,2代表冻结)。如果在Java代码中直接用整数(1, 2)来操作,代码可读性差,且容易传入错误的值(如3, 4)。
解决方案 :创建自定义枚举
UserStatus。
枚举中明确定义了
NORMAL(1, "正常")和FREEZE(2, "冻结")两个实例。在
User实体类中,将status字段的类型从Integer改为UserStatus。优势:
类型安全:编译器能检查,避免传入无效的状态值。
易读:使用
UserStatus.NORMAL比使用数字1意义明确得多。易于维护:状态值集中管理,修改只需改枚举。
在application.yaml中配置全局枚举处理器:
javaconfiguration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler创建enums枚举类
java@Getter public enum UserStatus { NORMAL(1,"正常"), FROZEN(2,"冻结"), ; @EnumValue //用于标记枚举中哪个字段的值对应数据库的存储值 private final int value; @JsonValue //用于控制枚举在 JSON 序列化时的输出格式 private final String desc; // JSON 输出 "正常", "冻结" UserStatus(int value, String desc) { this.value = value; this.desc = desc; }然后修改User的状态类型
最后替换代码中的常量
📍 总结:
如何使用 Mp 处理枚举Enums与数据库字段的映射
【第一步】标记枚举的数据库值
在枚举类中,用 @EnumValue注解标记哪个字段的值对应数据库存储
javapublic enum UserStatus { NORMAL(1, "正常"), // 数据库存 1 FROZEN(2, "冻结"); // 数据库存 2 @EnumValue // 关键注解 private final int value; // 这个字段的值会存入数据库 private final String desc; // ... 构造方法等 }【第二步】配置全局枚举处理器
在
application.yml中配置:
javamybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler这个配置会让 MyBatis-Plus 自动处理所有带
@EnumValue注解的枚举。实现的效果
保存数据时:
UserStatus.NORMAL→ 数据库存入1查询数据时:数据库中的
1→ 自动转换为UserStatus.NORMAL对象代码更安全:避免了直接使用数字1, 2,编译时就能发现错误
(5)JSON处理器
当数据库的某个字段存储的是 JSON 格式的复杂数据时,我们希望在 Java 代码中能像操作普通对象一样操作这个 JSON 数据,而不是手动拼接字符串。
我们可以使用MyBatis-Plus 自动完成 JSON ↔ Java 对象的转换:
【1】首先创建实体类UserInfo
java@Data @NoArgsConstructor @AllArgsConstructor(staticName = "of") //生成一个名为 of的静态工厂方法 public class UserInfo { private Integer age; private String intro; private String gender; }@AllArgsConstructor(staticName = "of")是一种创建对象的优雅方式,使代码更简洁、更函数式。
【2】替换User实体类中info的类型,从String→UserInfo
java/** * 详细信息 */ @TableField(typeHandler = JacksonTypeHandler.class) // MyBatis-Plus 提供的自动化 JSON ↔ 对象 转换器 private UserInfo info;
java@TableName(value = "user",autoResultMap = true) /** * autoResultMap = true * 核心功能:自动开启结果映射 * 解决问题:当实体类中字段类型不是数据库原生支持时(如枚举、JSON、自定义类型),自动创建 ResultMap进行转换 * 特别重要:当实体类中有特殊类型字段时,必须设置为 true */
7、插件功能
(1)分页插件
【1】配置分页插件
作用:启用了 MyBatis-Plus 的分页功能
java@Configuration public class MyBatisConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { // 创建 MyBatis-Plus 拦截器容器 MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 创建分页插件,指定 MySQL 数据库类型 PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); paginationInnerInterceptor.setMaxLimit(1000L); // 将分页插件添加到拦截器链 interceptor.addInnerInterceptor(paginationInnerInterceptor); return interceptor; } }【2】应用分页API
java@Test void testPageQuery() { int pageNo = 1, pageSize = 2; //模拟分页数据 // 1.准备分页条件 // 1.1.分页条件 Page<User> page = Page.of(pageNo, pageSize); // 1.2.排序条件 page.addOrder(new OrderItem("balance", true)); page.addOrder(new OrderItem("id", true)); // 2.分页查询 Page<User> p = userService.page(page); // 3.解析 long total = p.getTotal(); System.out.println("total = " + total); long pages = p.getPages(); System.out.println("pages = " + pages); List<User> users = p.getRecords(); users.forEach(System.out::println); }
(2)通用分页实体
【1】定义实体类
java@Data @ApiModel(description = "分页查询实体") public class PageQuery { @ApiModelProperty("页码") private Long pageNo; @ApiModelProperty("页码") private Long pageSize; @ApiModelProperty("排序字段") private String sortBy; @ApiModelProperty("是否升序") private Boolean isAsc; }需要定义3个实体:
UserQuery:分页查询条件的实体,包含++分页++ 、++排序参数等++(画横线这几个可以继承PageQuery)PageDTO:分页结果实体,包含总条数、总页数、当前页数据UserVO:用户页面视图实体UserQuery类
java@Data @ApiModel(description = "用户查询条件实体") public class UserQuery { @ApiModelProperty("用户名关键字") private String name; @ApiModelProperty("用户状态:1-正常,2-冻结") private UserStatus status; @ApiModelProperty("余额最小值") private Integer minBalance; @ApiModelProperty("余额最大值") private Integer maxBalance; }这其中缺少分页查询相关参数(页码、每页数据条数等),因为别的业务也需要使用分页,因此我们需要单独创建一个PageQuery类,然后让UserQuery继承这个实体。
PageQuery类
java@Data @ApiModel(description = "分页查询实体") public class PageQuery { @ApiModelProperty("页码") private Long pageNo; @ApiModelProperty("页码") private Long pageSize; @ApiModelProperty("排序字段") private String sortBy; @ApiModelProperty("是否升序") private Boolean isAsc; }【2】Controller层定义用户分页接口
java@ApiOperation("分页查询用户信息") @GetMapping("/page") public PageDTO<UserVO> queryUsersPage(UserQuery query){ return userService.queryUsersPage(query); }【3】Service层编写分页逻辑
java@Override public PageDTO<UserVO> queryUsersPage(UserQuery query) { String name = query.getName(); UserStatus status = query.getStatus(); // 1.构建分页条件 // 分页条件 Page<User> page = Page.of(query.getPageNo(), query.getPageSize()); // 排序条件 if(query.getSortBy() != null){ page.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc())); }else{ // 默认按更新时间排序 page.addOrder(new OrderItem("update_time", false)); } // 2.查询 Page<User> p = lambdaQuery() .like(name != null,User::getUsername,name) .eq(status != null,User::getStatus,status) .page(page); // 3.数据非空校验 List<User> records = p.getRecords(); if(CollectionUtil.isEmpty(records)){ return new PageDTO<>(p.getTotal(),p.getPages(),Collections.emptyList()) } // 4.数据转换 List<UserVO> list = BeanUtil.copyToList(records, UserVO.class); // 5.封装返回 return new PageDTO<>(p.getTotal(),p.getPages(),list); } }
(3)优化分页插件
【1】在
PageQuery实体中封装分页工具通过上面Service层的逻辑代码,我们发现【构建分页条件】【数据非空校验】【数据转换】在每一个分页业务中都是一样的,我们可以将其封装成工具方法,以后需要使用时直接调用。
java@Data public class PageQuery { @ApiModelProperty("页码") private Integer pageNo; @ApiModelProperty("总页数") private Integer pageSize; @ApiModelProperty("排序字段") private String sortBy; @ApiModelProperty("是否升序") private Boolean isAsc; /** * 泛型方法 * 泛型方法允许在调用方法时指定具体的类型,从而提高代码的复用性和类型安全性。 * 主要功能:将前端传入的分页参数转换为 MP 的 Page对象,支持灵活的排序规则处理。 * 可变参数 OrderItem ... orders:允许传入0个或多个排序条件(OrderItem对象) * */ public <T> Page<T> toMpPage(OrderItem ... orders){ // 1.分页条件 Page<T> p = Page.of(pageNo, pageSize); // 2.排序条件 // 2.1.先看前端有没有传排序字段 if (sortBy != null) { p.addOrder(new OrderItem(sortBy, isAsc)); return p; } // 2.2.再看有没有手动指定排序字段 if(orders != null){ p.addOrder(orders); } return p; } public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc){ return this.toMpPage(new OrderItem(defaultSortBy, isAsc)); } public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() { return toMpPage("create_time", false); } public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() { return toMpPage("update_time", false); } }
最灵活(核心):
toMpPage(OrderItem...)- 可传多个排序条件中等:
toMpPage(String, boolean)- 指定单个排序最方便:
toMpPageDefaultSortByXxx()- 直接调用,无需参数【2】在PageDTO中封装数据判空、转化工具
java/** * 将Mp分页结果转为 VO分页结果【允许用户自定义PO到VO的转换方式】 * p MybatisPlus的分页结果 * convertor PO到VO的转换函数 * <V> 目标VO类型 * <P> 原始PO类型 * VO的分页对象 * Function<P, V> 取出PO,返回VO */ public static <V, P> PageDTO<V> of(Page<P> p, Function<P, V> convertor) { // 1.非空校验 List<P> records = p.getRecords(); if (records == null || records.size() <= 0) { // 无数据,返回空结果 return empty(p); } // 2.数据转换(根据自定义规则,通过stream流自动转换) List<V> vos = records.stream() .map(convertor) //对每个user执行自定义的lambda函数 .collect(Collectors.toList()); // 3.封装返回 return new PageDTO<>(p.getTotal(), p.getPages(), vos); }
- empty() - 返回空结果,只保留分页信息
- of(Class) - 自动反射转换,适合简单场景
- of(Function) - 自定义转换,适合复杂场景
【3】优化分页逻辑代码
这样我们在UserServiceImpl中的分页查询业务层代码就能简化为:
java/** * 分页查询 * @param query * @return */ public PageDTO<UserVO> queryUsersPage(PageQuery query) { // 1.构建分页条件(按create_time降序排序) Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc(); // 2.查询 Page<User> p = lambdaQuery() .like(name != null,User::getUsername,name) .eq(status != null,User::getStatus,status) .page(page); // 3.数据校验 & 转换 & 封装返回 return PageDTO.of(p, UserVO.class); }如果封装数据时希望自定义,可以这么编写:
java/** * 分页查询 * @param query * @return */ @Override public PageDTO<UserVO> queryUsersPage(UserQuery query) { String name = query.getName(); UserStatus status = query.getStatus(); // 1.构建分页条件(按create_time降序排序) Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc(); // 2.查询 Page<User> p = lambdaQuery() .like(name != null,User::getUsername,name) .eq(status != null,User::getStatus,status) .page(page); // 3.数据校验 & 转换 & 封装返回 return PageDTO.of(p, user -> { //对每一个从数据库取出的user进行自定义 UserVO vo = BeanUtil.copyProperties(user, UserVO.class); // 用户名脱敏 String username = vo.getUsername(); vo.setUsername(username.substring(0,username.length()-2)+"**"); return vo; }); }关于自定义lambda函数运行流程:
二、Docker
Docker 是一个容器化平台,可以让你把应用和它的运行环境打包在一起,在任何地方都能一致运行。
" 把我的整个运行环境一起打包给你 "
包含:代码 + 软件 + 配置 + 系统环境❗️对于安装docker步骤我这里跳过(因为直接用的别人安装好docker的服务器)
1、Docker安装MySQL
(1)启动/关闭Docker
bash# 1. 启动Docker服务 sudo systemctl start docker # 2. 设置开机自启 sudo systemctl enable docker # 3. 验证是否启动成功 sudo systemctl status docker # 看到绿色的"active (running)"表示成功 ------------------------------------------------------------------------------------------------------------------------------------------ # 1. 停止Docker服务 sudo systemctl stop docker # 2. 禁止开机自启 sudo systemctl disable docker # 3. 验证是否停止 sudo systemctl status docker # 看到红色的"inactive (dead)"表示已停止(2)安装MySQL
安装前保证:
停止虚拟机中的 MySQL 服务
bash# 如果已经有原生MySQL,先停止 sudo systemctl stop mysql
确保 Docker 已安装并启动
bash# 启动Docker(如果还没启动) sudo systemctl start docker # 验证Docker是否运行 docker -v
bashdocker run -d \ # -d: 后台运行容器 --name mysql8 \ # 容器命名为 mysql8 -p 3307:3306 \ # 主机3307端口映射到容器3306端口 -e TZ=Asia/Shanghai \ # 设置时区为北京时间 -e MYSQL_ROOT_PASSWORD=123456 \ # 设置MySQL root密码为123456 mysql:8.0.37-debian # 使用的镜像版本外部无法直接访问容器内部端口(3306),需要通过服务器的3307端口 被映射到容器的3306端口。
注意:因为我的服务器3306端口被占用,所以我换成3307端口
容器已成功创建,容器ID为:
4dc488f4769e7facfe540d75fd842ad3338e2ad361c74faabccac58c2a6e7821用Navicat尝试连接数据库
连接成功!
2、镜像与容器
- 镜像:当我们利用Docker安装应用时,Docker会自动搜索并下载应用镜像(image)。镜像不仅包含应用本身,还包含应用运行所需要的环境、配置、系统函数库。
- 容器:Docker会在运行镜像时创建一个隔离环境,称为容器(container)
bash# 查看运行中的容器 docker ps
- 镜像仓库:存储和管理镜像的平台,Docker官方维护了一个公共仓库,里边包含了很多软件的镜像。
- 镜像命名规范
- 镜像名称由两部分组成,格式为:[repository]:[tag]
- repository:镜像名
- tag:版本号
- 没有指定tag时,默认latest,代表最新版本镜像
3、Docker常见命令
(1)镜像指令
- docker build
- 作用:构建镜像。它读取Dockerfile和上下文中的文件,按照步骤执行,最终生成一个可用的本地镜像。
- 流向:Dockerfile→ 本地镜像
- docker pull
- 作用:拉取镜像。从远端镜像仓库(如Docker Hub)下载指定的镜像到本地镜像库。
- 流向:镜像仓库 → 本地镜像
- docker push
- 作用:推送镜像。将本地的镜像上传到远端镜像仓库,供他人或其他机器使用。
- 流向:本地镜像 → 镜像仓库
- docker images
- 作用:列出镜像。查看本地已经下载或构建的所有镜像列表及其信息。
- docker rmi
- 作用:删除镜像。从本地镜像库中移除一个或多个不再需要的镜像,释放磁盘空间。
- docker save
- 作用:保存镜像为文件。将一个或多个本地镜像打包成一个tar归档文件,便于离线分享或备份。
(2)容器指令
- docker run
- 作用:创建并启动容器。它基于指定的本地镜像,创建并启动一个新的容器实例。如果本地没有该镜像,会先尝试执行docker pull。
- 流向:本地镜像 → 容器
- docker start
- 作用:启动容器。启动一个已存在但处于停止状态的容器,使其进入"运行中"状态。
- docker stop
- 作用:停止容器。向一个"运行中"的容器发送停止信号,让其停止运行。
- docker exec
- 作用:在运行中的容器内执行命令。进入一个正在运行的容器,并执行一个命令(例如 /bin/bash),常用于调试或执行临时操作。
- docker logs
- 作用:查看容器日志。获取容器运行过程中产生的标准输出和错误日志,是排查问题、查看应用输出的主要方式。
- docker ps
- 用于查看当前正在运行的容器列表。
因为docker ps返回的列表格式混乱,因此我们需要在指令后加命令格式。
但显然每次加命令格式会很繁琐,因此我们需要设定命令别名。
按回车进入vim编辑器,按i进入插入模式(下方会显示INSERT),写完命令按Esc退出,然后输入**:wq**保存并退出。
bashalias dps='docker ps --format "table {{.ID}}\t{{.Image}}\t{{.Ports}}\t{{.Status}}\t{{.Names}}"'最后在终端输入下面命令进行加载
bashsource ~/.bashrc以后输入dps就可以直接运行docker ps ......一长串命令!
4、数据卷
(1)安装nginx
首先拉取nginx镜像
bashdocker pull nginx然后启动nginx容器
bashdocker run -d --name nginx -p 80:80 nginx如果你的主机80端口被占用,先清理掉创建失败的容器,再重新创建
bashdocker rm nginx docker run -d --name nginx -p 81:80 nginx #主机81端口映射到容器80端口(2)数据卷简介
官方Nginx镜像为了保持轻量,只包含运行Nginx所必需的最小化软件包
不包含vi、vim、nano等文本编辑器
不推荐在运行的容器内直接修改文件
为了解决这个问题,我们引入数据卷:
数据卷(Volume) 是一个虚拟目录,是容器目录与宿主机目录之间映射的桥梁,方便我们操作容器内的文件,也方便迁移容器产生的数据。图中的连接线就表示这种映射关系。
双向同步:任何一边的修改都会实时反映到另一边
持久化:容器删除后,数据仍在宿主机保留
共享:多个容器可以挂载同一个数据卷
命令 说明 docker volume create创建数据卷 docker volume ls查看所有数据卷 docker volume rm删除指定数据卷 docker volume inspect查看某个数据卷的详情 docker volume prune清除未使用的数据卷 (3)数据卷挂载
如何挂载数据卷?
- 在
docker run命令中,通过-v 数据卷名称:容器内目录参数可以实现数据卷挂载- 如果挂载时指定的数据卷不存在,Docker 会自动创建这个数据卷
1.先删除nginx
bashdocker rm -f nginx2.进行数据挂载
bashdocker run -d --name nginx -p 81:80 -v html:/usr/share/nginx/html nginx
参数 说明 docker run创建并启动一个新容器的核心命令 -d让容器在后台运行(守护进程模式),并返回容器ID --name nginx为容器指定一个名称,便于后续管理(如启动、停止),这里命名为 nginx-p 81:80进行端口映射 ,将宿主机的 81端口映射到容器内部的80端口。这样,通过访问http://宿主机IP:81就能访问到容器内的Nginx服务-v html:/usr/share/nginx/html挂载数据卷 。将名为 html的数据卷(由Docker管理)挂载到容器内的/usr/share/nginx/html目录。这使网页文件可以持久化保存,即使容器删除,数据也不会丢失nginx指定要运行的镜像名称,这里使用的是Docker官方提供的Nginx镜像 3.然后确定一下数据卷有没有成功创建
bashdocker volume ls4.查看数据卷详情
bashdocker volume inspect html5.获取宿主机目录后,转到宿主机目录下
bashcd /var/lib/docker/volumes/html/_data6.这下用vi命令编辑index.html目录就没有问题了
bashvi index.html这是vim编辑器,按i进入编辑,改完按esc,最后输入:wq退出并保存
7.然后访问nginx主页就会发现修改完成!
bashhttp://你的服务器IP地址:你nginx的端口号
5、本地目录挂载
(1)查看mysql是否存在数据挂载
bashdocker inspect mysql我们会发现mysql自动创建了一个数据卷,用于数据持久化存储。
如果用户不指定挂载,Docker 会自动创建匿名数据卷
这是为了保证数据库数据不会因容器删除而丢失
但匿名卷难以管理,建议在生产环境中使用命名数据卷
(2)基于宿主机本地目录实现mysql数据挂载
先删掉mysql
bashdocker rm -f mysql确定一下我们本次需要实现3项文件的挂载,包括其对应容器内路径:
目录类型 容器内路径 作用说明 数据目录 /var/lib/mysql存储所有数据库文件、表数据、索引等,这是MySQL的核心数据存储位置 配置文件目录 /etc/mysql/conf.d存放自定义配置文件,MySQL会自动加载该目录下的所有 .cnf文件来覆盖默认配置初始化脚本目录 /docker-entrypoint-initdb.d存放SQL脚本文件( .sql、.sh、.sql.gz),容器首次启动时会自动执行这些脚本进行数据库初始化
- 挂载 /root/mysql/data 到容器内的 /var/lib/mysql 目录
- 挂载 /root/mysql/init 到容器内的 /docker-entrypoint-initdb.d 目录,携带课前资料准备的SQL脚本
- 挂载 /root/mysql/conf 到容器内的 /etc/mysql/conf.d 目录,携带课前资料准备的配置文件
【1】创建本地目录 /mysql下的/data、/init、/conf
先回到根目录下,然后通过mkdir指令创建目录,创建完用ls查看目录
【2】将课程资料中的脚本、配置文件放入本地目录
这里我是直接拖动文件,其他的存入方法可以问问AI
【3】进行本地目录挂载
注:看你本地目录名称叫什么,一般是root,我这里是roye,注意修改!
bashdocker run -d \ --name mysql8 \ -p 3307:3306 \ -e TZ=Asia/Shanghai \ -e MYSQL_ROOT_PASSWORD=123456 \ -v /roye/mysql/data:/var/lib/mysql \ -v /roye/mysql/init:/docker-entrypoint-initdb.d \ -v /roye/mysql/conf:/etc/mysql/conf.d \ mysql
- 在执行docker run命令时,使用 -v 本地目录:容器内目录 可以完成本地目录挂载本地目录
- 必须以" / "或" . "开头,如果直接以名称开头,会被识别为数据卷而非本地目录
- -v mysql:/var/lib/mysql 会被识别为一个数据卷叫mysql
- -v ./mysql:/var/lib/mysql 会被识别为当前目录下的mysql目录
最后输入dps查看mysql容器是否成功运行
6、自定义镜像
镜像就是包含了++应用程序++ 、++程序运行的系统函数库++ 、++运行配置++等文件的文件包。构建镜像的过程其实就是把上述文件打包的过程。
构建一个Java镜像的步骤:
- 准备一个Linux运行环境
- 安装JRE并配置环境变量
- 拷贝Jar包
- 编写运行脚本
(1)Dockerfile
Dockerfile 是一个文本文件,包含一系列用于构建 Docker 镜像的指令。它定义了如何从基础镜像开始,逐步添加配置、安装软件、复制文件,最终生成一个可运行的容器镜像。
指令 说明 示例 FROM 指定基础镜像 FROM centos:6ENV 设置环境变量,可在后面指令使用 ENV key valueCOPY 拷贝本地文件到镜像的指定目录 COPY ./jre11.tar.gz /tmpRUN 执行Linux的shell命令,一般是安装过程的命令 RUN tar -zxvf /tmp/jre11.tar.gz && EXPORTS path=/tmp/jre11:$pathEXPOSE 指定容器运行时监听的端口,是给镜像使用者看的 EXPOSE 8080ENTRYPOINT 镜像中应用的启动命令,容器运行时调用 ENTRYPOINT java -jar xx.jar(2)自定义镜像
通过上一部分学习,我们已经知道,如何构建一个Java应用镜像
现在我们只需要把打包好的镜像上传到docker并命名使用
【1】从资料包中获取demo包,可以看到已经打包好的镜像
- docker-demo.jar:Java 应用程序的可执行 JAR 包
- Dockerfile:镜像构建配置文件
【2】把整个demo目录拖进本地目录
【3】基础镜像涉及的jdk包需要下载,而资料包已经提供了jdk包,因此我们直接拖进本地目录即可。
【4】加载镜像
bashdocker load -i jdk.tar接着通过docker images查看镜像
构建一个名为
docker-demo的 Docker 镜像
bashdocker build -t docker-demo .
组成部分 解释 说明 ** docker build**Docker 镜像构建命令 核心命令,用于根据 Dockerfile构建一个新的 Docker 镜像** -t docker-demo**指定镜像名称和标签 -t是--tag的缩写,表示给构建的镜像命名。docker-demo是镜像的名称,此处没有指定标签,默认为latest** .**构建上下文路径 .表示当前目录。Docker 会寻找当前目录下的 Dockerfile文件,并将当前目录及其子目录作为构建上下文(文件默认可见范围)自定义镜像构建完成,如果想要启动则运行下面的命令
bashdocker run -d --name dd -p 8081:8080 docker-demo接着查看日志
bashdocker logs -f dd成功运行!
7、容器网络互联
默认情况下,所有容器都是以docker初始化的网桥连接的
但是容器间互联 会非常麻烦,需要手动获取IP地址,容器重启后IP会变化
因此我们需要掌握【自定义网络互联】
bash# 1. 创建自定义网络 docker network create my_network # 2. 启动容器并加入网络 docker run -d --name web1 --network my_network nginx docker run -d --name web2 --network my_network nginx # 3. 通过容器名直接通信 docker exec web1 ping web2
优势 说明 自动 DNS 解析 同一网络内的容器可以通过容器名称直接通信,无需知道 IP 地址 网络隔离 不同自定义网络中的容器默认无法通信,提供更好的安全性 灵活连接 容器可以在运行时动态连接或断开网络,无需重启 可配置性 支持指定子网、网关、IP 范围等精细配置 多网络支持 一个容器可以同时连接到多个自定义网络 这样就不需要知道IP地址,直接通过容器名称就可以进行容器间互联。下面是一些docker网络的指令。
命令 说明 docker network create创建一个网络 docker network ls查看所有网络 docker network rm删除指定网络 docker network prune清除未使用的网络 docker network connect使指定容器连接加入某网络 docker network disconnect使指定容器连接离开某网络 docker network inspect查看网络详细信息
三、项目部署
1、部署Java应用
【1】在idea打开资料包中的hmall文件
2、部署前端
3、DockerCompose
四、一些服务器的小技巧
这部分是给作者我记录看的,读者不需要看!!
1、如何进入服务器
启动Termius,登录进服务器
输入【bt 14】获取交互网址以及账号密码
然后 command+左键 进入【外网ipv4面板地址】,进入【终端】
输入:
- cd .. 或者 cd /
- cd /home/roye
成功进入我的目录,然后就可以操作啦!
2、常用指令记录
(1)基本查看命令
命令 功能说明 常用参数 pwd 显示当前工作目录的完整路径 无参数 ls 列出目录内容 -l(详细列表)、-a(显示隐藏文件)、-h(人性化显示文件大小)ll ls -l的别名,显示详细列表无参数 cd 切换目录 cd 目录名、cd ..(返回上级)、cd ~(返回家目录)
























































































