SSM框架学习(七、MyBatis-Plus高级用法:最优化持久层开发)

目录

一、MyBatis-Plus快速入门

1.简介

2.快速入门

二、MyBatis-Plus核心功能

1.基于Mapper接口CRUD

[(1)Insert 方法](#(1)Insert 方法)

(2)Delete方法

[(3)Update 方法](#(3)Update 方法)

(4)Select方法

2.基于Service接口CRUD

[(1)save 方法](#(1)save 方法)

(2)saveOrUpdate方法

[(3)remove 方法](#(3)remove 方法)

[(4)update 方法](#(4)update 方法)

[(5)get 和 list 方法](#(5)get 和 list 方法)

3.分页查询实现

4.条件构造器使用

(1)条件构造器继承

(2)条件构造器继承结构

[(3)基于QueryWrapper 组装条件](#(3)基于QueryWrapper 组装条件)

Ⅰ.组装查询条件

[Ⅱ. 组装排序条件](#Ⅱ. 组装排序条件)

[Ⅲ. 组装删除条件](#Ⅲ. 组装删除条件)

[Ⅳ. and 和 or 关键字使用(修改):](#Ⅳ. and 和 or 关键字使用(修改):)

[Ⅴ. 指定列映射查询](#Ⅴ. 指定列映射查询)

[Ⅵ. condition判断组织条件](#Ⅵ. condition判断组织条件)

[(4)基于 UpdateWrapper 组装条件](#(4)基于 UpdateWrapper 组装条件)

[(5)基于LambdaQueryWrapper 组装条件](#(5)基于LambdaQueryWrapper 组装条件)

[(6)基于 LambdaUpdateWrapper 组装条件](#(6)基于 LambdaUpdateWrapper 组装条件)

5.核心注解使用

(1)理解和介绍

[(2)@TableName 注解](#(2)@TableName 注解)

[(3)@TableId 注解](#(3)@TableId 注解)

[(4)@TableField 注解](#(4)@TableField 注解)

[三、MyBatis-Plus 高级扩展](#三、MyBatis-Plus 高级扩展)

1.逻辑删除实现

2.乐观锁实现

(1)悲观锁和乐观锁场景和介绍

(2)使用mybatis-plus数据使用乐观锁(基于版本号技术)

3.防全表更新和删除实现

四、MyBatis-Plus代码生成器(MyBatisX插件)

[1.Mybatisx 插件逆向工程](#1.Mybatisx 插件逆向工程)

[2.MyBatisX 快速代码生成](#2.MyBatisX 快速代码生成)


一、MyBatis-Plus快速入门

1.简介

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

特性:

**① 无侵入:**只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑

**② 损耗小:**启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作

**③ 强大的 CRUD 操作:**内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求

**④ 支持 Lambda 形式调用:**通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错

**⑤ 支持主键自动生成:**支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题

**⑥ 支持 ActiveRecord 模式:**支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作

⑦ 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )

**⑧ 内置代码生成器:**采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用

**⑨ 内置分页插件:**基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询

**⑩ 分页插件支持多种数据库:**支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库

**⑪ 内置性能分析插件:**可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询

**⑫ 内置全局拦截插件:**提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

支持数据库:

MySQL,Oracle,DB2,H2,HSQL,SQLite,PostgreSQL,SQLServer,Phoenix,Gauss ,ClickHouse,Sybase,OceanBase,Firebird,Cubrid,Goldilocks,csiidb,informix,TDengine,redshift

达梦数据库,虚谷数据库,人大金仓数据库,南大通用(华库)数据库,南大通用数据库,神通数据库,瀚高数据库,优炫数据库

mybatis-plus总结:

自动生成单表的CRUD功能

提供丰富的条件拼接方式

全自动ORM类型持久层框架

2.快速入门

① 准备数据库脚本

sql 复制代码
CREATE TABLE user
(
    id BIGINT(20) NOT NULL COMMENT '主键ID',
    name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
    age INT(11) NULL DEFAULT NULL COMMENT '年龄',
    email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
    PRIMARY KEY (id)
);


INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');

② 准备boot工程,导入依赖

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.5</version>
    </parent>

    <groupId>com.mihoyo</groupId>
    <artifactId>mybatis-plus-quick-01</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- 测试环境 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <!-- mybatis-plus  -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>

        <!-- 数据库相关配置启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <!-- druid启动器的依赖  -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-3-starter</artifactId>
            <version>1.2.21</version>
        </dependency>

        <!-- 驱动类-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
        </dependency>

    </dependencies>


    <!--    SpringBoot应用打包插件-->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

③ 配置文件和启动类

application.yaml:

java 复制代码
# 连接池配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      url: jdbc:mysql:///mybatis-example
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制态输入日志(sql语句)
  type-aliases-package: com.mihoyo.pojo #起别名

注意:

mybatis-plus 已经将驼峰式映射的默认值设置为 true, 无需手动设置

启动类:

java 复制代码
@MapperScan("com.mihoyo.mapper")
@SpringBootApplication
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class,args);
    }
    
}

④ 功能编码

编写实体类 User.java(此处使用了 Lombok 简化代码)

java 复制代码
@Data
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
}

编写 Mapper 包下的 UserMapper 接口

java 复制代码
public interface UserMapper extends BaseMapper<User> {

}

**注意:**继承mybatis-plus提供的基础Mapper接口,自带 crud 方法!

⑤ 测试和使用

添加测试类,进行功能测试:

java 复制代码
@SpringBootTest //springboot下测试环境注解
public class MybatisPlusTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testSelect() {
        //查询全部数据
        List<User> userList = userMapper.selectList(null);//条件为空
        userList.forEach(System.out::println);
    }
}

总结:

通过以上几个简单的步骤,我们就实现了 User 表的 CRUD 功能,甚至连 XML 文件都不用编写。

从以上步骤中,我们可以看到集成 MyBatis-Plus 非常的简单,只需要引入 starter 工程,并配置 mapper 扫描路径即可。

二、MyBatis-Plus核心功能

1.基于Mapper接口CRUD

通用 CRUD 封装 BaseMapper (opens new window) 接口, Mybatis-Plus 启动时自动解析实体表关系映射转换为 Mybatis 内部对象注入容器。

内部包含常见的单表操作:

(1)Insert 方法

java 复制代码
// 插入一条记录
// T 就是要插入的实体对象
// 默认主键生成策略为雪花算法(后面讲解)
int insert(T entity);

参数说明:

类型 参数名 描述
T entity 实体对象

注意:

T,即实体类名 必须要和 数据库表名 相同,Mybatis-Plus 会根据 实体类名 自动映射到 数据库对应的表。

测试:

java 复制代码
@SpringBootTest //springboot下测试环境注解
public class MybatisPlusTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void test_insert(){
        User user = new User();
        user.setName("张三");
        user.setAge(18);
        user.setEmail("xxxx@qq.com");
        //baseMapper提供的数据库插入方法
        int row = userMapper.insert(user);
        System.out.println("row = " + row);
    }
}

运行结果:

(2)Delete方法

java 复制代码
// 根据 entity 条件,删除记录
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);

// 删除(根据ID 批量删除)
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);

// 根据 ID 删除
int deleteById(Serializable id);

// 根据 columnMap 条件,删除记录
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);

参数说明:

类型 参数名 描述
Wrapper<T> wrapper 实体对象封装操作类(可以为 null)
Collection<? extends Serializable> idList 主键 ID 列表(不能为 null 以及 empty)
Serializable id 主键 ID
Map<String, Object> columnMap 表字段 map 对象

测试:

java 复制代码
    @Test
    public void test_delete(){
        //根据id删除
        int rows = userMapper.deleteById(1846463349906759682L);
        System.out.println("rows = " + rows);
        
        //根据条件(age=20)删除
        Map param = new HashMap();
        param.put("age",20);//如果有其他条件继续put
        int i = userMapper.deleteByMap(param);
        System.out.println("i = " + i);
    }

运行结果:

(3)Update 方法

java 复制代码
// 根据 whereWrapper 条件,更新记录
int update(@Param(Constants.ENTITY) T updateEntity, 
            @Param(Constants.WRAPPER) Wrapper<T> whereWrapper);

// 根据 ID 修改  主键属性必须值
int updateById(@Param(Constants.ENTITY) T entity);

参数说明:

类型 参数名 描述
T entity 实体对象 (set 条件值,可为 null)
Wrapper<T> updateWrapper 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)

测试:

java 复制代码
    @Test
    public void test_update() {
        //user  id=1的age改为30
        User user = new User();
        user.setId(1L);
        user.setAge(30);
        // update user set age=30 where id=1(当有属性值为空时,该属性不修改)
        int i = userMapper.updateById(user);
        System.out.println("i = " + i);

        //将所有人的年龄改为22
        User user1 = new User();
        user1.setAge(22);
        // update user set age=22
        int rows = userMapper.update(user1, null);
    }

注意:

① 根据 id 修改,user 的 id 属性必须给值。

② 修改时,如果 user 有部分属性为空,该属性就不修改。

③ 由于 ② 的因素,属性的类型必须为包装类型。 如果是基本数据类型,会存在默认值。

比如:

age 如果定义成 int 类型,即使没有通过 set 方法赋值,也会有默认值 0,修改时会被修改为 0。
如果定义成 Integer 类型,没有赋值就是 null,就不会发生修改。

运行结果:

(4)Select方法

java 复制代码
// 根据 ID 查询
T selectById(Serializable id);

// 根据 entity 条件,查询一条记录
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

// 查询(根据ID 批量查询)
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);

// 根据 entity 条件,查询全部记录
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

// 查询(根据 columnMap 条件)
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);

// 根据 Wrapper 条件,查询全部记录
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

// 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

// 根据 entity 条件,查询全部记录(并翻页)
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

// 根据 Wrapper 条件,查询全部记录(并翻页)
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

// 根据 Wrapper 条件,查询总记录数
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

参数说明:

类型 参数名 描述
Serializable id 主键 ID
Wrapper<T> queryWrapper 实体对象封装操作类(可以为 null)
Collection<? extends Serializable> idList 主键 ID 列表(不能为 null 以及 empty)
Map<String, Object> columnMap 表字段 map 对象
IPage<T> page 分页查询条件(可以为 RowBounds.DEFAULT)

测试:

java 复制代码
    @Test
    public void test_select(){
        //根据id查询
        User user = userMapper.selectById(1);
        System.out.println("user = " + user);

        //根据id集合查询
        List<Long> ids=new ArrayList<>();
        ids.add(1L);
        ids.add(2L);
        List<User> list = userMapper.selectBatchIds(ids);
        System.out.println("list = " + list);
    }

运行结果:

2.基于Service接口CRUD

通用 Service CRUD 封装IService (opens new window) 接口,进一步封装 CRUD 采用 get 查询单行 remove 删除 list 查询集合 page 分页 前缀命名方式区分 Mapper 层避免混淆。

对比Mapper接口CRUD区别:

① service添加了批量方法

② service层的方法自动添加事务

使用 Iservice 接口方式:

接口继承 IService 接口

java 复制代码
public interface UserService extends IService<User> {
}

实现类继承 ServiceImpl 实现类

java 复制代码
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService{

}

注意:

IService 接口中有一半方法默认实现,还有一半没有实现的抽象方法。

所以想要使用 IService 中的 crud 方法,还得重写另一半没有实现的方法。

ServiceImpl类 实现了 IService 接口,已经重写另一半方法,我们可以直接继承。

ServiceImpl 底层还是通过 mapper 对象调用方法,所以泛型中需要对应的 mapper 接口。

(1)save 方法

java 复制代码
// 插入一条记录(选择字段,策略插入)
boolean save(T entity);
// 插入(批量)
boolean saveBatch(Collection<T> entityList);
// 插入(批量)
boolean saveBatch(Collection<T> entityList, int batchSize);

参数说明:

类型 参数名 描述
T entity 实体对象
Collection<T> entityList 实体对象集合
int batchSize 插入批次数量

返回值: boolean,表示插入操作是否成功。

测试:

java 复制代码
    @Test
    public void test_save() {
        User user1 = new User();
        user1.setAge(18);
        user1.setName("张三");
        user1.setEmail("xxx@qq.com");

        User user2 = new User();
        user2.setAge(20);
        user2.setName("李四");
        user2.setEmail("yyy@qq.com");

        List<User> list = new ArrayList<>();
        list.add(user1);
        list.add(user2);

        boolean b = userService.saveBatch(list);
        System.out.println("b = " + b);
    }

运行结果:

(2)saveOrUpdate方法

java 复制代码
// TableId 注解存在更新记录,否插入一条记录
boolean saveOrUpdate(T entity);
// 根据updateWrapper尝试更新,否继续执行saveOrUpdate(T)方法
boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize);

参数说明:

类型 参数名 描述
T entity 实体对象
Wrapper<T> updateWrapper 实体对象封装操作类 UpdateWrapper
Collection<T> entityList 实体对象集合
int batchSize 插入批次数量

返回值: boolean,表示插入或更新操作是否成功。

测试:

java 复制代码
    @Test
    public void test_saveOrUpdate() {
        /* 如果id有值,不为null --> 修改
           如果id没值,为null  --> 保存(插入)
        */
        User user1 = new User();
        user1.setAge(18);
        user1.setName("Tom");
        user1.setEmail("xxx@qq.com");
        boolean b1 = userService.saveOrUpdate(user1);//插入
        System.out.println("b1 = " + b1);

        User user2 = new User();
        user2.setId(1L);
        user2.setAge(20);
        user2.setName("Jerry");
        user2.setEmail("yyy@qq.com");
        boolean b2 = userService.saveOrUpdate(user2);//修改
        System.out.println("b2 = " + b2);
    }

运行结果:

(3)remove 方法

java 复制代码
// 根据 queryWrapper 设置的条件,删除记录
boolean remove(Wrapper<T> queryWrapper);
// 根据 ID 删除
boolean removeById(Serializable id);
// 根据 columnMap 条件,删除记录
boolean removeByMap(Map<String, Object> columnMap);
// 删除(根据ID 批量删除)
boolean removeByIds(Collection<? extends Serializable> idList);

参数说明:

类型 参数名 描述
Wrapper<T> queryWrapper 实体包装类 QueryWrapper
Serializable id 主键 ID
Map<String, Object> columnMap 表字段 map 对象
Collection<? extends Serializable> idList 主键 ID 列表

返回值: boolean,表示删除操作是否成功。

测试:

java 复制代码
    @Test
    public void test_remove() {
        boolean b = userService.removeById(1846557126650564609L);
        System.out.println("b = " + b);
    }

运行结果:

(4)update 方法

java 复制代码
// 根据 UpdateWrapper 条件,更新记录 需要设置sqlset
boolean update(Wrapper<T> updateWrapper);
// 根据 whereWrapper 条件,更新记录
boolean update(T updateEntity, Wrapper<T> whereWrapper);
// 根据 ID 选择修改
boolean updateById(T entity);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList, int batchSize);

参数说明:

类型 参数名 描述
Wrapper<T> updateWrapper 实体对象封装操作类 UpdateWrapper
T entity 实体对象
Collection<T> entityList 实体对象集合
int batchSize 更新批次数量

返回值: boolean,表示更新操作是否成功。

测试:

java 复制代码
    @Test
    public void test_update() {
        User user = new User();
        user.setId(1L);
        user.setAge(18);
        user.setName("Tom");
        user.setEmail("xxx@qq.com");
        boolean b = userService.updateById(user);
        System.out.println("b = " + b);
    }

运行结果:

(5)get 和 list 方法

java 复制代码
查询:
// 根据 ID 查询
T getById(Serializable id);
// 根据 Wrapper,查询一条记录。结果集,如果是多个会抛出异常,随机取一条加上限制条件 wrapper.last("LIMIT 1")
T getOne(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
T getOne(Wrapper<T> queryWrapper, boolean throwEx);
// 根据 Wrapper,查询一条记录
Map<String, Object> getMap(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
<V> V getObj(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

集合:
// 查询所有
List<T> list();
// 查询列表
List<T> list(Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
Collection<T> listByIds(Collection<? extends Serializable> idList);
// 查询(根据 columnMap 条件)
Collection<T> listByMap(Map<String, Object> columnMap);
// 查询所有列表
List<Map<String, Object>> listMaps();
// 查询列表
List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper);
// 查询全部记录
List<Object> listObjs();
// 查询全部记录
<V> List<V> listObjs(Function<? super Object, V> mapper);
// 根据 Wrapper 条件,查询全部记录
List<Object> listObjs(Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录
<V> List<V> listObjs(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

参数说明:

类型 参数名 描述
Serializable id 主键 ID
Wrapper<T> queryWrapper 实体对象封装操作类 QueryWrapper
boolean throwEx 有多个 result 是否抛出异常
T entity 实体对象
Function<? super Object, V> mapper 转换函数
Collection<? extends Serializable> idList 主键 ID 列表
Map<String, Object> columnMap 表字段 map 对象

返回值: 查询结果,可能是实体对象、Map 对象或其他类型。

测试:

java 复制代码
    @Test
    public void test_getOrList(){
        User user = userService.getById(1L);// get返回的是单个对象
        System.out.println("user = " + user);

        List<User> list = userService.list(null);//查询全部,list返回的是一个集合
        System.out.println("list = " + list);
    }

运行结果:

3.分页查询实现

① 导入分页插件

java 复制代码
@MapperScan("com.mihoyo.mapper")
@SpringBootApplication
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class,args);
    }

    //mybatis-plus插件集合加入到ioc容器
    @Bean
    public MybatisPlusInterceptor plusInterceptor(){
        //mybatis-plus的插件集合(所有插件都加入到这个集合:分页插件,乐观锁插件...)
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        //加入分页插件(参数:数据库类型)
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return mybatisPlusInterceptor;
    }
    
}

② 使用分页查询

java 复制代码
@SpringBootTest
public class MybatisPlusTest {
    
    @Autowired
    private UserMapper userMapper;

    @Test
    public void test_page(){
        //接口:IPage --> 实现类:Page(页码,页容量)
        Page<User> page = new Page<>(1,3);
        userMapper.selectPage(page, null);
        //结果也会被封装进page中
        long current = page.getCurrent();//页码
        System.out.println("current = " + current);
        
        long size = page.getSize();//页容量
        System.out.println("size = " + size);
        
        List<User> records = page.getRecords();//当前页的数据
        System.out.println("records = " + records);
        
        long total = page.getTotal();//总条数
        System.out.println("total = " + total);

    }
}

**************** Question:如果想要自定义一个能够分页的查询方法,如何实现呢?****************

步骤:

① mapper 接口定义方法

java 复制代码
public interface UserMapper extends BaseMapper<User> {
    //定义一个方法:根据age进行查询,且分页
    IPage<User> queryByAge(IPage<User> page, @Param("age") Integer age);
}

② mapper.xml

XML 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.mihoyo.mapper.UserMapper">
    <!-- 查询返回的是user集合,所以resultType是集合的泛型,也就是page的泛型user -->
    <select id="queryByAge" resultType="user">
        select * from user where age > #{age}
    </select>
</mapper>

③ 测试

java 复制代码
    @Test
    public void testMyPage(){
        Page<User> page = new Page<>(1,3);
        userMapper.queryByAge(page,1);

        long current = page.getCurrent();//页码
        System.out.println("current = " + current);

        long size = page.getSize();//页容量
        System.out.println("size = " + size);

        List<User> records = page.getRecords();//当前页的数据
        System.out.println("records = " + records);

        long total = page.getTotal();//总条数
        System.out.println("total = " + total);
    }

运行结果:

4.条件构造器使用

(1)条件构造器继承

java 复制代码
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", "John"); // 添加等于条件
queryWrapper.ne("age", 30); // 添加不等于条件
queryWrapper.like("email", "@gmail.com"); // 添加模糊匹配条件
等同于: 
delete from user where name = "John" and age != 30
                                  and email like "%@gmail.com%"
// 根据 entity 条件,删除记录
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);

使用MyBatis-Plus 的条件构造器,你可以构建灵活、高效的查询条件,而不需要手动编写复杂的 SQL 语句。

它提供了许多方法来支持各种条件操作符,并且可以通过链式调用来组合多个条件。这样可以简化查询的编写过程,并提高开发效率。

(2)条件构造器继承结构

条件构造器类结构:

Wrapper : 条件构造抽象类,最顶端父类

  • AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
    • QueryWrapper : 查询/删除条件封装
    • UpdateWrapper : 修改条件封装
    • AbstractLambdaWrapper : 使用 Lambda 语法
      • LambdaQueryWrapper :用于 Lambda 语法使用的查询 Wrapper
      • LambdaUpdateWrapper : Lambda 更新封装 Wrapper

(3)基于QueryWrapper 组装条件

Ⅰ.组装查询条件

需求:查询用户名包含 a,年龄在 20 到 30 之间,并且邮箱不为空的用户信息

java 复制代码
@SpringBootTest
public class MybatisPlusQueryWrapper {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void test_01(){
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        //条件(动态调用wrapper的方法完成拼接)
        /*
        queryWrapper.like("name","a");
        queryWrapper.between("age",20,30);
        queryWrapper.isNotNull("email");
        */
        //链式调用
        queryWrapper.like("name","a").between("age",20,30).isNotNull("email");
        //select * from user where name like "%a%" and age >= 20 age <= 30 email is not null
        List<User> users = userMapper.selectList(queryWrapper);
    }
}

运行结果:

Ⅱ. 组装排序条件

需求:按年龄降序查询用户,如果年龄相同则按 id 升序排列

java 复制代码
    @Test
    public void test_02(){
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.orderByDesc("age").orderByAsc("id");
        // order by age desc, id asc;
        List<User> users = userMapper.selectList(queryWrapper);
    }

运行结果:

Ⅲ. 组装删除条件

需求:删除 email 为空的用户

java 复制代码
    @Test
    public void test_03() {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.isNull("email");
        userMapper.delete(queryWrapper);
    }

运行结果:

Ⅳ. and 和 or 关键字使用(修改):

需求:将年龄大于 20 并且用户名中包含有 a 或 邮箱为 null 的用户信息修改

java 复制代码
    @Test
    public void test_04(){
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.gt("age",20).like("name","a") //条件直接调用方法,默认 and 拼接
                .or().isNull("email"); //想要使用 or 拼接,前面必须紧跟着一个 or()
        User user = new User();
        user.setAge(88);
        user.setEmail("hehehe@qq.com");
        userMapper.update(user,queryWrapper);
    }

运行结果:

Ⅴ. 指定列映射查询

需求:查询用户信息的 name 和 age 字段

java 复制代码
    @Test
    public void test_05() {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.gt("id", 1L);
        //默认是查询全部列
        queryWrapper.select("name", "age");//指定要查询的列名
        userMapper.selectList(queryWrapper);
    }

运行结果:

Ⅵ. condition判断组织条件

需求:前端传入了两个参数(name 和 age),如果 name 不为空,作为 = 条件查询。age > 18,作为 = 条件查询

方案一:手动判断

java 复制代码
    @Test
    public void test_06() {
        //模拟前端输入
        String name = "xxx";
        Integer age = 20;

        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        //动态条件判断
        if (!StringUtils.isBlank(name)) {
            queryWrapper.eq("name", name);
        }
        if (age != null || age > 18) {
            queryWrapper.eq("age", age);
        }
        userMapper.selectList(queryWrapper);
    }

方案二:拼接 condition 判断

每个条件方法都会有一个 boolean 类型的 condition,允许我们放入一个比较表达式。

如果表达式为 true,整个条件生效;否则不生效。

java 复制代码
    @Test
    public void test_06() {
        //模拟前端输入
        String name = "xxx";
        Integer age = 20;

        QueryWrapper<User> queryWrapper = new QueryWrapper<>();

        queryWrapper.eq(!StringUtils.isBlank(name),"name",name);
        queryWrapper.eq(age != null || age > 18,"age", age);
        userMapper.selectList(queryWrapper);
    }

运行结果:

(4)基于 UpdateWrapper 组装条件

需求:将年龄大于 20 并且用户名中包含有 a 或 邮箱为 null 的用户信息修改

使用 queryWrapper:

java 复制代码
    @Test
    public void test_04(){
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.gt("age",20).like("name","a") //条件直接调用方法,默认 and 拼接
                .or().isNull("email"); //想要使用 or 拼接,前面必须紧跟着一个 or()
        User user = new User();
        user.setAge(88);
        user.setEmail("hehehe@qq.com");
        userMapper.update(user,queryWrapper);
    }

缺点:

① 必须要准备修改的实体类数据(User 对象)

② 不能将某个属性值修改为 null。(queryWrapper 发现该属性值是 null,就不会对其修改)


使用 updateWrapper:

java 复制代码
    @Test
    public void test_update(){
        UpdateWrapper<User> queryWrapper = new UpdateWrapper<>();
        queryWrapper.gt("age",20).like("name","a")
                .or().isNull("email")
                .set("name",null)//直接修改,可以是任意值
                .set("age",99);
        userMapper.update(null,queryWrapper);
    }

优点:

① 可以通过 set 方法直接携带修改数据

② 可以修改成任意数据

运行结果:

(5)基于LambdaQueryWrapper 组装条件

需求:查询用户名包含 a,年龄在 20 到 30 之间,并且邮箱不为空的用户信息

java 复制代码
    @Test
    public void test_01() {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.like("name", "a").between("age", 20, 30).isNotNull("email");
        //select * from user where name like "%a%" and age >= 20 age <= 30 email is not null
        List<User> users = userMapper.selectList(queryWrapper);
    }

缺点:

在条件方法(like,between...)中,我们每次都要书写属性名的字符串,这很容易出现书写错误!

使用方法引用,可以有效避免这个问题。

java 复制代码
    @Test
    public void test_01() {
        //使用LambdaQueryWrapper
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(User::getName, "a").between(User::getAge, 20, 30).isNotNull(User::getEmail);
        //select * from user where name like "%a%" and age >= 20 age <= 30 email is not null
        List<User> users = userMapper.selectList(queryWrapper);
    }

注意:

虽然被引用处是一个函数式接口,可以使用 Lambda 表达式。

但由于 MybatisPLus 的 LambdaQueryWrapper 在内部对 SFunction 接口 进行了封装,只有使用 方法引用 能确保发挥 LambdaQueryWrapper 的完整功能,并避免翻译错误。

所以,不能直接使用 Lambda 表达式:like(user -> user.getName(),"a")

(6)基于 LambdaUpdateWrapper 组装条件

需求:将年龄大于 20 并且用户名中包含有 a 或 邮箱为 null 的用户信息修改

使用 updateWrapper:

java 复制代码
    @Test
    public void test_update(){
        UpdateWrapper<User> queryWrapper = new UpdateWrapper<>();
        queryWrapper.gt("age",20).like("name","a")
                .or().isNull("email")
                .set("name",null)//直接修改,可以是任意值
                .set("age",99);
        userMapper.update(null,queryWrapper);
    }

使用 LambdaUpdateWrapper:

java 复制代码
    @Test
    public void test_update(){
        //使用LambdaUpdateWrapper
        LambdaUpdateWrapper<User> queryWrapper = new LambdaUpdateWrapper<>();
        queryWrapper.gt(User::getAge,20).like(User::getName,"a")
                .or().isNull(User::getEmail)
                .set(User::getAge,null)
                .set(User::getEmail,99);
        userMapper.update(null,queryWrapper);
    }

5.核心注解使用

(1)理解和介绍

MyBatis-Plus是一个基于 MyBatis 框架的增强工具,提供了一系列简化和增强的功能,用于加快开发人员在使用 MyBatis 进行数据库访问时的效率。

MyBatis-Plus 提供了一种基于注解的方式来定义和映射数据库操作,其中的注解起到了重要作用。

理解:

java 复制代码
public interface UserMapper extends BaseMapper<User> {

}

此接口对应的方法为什么会自动触发 user 表的crud呢?

默认情况下, 根据指定的 <实体类> 的名称对应数据库表名,属性名对应数据库的列名。

但不是所有数据库的信息和实体类都完全映射!

例如: 表名 t_user → 实体类 User 这时候就不对应了。

自定义映射关系就可以使用 mybatis-plus 提供的注解即可!

(2)@TableName 注解

**描述:**表名注解,标识实体类对应的表

**使用位置:**实体类

java 复制代码
@TableName("t_user") //对应数据库表名
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
}

注意:

① 特殊情况:如果表名和实体类名相同(忽略大小写)可以省略该注解

② 如果每个数据库表都以 t_ 开头,就要对每个实体类加上 @TableName 注解,过于麻烦。

可以在 application.yml 中设置 全局设置前缀:

java 复制代码
mybatis-plus: # mybatis-plus的配置
  global-config:
    db-config:
      table-prefix: t_ # 表名前缀字符串

(3)@TableId 注解

**描述:**主键注解

**使用位置:**实体类主键字段

java 复制代码
@TableName("t_user")
public class User {
    @TableId(value="主键列名",type=主键策略)
    private Long id;
    private String name;
    private Integer age;
    private String email;
}

属性:

属性 类型 必须指定 默认值 描述
value String "" 主键字段名
type Enum IdType.NONE 指定主键类型

IdType 属性可选值:

描述
AUTO 数据库 ID 自增 (mysql 配置主键自增长 auto_increment)
ASSIGN_ID(默认) 分配 ID(主键类型为 Number(Long )或 String)(since 3.3.0), 使用接口 IdentifierGenerator nextId 方法 (默认实现类为 DefaultIdentifierGenerator 雪花算法)

细节:

① 雪花算法(默认):

  • 数据库主键类型设置:bigint / varchar(64)
  • 实体类主键类型设置:long / string
  • 随机生成一个主键值,与之前的不会重复

② auto:mysql 在创建数据库表时,必须要给主键设置自增:auto_increment

③ 如果有多个表都想设置主键为自增,一个个设置会很麻烦,可以在 application.yml 中全局设置 :

java 复制代码
mybatis-plus: # mybatis-plus的配置
  global-config:
    db-config:
      id-type: auto # 全局设置主键策略

使用场景:

主键的列名 与 实体类的属性名不一致 --> 使用 value 指定主键名

(u_id --> uid 是一致的,因为 MyBatis-Plus 会自动开启驼峰命名风格映射)

② 指定插入新数据时,主键值如何生成(自增 还是 随机 --> 使用 IdType 指定主键策略)。


**雪花算法(Snowflake Algorithm):**是一种用于生成唯一ID的算法。它由Twitter公司提出,用于解决分布式系统中生成全局唯一ID的需求。

在传统的自增ID生成方式中,使用单点数据库生成 ID 会成为系统的瓶颈,而雪花算法通过在分布式系统中生成唯一ID,避免了单点故障和性能瓶颈的问题。

雪花算法生成的ID是一个64位的整数,由以下几个部分组成:

① 时间戳:41位,精确到毫秒级,可以使用69年。

② 节点ID:10位,用于标识分布式系统中的不同节点。

③ 序列号:12位,表示在同一毫秒内生成的不同ID的序号。

通过将这三个部分组合在一起,雪花算法可以在分布式系统中生成全局唯一的ID,并保证ID的生成顺序性。

雪花算法的工作方式如下:

① 当前时间戳从某一固定的起始时间开始计算,可以用于计算 ID 的时间部分。

② 节点 ID 是分布式系统中每个节点的唯一标识,可以通过配置或自动分配的方式获得。

③ 序列号用于记录在同一毫秒内生成的不同ID的序号,从0开始自增,最多支持4096个ID生成。

需要注意的是,雪花算法依赖于系统的时钟,需要确保系统时钟的准确性和单调性,否则可能会导致生成的ID不唯一或不符合预期的顺序。

总结: 雪花算法是一种简单但有效的生成唯一ID的算法,广泛应用于分布式系统中,如微服务架构、分布式数据库、分布式锁等场景,以满足全局唯一标识的需求。

(4)@TableField 注解

**描述:**字段注解(非主键)

java 复制代码
@TableName("t_user")
public class User {
    @TableId
    private Long id;
    @TableField("nickname")
    private String name;
    private Integer age;
    private String email;
}
属性 类型 必须指定 默认值 描述
value String "" 数据库字段名
exist boolean true 是否为数据库字段

使用场景:

① 非主键的列名 与 实体类的属性名不一致 --> 使用 value 指定字段名

(u_name --> uname 是一致的,因为 MyBatis-Plus 会自动开启驼峰命名风格映射)

② 该实体类中定义了其他成员变量,但并不是作为数据库表中的字段 --> 使用 exist = false

三、MyBatis-Plus 高级扩展

1.逻辑删除实现

概念:

逻辑删除,可以方便地实现对数据库记录的逻辑删除而不是物理删除。逻辑删除是指通过更改记录的状态或添加标记字段来模拟删除操作,从而保留了删除前的数据,便于后续的数据分析和恢复。

  • 物理删除: 真实删除,将对应数据从数据库中删除,之后查询不到此条被删除的数据
  • 逻辑删除: 假删除,将对应数据中代表是否被删除字段的状态修改为 "被删除状态",之后在数据库中仍旧能看到此条数据记录

逻辑删除实现:

① 数据库和实体类添加逻辑删除字段(可以是一个布尔类型、整数类型或枚举类型)

sql 复制代码
# int 类型 规定: 1 逻辑删除 0 未逻辑删除
ALTER TABLE USER ADD deleted INT DEFAULT 0 ;

② 实体类添加逻辑删除属性,属性添加 @TableLogic 注解

java 复制代码
@Data
public class User {

   // @TableId
    private Integer id;
    private String name;
    private Integer age;
    private String email;
    
    @TableLogic
    //逻辑删除字段 int mybatis-plus下,默认 逻辑删除值为1 未逻辑删除 0 
    private Integer deleted;
}

细节:

① 实体类中的逻辑删除属性,要与数据库中的逻辑删除字段相对应。

② 设置逻辑删除后:

  • 删除数据,自动变成修改此条数据的的逻辑删除字段 deleted,将其变成 1
  • 查询数据,默认只查询 deleted = 0,即未被逻辑删除的数据

③ 如果有多个表都要设置逻辑删除,可以进行全局设置:

java 复制代码
mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted # 全局逻辑删除的实体属性名
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

③ 演示逻辑删除操作

java 复制代码
//逻辑删除
@Test
public void testQuick5(){
    //逻辑删除
    userMapper.deleteById(3);
}

运行结果:

2.乐观锁实现

(1)悲观锁和乐观锁场景和介绍

并发问题场景演示:

**解决思路:**乐观锁和悲观锁是在并发编程中用于处理并发访问和资源竞争的两种不同的锁机制。

  • 悲观锁: 悲观锁的基本思想是,在整个数据访问过程中,将共享资源锁定,以确保其他线程或进程不能同时访问和修改该资源。悲观锁的核心思想是"先保护,再修改"。在悲观锁的应用中,线程在访问共享资源之前会获取到锁,并在整个操作过程中保持锁的状态,阻塞其他线程的访问。只有当前线程完成操作后,才会释放锁,让其他线程继续操作资源。这种锁机制可以确保资源独占性和数据的一致性,但是在高并发环境下,悲观锁的效率相对较低。
  • 乐观锁: 乐观锁的基本思想是,认为并发冲突的概率较低,因此不需要提前加锁,而是在数据更新阶段进行冲突检测和处理。乐观锁的核心思想是"先修改,后校验"。在乐观锁的应用中,线程在读取共享资源时不会加锁,而是记录特定的版本信息。当线程准备更新资源时,会先检查该资源的版本信息是否与之前读取的版本信息一致,如果一致则执行更新操作,否则说明有其他线程修改了该资源,需要进行相应的冲突处理。乐观锁通过避免加锁操作,提高了系统的并发性能和吞吐量,但是在并发冲突较为频繁的情况下,乐观锁会导致较多的冲突处理和重试操作。

**注意:**悲观锁和乐观锁是两种解决并发数据问题的思路,不是具体技术。

具体技术和方案:

① 乐观锁实现方案和技术:

  • 版本号/时间戳:为数据添加一个版本号或时间戳字段,每次更新数据时,比较当前版本号或时间戳与期望值是否一致,若一致则更新成功,否则表示数据已被修改,需要进行冲突处理。
  • CAS(Compare-and-Swap):使用原子操作比较当前值与旧值是否一致,若一致则进行更新操作,否则重新尝试。
  • 无锁数据结构:采用无锁数据结构,如无锁队列、无锁哈希表等,通过使用原子操作实现并发安全。

② 悲观锁实现方案和技术:

  • 锁机制:使用传统的锁机制,如互斥锁(Mutex Lock)或读写锁(Read-Write Lock)来保证对共享资源的独占访问。
  • 数据库锁:在数据库层面使用行级锁或表级锁来控制并发访问。
  • 信号量(Semaphore):使用信号量来限制对资源的并发访问。

版本号乐观锁技术的实现流程:

  1. 每条数据添加一个版本号字段 version
  2. 取出记录时,获取当前 version
  3. 更新时,检查获取版本号是不是数据库当前最新版本号
  4. 如果是 [ 证明没有人修改数据 ],执行更新,set 数据更新,version = version+ 1
  5. 如果 version 不对 [ 证明有人已经修改了 ],我们现在的其他记录就是失效数据,就更新失败

(2)使用mybatis-plus数据使用乐观锁(基于版本号技术)

① 添加版本号更新插件

java 复制代码
@MapperScan("com.mihoyo.mapper")
@SpringBootApplication
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class,args);
    }

    //mybatis-plus插件集合加入到ioc容器
    @Bean
    public MybatisPlusInterceptor plusInterceptor(){
        //mybatis-plus的插件集合(所有插件都加入到这个集合:分页插件,乐观锁插件...)
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        //乐观锁(版本号插件)
        mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return mybatisPlusInterceptor;
    }
    
}

② 数据库添加版本号 version 字段

sql 复制代码
ALTER TABLE USER ADD VERSION INT DEFAULT 1 ;  # int 类型 乐观锁字段

③ 实体类添加版本号属性,属性添加 @Version 注解

java 复制代码
@Data
public class User {

   // @TableId
    private Integer id;
    private String name;
    private Integer age;
    private String email;
    
    @TableLogic
    //逻辑删除字段 int mybatis-plus下,默认 逻辑删除值为1 未逻辑删除 0 
    private Integer deleted;

    @Version
    private Integer version;//版本号字段
}

细节:

  • 支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime
  • 仅支持 updateById(id) 与 update(entity, wrapper) 方法

④ 正常更新使用即可

java 复制代码
//演示乐观锁生效场景
@Test
public void testQuick7(){
    //步骤1: 先查询,取出数据,从而获取当前版本号
    User user  = userMapper.selectById(5);
    User user1  = userMapper.selectById(5);

    user.setAge(20);
    user1.setAge(30);

    //修改成功,version + 1 = 2
    userMapper.updateById(user);
    //版本号校验不对,乐观锁生效,失败!
    userMapper.updateById(user1);
}

运行结果:

3.防全表更新和删除实现

针对 update 和 delete 语句

**作用:**阻止恶意的全表更新删除

步骤:

① 添加防止全表更新和删除拦截器

java 复制代码
@MapperScan("com.mihoyo.mapper")
@SpringBootApplication
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class,args);
    }

    //mybatis-plus插件集合加入到ioc容器
    @Bean
    public MybatisPlusInterceptor plusInterceptor(){
        //mybatis-plus的插件集合(所有插件都加入到这个集合:分页插件,乐观锁插件...)
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        //防止全表删除和更新的拦截器
        mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        return mybatisPlusInterceptor;
    }
    
}

② 测试全部更新或者删除

java 复制代码
    @Test
    public void test_delete(){
        //全表删除 delete from user;
        userMapper.delete(null);
    }

运行结果:

四、MyBatis-Plus代码生成器(MyBatisX插件)

1.Mybatisx 插件逆向工程

MyBatis-Plus为我们提供了强大的 mapper 和 service 模板,能够大大的提高开发效率

但是在真正开发过程中,MyBatis-Plus 并不能为我们解决所有问题,例如一些复杂的SQL,多表联查,我们就需要自己去编写代码和SQL语句,我们该如何快速的解决这个问题呢,这个时候可以使用 MyBatisX 插件。

MyBatisX一款基于 IDEA 的快速开发插件,为效率而生。

2.MyBatisX 快速代码生成

使用 mybatisX 插件,自动生成 sql 语句实现(快捷键:Alt + enter)

具体介绍,可查看官方文档:Mybatis X 插件 | MyBatis-Plus (baomidou.com)https://baomidou.com/guides/mybatis-x/

相关推荐
Coder码匠26 分钟前
Dockerfile 优化实践:从 400MB 到 80MB
java·spring boot
菜的不敢吱声5 小时前
swift学习第4天
服务器·学习·swift
李慕婉学姐8 小时前
【开题答辩过程】以《基于JAVA的校园即时配送系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·开发语言·数据库
想进部的张同学8 小时前
hilinux-3599---设备学习---以及部署yolo
学习·yolo·海思
HyperAI超神经9 小时前
【vLLM 学习】Rlhf
人工智能·深度学习·学习·机器学习·vllm
奋进的芋圆9 小时前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
sxlishaobin10 小时前
设计模式之桥接模式
java·设计模式·桥接模式
model200510 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉10 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国10 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos