【框架学习 | 第一篇】一篇文章读懂MyBatis

文章目录

  • 1.Mybatis介绍
    • 1.1Mybatis历史
    • 1.2Mybatis特点
    • 1.3与其他持久化框架对比
    • 1.4对象关系映射——ORM
  • 2.搭建Mybatis
    • 2.1引入依赖
    • 2.2创建核心配置文件
    • 2.3创建表、实体类、mapper接口
    • 2.4创建映射文件
      • 2.4.1映射文件命名位置规则
      • 2.4.2编写映射文件
      • 2.4.3修改核心配置文件中映射文件路径
    • 2.5Junit测试
    • 2.6修改、删除、查询功能测试
  • 3.核心配置文件详讲(了解)
    • 3.1environment、properties讲解
    • 3.2typeAliases讲解
    • 3.3mappers
      • 3.3.1将UserMapper.xml转移到新目录下
      • 3.3.2在核心配置文件中引入映射文件
  • 4.MyBatis获取参数值的两种方式(重要)
    • 4.1区分${}和#{}
      • 4.4.1区分点
      • 4.4.2预编译过程
      • 4.4.3 ${}的sql注入问题
    • 4.2获取参数值的各种情况
      • 4.2.1情况一:参数为单个的字面量类型
      • 4.2.2情况二:参数为多个时
        • (1)使用原生的map
        • (2)使用自己的map(${}、#{})
      • 4.2.3参数是实体类类型的参数
      • 4.2.4使用@Param注解命名参数(以这个和实体类类型两种情况为标准)
    • 4.3举例
      • 4.3.1mapper接口
      • 4.3.2映射文件
      • 4.3.3测试
    • 4.4小结
    • 4.5@Param标识的源码解析
  • 5.MyBatis的各种查询功能
    • 5.1查询的数据只有一条
      • 5.1.1接收情况
      • 5.1.2举例
    • 5.2查询的数据有多条
    • 5.3查询单个数据(聚合函数)
  • 6.特殊SQL的执行
    • 6.1模糊查询
    • 6.2批量删除
    • 6.3动态设置表名(会出现sql注入问题)
    • 6.4添加功能获取自增的主键
  • 7.自定义映射resultMap
    • 7.1搭建mybatis框架
    • 7.2字段名和属性名不一致
      • 7.2.1给字段名起别名
      • 7.2.2全局配置mapUnderscoreToCamelCase
      • 7.2.3resultMap(建议使用)
    • 7.3多对一(从员工到部门)映射关系
      • 7.3.1通过级联属性(连接查询)赋值
      • 7.3.2通过association标签
      • 7.3.3通过分步查询(推荐)
        • (1)通过id查询员工信息、并查出did
        • (2)通过did查询员工所对应的部门
        • (3)测试
      • 7.3.4分步查询的好处:延迟加载
        • (1)开启延迟加载之前
        • (2)开启全局延迟加载之后
        • (3)可以局部关闭延迟加载
    • 7.4.一对多映射关系
      • 7.4.1通过collection解决一对多映射关系
  • 8.动态SQL
    • 8.1if-where-trim标签
      • 8.1.1if标签
      • 8.1.2where标签
      • 8.1.3trim标签
    • 8.2choose-when-ohterwise标签
    • 8.3.foreach标签
      • 8.3.1实现批量删除方式一
      • 8.3.2实现批量删除方式二
      • 8.3.3实现批量添加
    • 8.4SQL标签
  • 9.MyBatis缓存
    • 9.1介绍
      • 9.1.1一级缓存
      • 9.1.2二级缓存
    • 9.2一级缓存
      • 9.2.1一级缓存生效(默认生效)
      • 9.2.2一级缓存失效
    • 9.3二级缓存
      • 9.3.1二级缓存开启
      • 9.3.2二级缓存失效
      • 9.3.3二级缓存的相关配置
    • 9.4缓存查询的顺序
  • 10_MyBaits的逆向工程
    • 10.1创建逆向工程的步骤
    • 10.2豪华尊享版两种函数的区别
    • 10.3QBC查询
    • 10.4QBC修改
  • 11_MyBatis分页
    • 11.1使用步骤
    • 11.2使用

在这里插入图片描述

框架系列第一篇主要围绕MyBatis展开,从MyBatis介绍、搭建学习环境,再到探讨映射文件获取参数值(${}、#{})@param注解命名参数、各种查询功能(1条、多条、单个数据)、再到特殊SQL的执行(模糊查询、批量删除)、自定义resultMap(重点讲述了多对一、一对多的解决方案)、动态SQL(if、where、foreach标签)MyBatis的一二级缓存

1.Mybatis介绍

1.1Mybatis历史

  1. 原是Apache的一个开源项目iBatis, 2010年6月这个项目由Apache Software Foundation 迁移到了Google Code,随着开发团队转投Google Code旗下, iBatis3.x正式更名为MyBatis ,代码于2013年11月迁移到Github(下载地址见后)
  2. iBatis一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。 iBatis提供的持久层框架包括SQL Maps和Data Access Obje

1.2Mybatis特点

  1. Mybatis是一个半自动的ORM(Object Relation Mapping)框架
  2. 面向接口编程思想:可以使用简单的XML或注解用于配置和原始映射,将接口和Java的实体类对象映射成数据库中的记录
  3. 支持定制化SQL、存储过程以及==高级映射(一对多的映射关系)==的持久化框架
  4. 封装了几乎所有的JDBC代码和手动设置参数以及获取结果集

1.3与其他持久化框架对比

  • JDBC
    • SQL夹杂在Java代码中耦合度高,导致硬编码内伤。
    • 维护不易且实际开发需求中 SQL有变化,频繁修改的情况多见。
    • 代码冗长,开发效率低。
  • Hibernate和IPA
    • 操作简便,开发效率高
    • 程序中的长难复杂 SQL需要绕过框架
    • 内部自动生产的 SQL,不容易做特殊优化。
    • 基于全映射的全自动框架,大量字段的 POJ0 进行部分映射时比较困难
    • 反射操作太多,导致数据库性能下降
  • MyBatis
    • 轻量级,性能出色
    • SQL和Java 编码分开,功能边界清晰。Java代码专注业务、SQL语句专注数据。
    • 开发效率稍逊于HIbernate,但是完全能够接受

1.4对象关系映射——ORM

ORM即Object Relationship Mapping,对象关系映射!

解释/含义
对象java的实体类对象
关系关系型数据库
映射二者之间的对应关系

在java概念和数据库概念中的对应关系如下:

java概念数据库概念
属性字段/列
对象记录/行

2.搭建Mybatis

搭建一个基于Maven的project

2.1引入依赖

<dependencies>
    <!-- Mybatis核心 -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.10</version>
    </dependency>
    <!-- junit测试 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <!-- MySQL驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.29</version>
    </dependency>

    <!-- log4j日志 -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
</dependencies>

2.2创建核心配置文件

  1. 习惯上命名为mybatis-config.xml,这个文件名仅仅只是建议,并非强制要求。
  2. 将来整合Spring之后,这个配置文件可以省略
  3. 核心配置文件主要用于配置连接数据库的环境以及MyBatis的全局配置信息核心配置文件存放的位置是src/main/resources目录下

image-20240304232348464

2.3创建表、实体类、mapper接口

image-20240304232548553

MyBatis 中的 mapper 接口相当于以前的 DAO 接口,但是区别在于, mapper 仅仅是接口,我们不需要提供实现类(面向接口编程);

image-20240304232617262

2.4创建映射文件

2.4.1映射文件命名位置规则

  1. 表所对应的 实体类的类名+Mapper.xml ,例如:表 t_user,映射的实体类为 User,所对应的映射文件为 UserMapper.xml放到resources目录下
  2. 因此一个映射文件对应一个实体类,对应一张表的操作
  3. MyBatis 映射文件用于编写 SQL,访问以及操作表中的数据

2.4.2编写映射文件

MyBatis中面向接口编程的两个一致:

  •  ==映射文件的namespace要和mapper接口的全类名保持一致==
    
  •  ==映射文件中SQL语句的id要和mapper接口中的方法名一致==
    

image-20240304232819888

首先先根据核心配置文件中的来引入映射文件,再根据方法名匹配到SQL语句

2.4.3修改核心配置文件中映射文件路径

若修改核心配置文件中的映射文件路径,则需将UserMapper.xml文件放置在resources/mapper文件夹下

image-20240304232954726

2.5Junit测试

image-20240304233129822

2.6修改、删除、查询功能测试

  • UserMapper.java

image-20240304233507867

  • UserMapper.xml

image-20240304233546616

3.核心配置文件详讲(了解)

MyBatis核心配置文件中,标签的顺序
    properties?,settings?,typeAliases?,typeHandlers?,
    objectFactory?,objectWrapperFactory?,reflectorFactory?,
    lugins?,environments?,databaseIdProvider?,mappers?

3.1environment、properties讲解

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <!--
        MyBatis核心配置文件中,标签的顺序:
        properties?,settings?,typeAliases?,typeHandlers?,
        objectFactory?,objectWrapperFactory?,reflectorFactory?,
        plugins?,environments?,databaseIdProvider?,mappers?
    -->

    <!--引入properties文件,此时就可以${属性名}的方式访问属性值-->
    <properties resource="jdbc.properties" />

    <!--设置类型别名-->
    <typeAliases>
        <!--
            typeAlias:设置某个类型的别名
            属性:
                type:设置需要设置别名的类型
                alias:设置某个类型的别名,若不设置该属性,那么该类型拥有默认的别名,即类名且不区分大小写
        -->
        <!--<typeAlias type="com.atguigu.User"></typeAlias>-->
        <!--以包为单位,将包下所有的类型设置默认的类型别名,即类名且不区分大小写-->
        <package name="com.atguigu.mybatis.pojo"/>
    </typeAliases>

    <!--
        environments:配置多个连接数据库的环境
        属性:
            default:设置默认使用的环境的id
    -->
    <environments default="development">
        <!--
            environment:配置某个具体的环境
            属性:
                id:表示连接数据库的环境的唯一标识,不能重复
        -->
        <environment id="development">
            <!--
                transactionManager:设置事务管理方式
                属性:
                    type="JDBC|MANAGED"
                    JDBC:表示当前环境中,执行SQL时,使用的是JDBC中原生的事务管理方式,事务的提交或回滚需要手动处理
                    MANAGED:被管理,例如被Spring管理
            -->
            <transactionManager type="JDBC"/>
            <!--
                dataSource:配置数据源,与spring整合后不用再设置dataSource
                属性:
                    type:设置数据源的类型
                    type="POOLED|UNPOOLED|JNDI"
                    POOLED:表示使用数据库连接池缓存数据库连接,即会将创建的连接进行缓存,下次使用可以从缓存中直接获取,不需要重新创建
                    UNPOOLED:表示不使用数据库连接池,即每次使用连接都需要重新创建
                    JNDI:表示使用上下文中的数据源
            -->
            <dataSource type="POOLED">
                <!--设置连接数据库的驱动-->
                <property name="driver" value="${jdbc.driver}"/>
                <!--设置连接数据库的连接地址-->
                <property name="url" value="${jdbc.url}"/>
                <!--设置连接数据库的用户名-->
                <property name="username" value="${jdbc.username}"/>
                <!--设置连接数据库的密码-->
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>

    <!--引入映射文件-->
    <mappers>
        <!--<mapper resource="mappers/UserMapper.xml"/>-->
        <!--
            以包为单位引入映射文件
            要求:
            1、mapper接口所在的包要和映射文件所在的包一致
            2、mapper接口要和映射文件的名字一致
        -->
        <package name="com.atguigu.mybatis.mapper"/>
    </mappers>
</configuration>

3.2typeAliases讲解

作用:设置类型别名

<!-- 设置类型别名
        方式一:以某一个类为单位(不常用)
        方式二:以包为单位,将包下所有的类型设置为默认类型别名,即类型且不区分大小写(常用)
-->
<typeAliases>
    <!--
        方式一:
        typeAlias:设置某个类型的别名
        属性:
            type:设置需要设置别名的类型
            alias:设置某个类型的别名,如果不设置该属性,那么该类型拥有默认的别名,即类名而且不区分大小写
    -->
    <typeAlias type="com.peng.mybatis.pojo.User" alias="User"></typeAlias>

    <!--
        方式二:
    -->
    <package name="com.peng.mybatis.pojo.User"></package>

</typeAliases>

3.3mappers

3.3.1将UserMapper.xml转移到新目录下

image-20240304234039332

在 resources 包下面创建 directory(目录)创建 package(包) 是不一样的

不能用com.peng.mybatis.mapper 这种方式创建多个文件夹,这样创建出来的是一个包名;应该用 com/peng/mybatis/mapper 这种方式。

3.3.2在核心配置文件中引入映射文件

<!-- 引入映射文件
        以包为单位引入映射文件
            要求:mapper接口所在的包要和映射文件所在的包一致
                 mapper接口要和映射文件的名字一致
-->
<mappers>
    <package name="com.peng.mybatis.mapper"/>
</mappers>

4.MyBatis获取参数值的两种方式(重要)

4.1区分${}和#{}

4.4.1区分点

  1. MyBatis获取参数值的两种方式:${}和#{}

    ${}本质为字符串替换

    #{}本质为占位符赋值

    ${}和#{}的相同点为都能获取参数变量的值,在预编译语法上则不同

${}#{}
预编译不行将#{变量}编译成?,在执行时后再取值(自动加上引号),防止sql注入
应用场景传入的参数是sql片段的场景在sql映射文件中动态拼接sql时的开发场景(获取变量)

传入的参数是sql片段的场景

<select id="getUserPage" resultType="com.ymxx.oa.bean.User">
        ${sql片段}
</select>
  • 对于这样外部传入的sql,就不能使用#{},上面也说了,#{}会进行预编译,检测到该sql片段是个字符串,就会加上引号,即’sql片段’,这样就是字符串了而不是sql,执行会报错。

4.4.2预编译过程

mybatis在预编译过程大体上可以分为 数据类型检查安全检查 两部分

数据类型检查若检测到为数值类型,就不加引号,即?若检测到位字符串类型,就加上引号,即’?'

安全检查:若变量的值带有引号,会对引号进行转义处理,这样可以防止sql注入

4.4.3 ${}的sql注入问题

用${}时要特别注意sql注入的风险,如果该sql片段是根据用户的输入拼接的,要注意检查sql注入的问题,防止数据的泄露与丢失!

案例:

//sql语句
select * from ${tableName} where name = ${name}

如果tableName的值为 user; delete user; –,该sql最终解析成

select * from user; delete user; -- where name = xxx

结果查询了整张表,然后把user表给删了

4.2获取参数值的各种情况

获取参数值的情况可以分为以下四种情况:

  • 参数为单个的字面量类型
  • 参数为多个
    • 使用原生的map
    • 使用自己的map(${}、#{})
  • 参数是实体类类型的参数
  • 使用@Param注解命名参数

4.2.1情况一:参数为单个的字面量类型

  • 可以通过KaTeX parse error: Expected 'EOF', got '#' at position 4: {}和#̲{}以任意的字符串获取参数值 …{}需要额外加上单引号

4.2.2情况二:参数为多个时

(1)使用原生的map
  • mybatis会把形参的值放在一个map集合中,以两种方式进行存储
  • arg0,arg1为键或者以param0,param1为键
  • 以参数值为值,可以混着用
(2)使用自己的map(${}、#{})
  • 使用自己的map可以通过KaTeX parse error: Expected 'EOF', got '#' at position 4: {}和#̲{}以键名的方式获取参数值==…{}需要额外加上单引号
  • 可以==通过KaTeX parse error: Expected 'EOF', got '#' at position 4: {}和#̲{}以自己设置的键名的方式获取…{}需要额外加上单引号

4.2.3参数是实体类类型的参数

  • 通过${}和#{}以自己设置的键名实体类里面的属性)的方式获取参数值
  • 使用${}需要额外加上单引号

4.2.4使用@Param注解命名参数(以这个和实体类类型两种情况为标准)

  • mybatis会把形参的值放在一个map集合中,以两种方式进行存储
  • 以自己使用的==@Param注解的参数名为键或者以param1,param2为键==,
  • 以参数值为值,可以混着用

4.3举例

4.3.1mapper接口

image-20240305084222152

4.3.2映射文件

image-20240305085039896

4.3.3测试

image-20240305090616441

4.4小结

  1. 有实体类的时候直接用属性值来访问

    例如,如果有一个 User 实体类,其中包含了 idname 两个属性,那么在 SQL 语句中就可以直接引用这些属性:

    <select id="getUserById" resultType="User">
        SELECT * FROM users WHERE id = #{id}
    </select>
    
  2. 没有实体类的时候用 @Param 注解命名参数,不管字面量单个还是多个,是不是 map 集合都可以使用自己用注解定义的参数来访问

    当没有实体类或需要额外命名参数时,可以使用 @Param 注解来命名参数。例如,假设我们需要在 SQL 中引用多个参数,可以这样做:

    • mapper接口:
    public interface UserMapper {
        List<User> getUsersByAgeRange(@Param("minAge") int minAge, @Param("maxAge") int maxAge);
    }
    
    • 映射文件:

      <select id="getUsersByAgeRange" resultType="User">
          SELECT * FROM users WHERE age BETWEEN #{minAge} AND #{maxAge}
      </select>
      

4.5@Param标识的源码解析

为啥我们能将我们想要的 key 值,以及对应的 value 值设置进去呢?

(1)首先看这个方法的第三步的内部是如何调用的?

在这里插入图片描述

(2) mapper 映射的底层使用了代理模式,通过反射执行当前命令对应的方法

  • MapperProxy.java

在这里插入图片描述

在这里插入图片描述

(3)当前命令中,name 对应的是要执行的 sql 语句唯一标识:mapper 映射文件中 namespace + id),方法对应的是 select 方法,所以 switch - case 直接跳到 select 方法去执行对应的方法:

  • MapperMethod.java

在这里插入图片描述在这里插入图片描述

在这里插入图片描述

这边在 else 模块中,第一个方法是将 args 参数转换成 sql 要求的参数,点进这个方法看一下里面怎么写的?

在这里插入图片描述

再进入方法内部,这个方法是一个将注解设定的命名规则的名称设置成 map 容器的 key 值的方法

  • ParamNameResolver.java

在这里插入图片描述

第一步:name 是一个排序的 map 容器,其中 0 号位置放了我们通过注解命名的第一个参数名称1 号位置放置了我们通过注解命名的第二个参数名称:

在这里插入图片描述第二步:又因为我们有参数注解而且参数注解的个数不为1,所以跳到 else 中执行,新建一个 map 容器用来放置 (键值,参数值)

第三步遍历 names 这个 map 容器,取出其中第一个键值对

第四步:将它的 value 值即放置的自定义的 username 注解名称当作键值,将 args[当前键值对的键值] 即 args[0] 当作 value 值放进 params 这个 map 容器中

在这里插入图片描述

第五步定义一个字符串 param1,这里的 1 表示的是遍历第一个键值对即 i = 0;

第六步如果当前 names 这个 map 容器中没有包含当前的这个 param1 这个值(防止我们自己定义了,重复放置),那么我们就将(param1 ,args[0])这一个键值对放进 param 这个 map 容器中,也就是我们既可以用我们自己设定的 username 来访问对应的参数值,也可以用 param1 来访问对应的参数值。

第七步:i++,遍历 names 容器中的下一个键值对,重复执行 4 - 6;

第八步:最后将生成的 param 这个 map 容器返回,我们可以从中任意选择一个键值来访问对应的参数。

5.MyBatis的各种查询功能

5.1查询的数据只有一条

5.1.1接收情况

若查询的数据只有一条,那可以分为以下三种情况:

  1. 通过实体类对象接收
  2. 通过list集合接收(建议使用list集合接收
  3. 通过map集合接收

5.1.2举例

  • mapper 接口中方法的定义:

image-20240305091458001

  • mapper 映射文件中 sql 语句的编写:

    注意:通过map集合接收的sql语句的 resultType值 必须修改为map

image-20240305091816596

  • 测试类

image-20240305091824270

5.2查询的数据有多条

若查询的数据有多条,那可以分为以下三种情况:

  1. 通过实体类类型的list集合接收

  2. 通过map类型的list集合接收

  3. 通过注解@MapKey注解实现注入

    可以在mapper接口的方法上添加@MapKey注解,此时就可以将每条数据转换为map集合作为值,以某个字段的值作为键,放在同一个map集合中

  • mapper接口中方法的定义

image-20240305092035641

  • mapper 映射文件中 sql 语句的编写:

image-20240305092056982

  • 测试

image-20240305092312490

5.3查询单个数据(聚合函数)

  • mapper 接口中方法的定义:

image-20240305092355706

  • mapper 映射文件中 sql 语句的编写:

image-20240305092424526

  • 测试

image-20240305092501138

补充:MyBatis 中设置了默认的类型别名,可以在resultType使用别名

java.lang.Integer --> int,integer
int --> _int,_integer
Map --> map
String --> string

6.特殊SQL的执行

6.1模糊查询

模糊查询语句:

select * from tb_user where username like '%鹏%';

重点在如何解析出鹏字,由前面可知,${}本质为字符串替换、#{}本质为占位符(会进行预编译,若在数据类型检查中,检查为字符串类型,则会自动添加’‘引号)

mapper 映射文件中 sql 语句的编写,一共有三种方法可以实现:

  1. 使用${}
  2. 使用 concat#{} 进行字符串拼接(推荐)
  3. 使用"%#{}%"
  • mapper 接口中方法的定义:

image-20240305092616279

  • 映射文件:

image-20240305092943837

  1. 这里不可以使用 select * from t_user where username like '%#{username}%',即将 #{} 这种占位符赋值获取参数值的放在 单引号 里面;

  2. 这样直接将单引号里面的三个字符都当作字符串的一部分,而不会解析成占位符,看报错信息可以看出来,?直接放在单引号内部,当成一个==字符?==了,而不会被当成一个占位符

  3. 解析结果:

    image-20240305093102586

  • 测试

image-20240305095726057

6.2批量删除

  • mapper 接口中方法的定义:

image-20240305095817002

  • mapper 映射文件中 sql 语句的编写,这里只能用 ${} 这种字符串替换的方法来实现:

image-20240305095922782

  • 不能使用 #{}这种方式:#{} 这种占位符赋值的方式,会自动给参数加上单引号
delete from t_user where id in (#{ids});
# in (1,2,3,4)——>in('1,2,3,4'),结果必定报错
  • 测试

image-20240305100210905

6.3动态设置表名(会出现sql注入问题)

  • mapper 接口中方法的定义:

image-20240305100300248

  • mapper 映射文件中 sql 语句的编写

image-20240305100317945

  • 测试

image-20240305100344989

6.4添加功能获取自增的主键

  • mapper 接口中方法的定义:

image-20240305100738436

  • mapper 映射文件中 sql 语句的编写

image-20240305100801535

  • 测试

image-20240305100826298

7.自定义映射resultMap

7.1搭建mybatis框架

  • 数据库新建两张表t_emp和t_dept,并存入测试数据

    • t_emp表

      image-20240305100947447

    • t_dept表和数据

      image-20240305101049361

  • 新建两个类,其中emp_name对应的属性名为empName

    image-20240305101100400

7.2字段名和属性名不一致

解决字段名和属性名不一致的情况的三种方式

  1. 为字段名起别名
  2. 全局配置mapUnderscoreToCamelCase(驼峰映射)
  3. resultMap设置自定义映射关系(推荐)

7.2.1给字段名起别名

image-20240305101327430

7.2.2全局配置mapUnderscoreToCamelCase

  • mybatis-config.xml

image-20240305101342388

7.2.3resultMap(建议使用)

image-20240305101442659

7.3多对一(从员工到部门)映射关系

定义:多对一的映射关系指的是一个对象(或实体)在关联另一个对象时,该对象拥有多个对应关联对象的情况。具体来说,多对一关系表示多个对象共同指向同一个对象

举例:假设有两个实体类 DepartmentEmployee,一个部门可以有多名员工,而一名员工只能属于一个部门。在这种情况下,EmployeeDepartment 的关系就是多对一的映射关系,因为多名员工可以指向同一个部门

处理多对一的映射关系可以有以下几种方法:

  1. 通过 级联属性 赋值

  2. 通过 association标签

  3. 通过 分步查询

7.3.1通过级联属性(连接查询)赋值

  • 员工对象

image-20240305155201552

  • 部门对象

image-20240305155230176

  • mapper接口中方法定义

image-20240305155041400

  • mapper映射文件

image-20240305155116762

7.3.2通过association标签

  • mapper映射文件

image-20240305155704639

  • 测试

image-20240305155715928

7.3.3通过分步查询(推荐)

(1)通过id查询员工信息、并查出did
  • EmpMapper接口方法定义

image-20240305155729119

  • EmpMapper.xml映射文件

image-20240305155838408

解释:

  • association 标签中的 column 属性将两个查询到的信息关联了起来
  • select 属性中写的是 mapper 接口的全类名(唯一标识)
(2)通过did查询员工所对应的部门
  • DeptMapper接口方法定义

image-20240305160029531

  • DeptMapper.xml映射配置文件,注意字段名和属性名并不相同

image-20240305160047325

(3)测试

image-20240305160117304

7.3.4分步查询的好处:延迟加载

  1. 可以实现 延迟加载,但是必须在核心配置文件中设置全局配置信息(配置如下两个信息):没有全局设置的话,默认的是立即加载;

  2. lazyLoadingEnabled延迟加载的全局开关

    当开启时,所有关联对象都会延迟加载;

    需要 设置为true,默认为false。

  3. aggressiveLazyLoading设置为false,默认为false。当开启时,任何方法的调用都会加载该对象的所有属性 ,需要关闭他。关闭后每个属性会按需加载。

(1)开启延迟加载之前

image-20240305160614749

(2)开启全局延迟加载之后
  • mybatis-config.xml

image-20240305160630439

image-20240305160652120

(3)可以局部关闭延迟加载

条件:全局延迟加载开启

image-20240305160845187

7.4.一对多映射关系

  1. 定义:一对多关系表示一个对象指向多个相关联的对象
  2. 假设有两个实体类 DepartmentEmployee,一个部门可以有多名员工,而一名员工只能属于一个部门。在这种情况下,DepartmentEmployee 的关系就是一对多的映射关系,因为一个部门可以指向多名员工

7.4.1通过collection解决一对多映射关系

  • DeptMapper接口方法定义

image-20240305161128118

  • DeptMapper.xml映射配置文件

image-20240305161135178

  • 测试

image-20240305161209832

8.动态SQL

在 MyBatis 中,可以使用动态 SQL 来根据条件生成不同的 SQL 语句

  1. if:根据标签中test属性所对应的表达式决定标签中的内容是否需要拼接到 SQL 语句

  2. where

    • 当where标签中有内容时,会自动生成where关键字,并且将内容前多余的and或者or去掉

    • 当where标签中没有内容时,此时where标签没有任何效果

    • 注意:where标签不能将其中内容后面多余的and或者or去掉,只能将内容前多余的and或者or去掉

  3. trim

    • prefix|suffix:将trim标签中内容前面或者后面添加指定内容
    • prefixOverrides|suffixOverrides:将trim标签中内容前面或者后面去掉指定内容
    • 若标签中没有任何内容时,trim标签也没有任何效果
  4. choose、when、otherwise

    • 相当于if…else if…else if…otherwise
    • 其中when至少要有一个,otherwise最多只能有一个
  5. foreach

  • collection:设置需要循环的数组或者集合
  • item:表示数组或者集合中的每一个数据
  • separator:循环体之间的分隔符\
  • open:foreach标签所循环的所有内容的开始符
  • close:foreach标签所循环的所有内容的结束符

8.1if-where-trim标签

8.1.1if标签

image-20240305162857172

8.1.2where标签

image-20240305162915829

8.1.3trim标签

image-20240305163028054

image-20240305163038846

8.2choose-when-ohterwise标签

choose、when、otherwise,相当于if…else if…else

image-20240305163118041

8.3.foreach标签

8.3.1实现批量删除方式一

image-20240305163209567

8.3.2实现批量删除方式二

image-20240305163238958

8.3.3实现批量添加

image-20240305163251101

image-20240305163315943

8.4SQL标签

  • 设置SQL片段:

    <sql id="empColumns">eid,emp_name,age,sex,email</sql>
    
  • 引用SQL片段:

    <include refid="empColumns"></include>
    

image-20240305163350471

9.MyBatis缓存

9.1介绍

MyBatis 中的缓存主要分为一级缓存和二级缓存,它们的作用是提高数据库访问效率,减少不必要的数据库查询操作。

9.1.1一级缓存

  • 一级缓存是指在同一个 SqlSession 中执行相同的查询操作时,第二次及以后的查询会直接从缓存中获取结果,而不会再次查询数据库。
  • 一级缓存是默认开启的,且无法关闭。它的范围是 SqlSession 级别的,即在同一个 SqlSession 中共享缓存。
  • 一级缓存可以通过清除缓存的方式来刷新数据,比如调用 clearCache() 方法。

9.1.2二级缓存

  • 二级缓存是指多个 SqlSession 共享相同的缓存,可以跨 SqlSession 进行数据共享。
  • 使用二级缓存需要在映射文件中配置 <cache> 标签,并在 MyBatis 配置文件中开启二级缓存功能
  • 二级缓存的范围是 Mapper 级别的,即同一个 Mapper 接口下的多个 SqlSession 可以共享缓存。
  • 二级缓存对于经常被访问且不经常改变的数据非常有效,但在数据频繁更新的情况下,需要考虑缓存的有效性和数据一致性。

9.2一级缓存

9.2.1一级缓存生效(默认生效)

一级缓存是 SqlSession 级别 的,通过 同一个 SqlSession 查询 的数据会被缓存,下次查询相同的数据,就会从缓存中直接获取,不会从数据库重新访问。

  • mapper接口方法定义

image-20240305164155208

  • mapper接口映射文件

image-20240305164204852

  • 测试,同一个sqlSession是去缓存中取数据

image-20240305164226716

9.2.2一级缓存失效

使得一级缓存失效的四种情况

  • 不同的SqlSession对应不同的一级缓存
  • 同一个SqlSession但是查询条件不同
  • 同一个SqlSession两次查询期间执行了任何一次增删改操作,数据发生了变化
  • 同一个SqlSession两次查询期间手动清空了缓存

image-20240305164256605

9.3二级缓存

二级缓存是 SqlSessionFactory 级别,通过 同一个 SqlSessionFactory 创建的 SqlSession 查询的结果会被缓存;此后若再次执行相同的查询语句,结果就会从缓存中获取。

9.3.1二级缓存开启

(1)在核心配置文件中,设置全局配置属性 cacheEnabled=“true”,默认为 true,不需要设置;

(2)在映射文件中设置标签 <cache />

image-20240305164322284

(3)二级缓存必须在 SqlSession 关闭或提交之后有效;

(4)查询的数据所转换的实体类类型必须实现序列化的接口。

image-20240305164351626

  • 测试

image-20240305164420343

9.3.2二级缓存失效

失效情况:两次查询之间执行了任意的增删改,会使一级和二级缓存同时失效

9.3.3二级缓存的相关配置

image-20240305164510303

9.4缓存查询的顺序

  1. 先查询二级缓存,因为二级缓存中可能会有其他程序已经查出来的数据,可以拿来直接使用
  2. 如果二级缓存没有命中,再查询一级缓存
  3. 如果一级缓存也没有命中,则查询数据库
  4. SqlSession关闭之后,一级缓存中的数据会写入二级缓存

10_MyBaits的逆向工程

10.1创建逆向工程的步骤

(1)添加依赖和插件,这里的 mybatis 和 JDBC 换成你自己对应的版本:

<?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>

    <groupId>com.peng.mybatis</groupId>
    <artifactId>MyBatis_demo04</artifactId>
    <version>1.0-SNAPSHOT</version>

    <packaging>jar</packaging>

    <!-- 依赖MyBatis核心包 -->
    <dependencies>
        <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.4</version>
        </dependency>

        <!-- MySQL驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>

        <!-- log4j日志 -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>


        <dependency>
            <groupId>org.apache.ibatis</groupId>
            <artifactId>ibatis-core</artifactId>
            <version>3.0</version>
        </dependency>

        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.2.0</version>
        </dependency>

    </dependencies>

    <!-- 控制Maven在构建过程中相关配置 -->
    <build>
        <!-- 构建过程中用到的插件 -->
        <plugins>
            <!-- 具体插件,逆向工程的操作是以构建过程中插件形式出现的 -->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.0</version>
                <!-- 插件的依赖 -->
                <dependencies>
                    <!-- 逆向工程的核心依赖 -->
                    <dependency>
                        <groupId>org.mybatis.generator</groupId>
                        <artifactId>mybatis-generator-core</artifactId>
                        <version>1.3.2</version>
                    </dependency>
                    <!-- 数据库连接池 -->
                    <dependency>
                        <groupId>com.mchange</groupId>
                        <artifactId>c3p0</artifactId>
                        <version>0.9.2</version>
                    </dependency>
                    <!-- MySQL驱动 -->
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>8.0.29</version>
                    </dependency>

                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>


(2)创建 MyBatis 的核心配置文件 mybatis-config.xml

(3)创建逆向工程的配置文件,文件名必须是:generatorConfig.xml(修改数据库名称和密码、修改包名、修改对应的数据库表名)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
    <!--
        targetRuntime: 执行生成的逆向工程的版本
        MyBatis3Simple: 生成基本的CRUD(清新简洁版),只有增删改查(查所有和根据 Id 查)5个方法
        MyBatis3: 生成带条件的CRUD(奢华尊享版)
    -->
    <context id="DB2Tables" targetRuntime="MyBatis3">
        <!-- 数据库的连接信息 -->
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/mybatis"
                        userId="root"
                        password="123456">
        </jdbcConnection>
        <!-- javaBean的生成策略-->
        <javaModelGenerator targetPackage="com.peng.mybatis.pojo"
                            targetProject=".\src\main\java">
            <!--true生成的是子包,false生成的是一个文件夹名-->
            <property name="enableSubPackages" value="true"/>
            <!-- 去掉字符串前后的空格-->
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>
        <!-- SQL映射文件的生成策略 -->
        <sqlMapGenerator targetPackage="com.peng.mybatis.mapper"
                         targetProject=".\src\main\resources">
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>
        <!-- Mapper接口的生成策略 -->
        <javaClientGenerator type="XMLMAPPER"
                             targetPackage="com.peng.mybatis.mapper" targetProject=".\src\main\java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>
        <!-- 逆向分析的表 -->
        <!-- tableName设置为*号,可以对应所有表,此时不写domainObjectName -->
        <!-- domainObjectName属性指定生成出来的实体类的类名,会自动在后面补上Mapper -->
        <table tableName="t_emp" domainObjectName="Emp"/>
        <table tableName="t_dept" domainObjectName="Dept"/>
    </context>
</generatorConfiguration>
  • MyBatis3Simple: 生成基本的CRUD(清新简洁版),只有增删改查(查所有和根据 Id 查)5个方法
  • MyBatis3: 生成带条件的CRUD(奢华尊享版)

(5)执行MBG插件的generate目标

在这里插入图片描述

10.2豪华尊享版两种函数的区别

在这里插入图片描述

10.3QBC查询

image-20240305164823124

  • 报了错误Error:(6, 37) java: 程序包org.apache.ibatis.annotations不存在,网上找不到解决方法,怀疑是配置文件里面版本问题

10.4QBC修改

image-20240305164840117

  • and 条件都是链式添加的;
  • or 条件需要换行,调用 or() 方法之后再调用 and() 方法。

11_MyBatis分页

11.1使用步骤

  • 核心配置文件中配置分页插件
<plugins>
	<!--设置分页插件-->
	<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
  • 在pom.xml中添加依赖
<dependency>
	<groupId>com.github.pagehelper</groupId>
	<artifactId>pagehelper</artifactId>
	<version>5.2.0</version>
</dependency>

11.2使用

public class PageHelperTest {

    /**
     * limit index,pageSize
     * index:当前页的起始索引
     * pageSize:每页显示的条数
     * pageNum:当前页的页码
     *
     * index=(pageNum-1)*pageSize
     *
     * 使用MyBaits的分页插件实现分页功能
     * 1.需要在查询功能之前开启分页,2代表了当前页码,4代表当前页显示的数据
     * PageHelper.startPage(2,4);
     * 2.在查询功能之后获取分页相关信息
     * PageInfo<Emp> page=new PageInfo<>(list,5);
     * list代表分页数据
     * 5代表当前导航分页的数量
     */
    @Test
    public void testPageHelper(){
        try {
            InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
            SqlSession sqlSession = sqlSessionFactory.openSession(true);
            EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);

            //开启分页功能

            //查询详细分页信息方式一:
//            Page<Object> page = PageHelper.startPage(2, 4);
//            System.out.println(page);

            PageHelper.startPage(2,4);

            //查询所有数据
            List<Emp> list = mapper.selectByExample(null);
            list.forEach(emp-> System.out.println(emp));

            //查询详细分页信息方式二,其中5代表导航数
            PageInfo<Emp> page=new PageInfo<>(list,5);
            System.out.println(page);


        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}
  • Page<Object> page = PageHelper.startPage(2, 4); ,这个函数返回的 page 是包含少量信息的 page数据;
  • PageInfo<Emp> page = new PageInfo<>(list, 5);,展示非常详细的 pageInfo 信息,里面包含 page 信息。

在这里插入图片描述

常用数据:

  • pageNum:当前页的页码
  • pageSize:每页显示的条数
  • size:当前页显示的真实条数(有可能每页显示 5 条,但是最后一页只剩 2 条数据这样子)
  • total:总记录数
  • pages:总页数
  • prePage:上一页的页码
  • nextPage:下一页的页码
  • isFirstPage/isLastPage:是否为第一页/最后一页
  • hasPreviousPage/hasNextPage:是否存在上一页/下一页
  • navigatePages:导航分页的页码数
  • navigatepageNums:导航分页的页码,[1,2,3,4,5]

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

flutterpageview动画,小程序FMP优化实录

是否能进一步优化自己的代码 1.保存在内存中的图片&#xff0c;是否做过压缩处理再保存在内存里否则可能由于图片质量太高&#xff0c;导致 OOM 2.Intent 传递的数据太大&#xff0c;会导致页面跳转过慢。太大的数据可以通过持久化的形式传递&#xff0c;例如读写文件 3.频繁…

could not publish server configuration for tomcat at localhost

1&#xff0c;报错信息如图&#xff1a; 2&#xff0c;找到servers双击&#xff0c;选择Modules&#xff0c;如果有两个webModules ,remove一个&#xff0c; 3&#xff0c;如果重启还是报错&#xff0c;干脆两个都remove&#xff0c;双击tomcat服务add And Remove重新添加

【论文翻译】结构化状态空间模型

文章目录 3.2 对角结构化状态空间模型3.2.1 S4D:对角SSM算法3.2.2 完整应用实例 3.3 对角化加低秩&#xff08;DPLR&#xff09;参数化3.3.1 DPLR 状态空间核算法3.3.2 S4-DPLR 算法和计算复杂度3.3.3赫尔维兹&#xff08;稳定&#xff09;DPLR形式 这篇文章是Mamba作者博士论文…

Blender和3ds Max哪个会是行业未来?

Blender和3ds Max都是很强大的三维建模和渲染软件&#xff0c;各有各的好处。选择哪个软件更好&#xff0c;要看你的需求、预算、技术水平以及行业趋势等因素。 Blender最大的优点是免费且开源&#xff0c;这对预算有限的个人和小团队来说很有吸引力。它有很多建模工具和功能&…

MyBatis介绍

MyBatis是一个优秀的持久层框架&#xff08;就是将某些数据持久化到硬盘或其他存储器中的框架&#xff09;&#xff0c;它把jdbc对数据库的操作进行了封装&#xff0c;使用户只需关注sql本身&#xff0c;不需要去执行jdbc的那一套复杂的操作。 MyBatis通过配置xml文件或注解的方…

YOLOv9独家改进|动态蛇形卷积Dynamic Snake Convolution与RepNCSPELAN4融合

专栏介绍&#xff1a;YOLOv9改进系列 | 包含深度学习最新创新&#xff0c;主力高效涨点&#xff01;&#xff01;&#xff01; 一、改进点介绍 Dynamic Snake Convolution是一种针对细长微弱的局部结构特征与复杂多变的全局形态特征设计的卷积模块。 RepNCSPELAN4是YOLOv9中的特…

智慧城市的新引擎:物联网技术引领城市创新与发展

目录 一、引言 二、物联网技术与智慧城市的融合 三、物联网技术在智慧城市中的应用 1、智慧交通管理 2、智慧能源管理 3、智慧环保管理 4、智慧公共服务 四、物联网技术引领城市创新与发展的价值 五、挑战与前景 六、结论 一、引言 随着科技的日新月异&#xff0c;物…

图像处理 mask掩膜

1&#xff0c;图像算术运算 图像的算术运算有很多种&#xff0c;比如两幅图像可以相加&#xff0c;相减&#xff0c;相乘&#xff0c;相除&#xff0c;位运算&#xff0c;平方根&#xff0c;对数&#xff0c;绝对值等&#xff1b;图像也可以放大&#xff0c;缩小&#xff0c;旋…

uni-app头像编辑上传

实现比较简单&#xff0c;文档中都有描述&#xff0c;就是第一次做可能会有疏漏&#xff0c;记录一下&#xff1a; <view class"edict-item" click"selectPic"><text class"item-name" :style"$em.$getThemeStyle([avatarConText…

GIT使用学习笔记 远程仓库篇

git clone xxxxx 将远程 你可能注意到的第一个事就是在我们的本地仓库多了一个名为 o/main 的分支, 这种类型的分支就叫远程分支。由于远程分支的特性导致其拥有一些特殊属性。 远程分支反映了远程仓库(在你上次和它通信时)的状态。这会有助于你理解本地的工作与公共工作的差…

ssm核心面试题汇总

文章目录 1. Spring1.1 Spring Beans1.谈谈你对Spring的理解以及优缺点2. 什么是Spring beans3. 配置注册Bean有哪几种方式4. Spring支持的几种bean的作用域5. 单例bean的优势6. 单例bean是线程安全的吗&#xff1f;如何优化为线程安全7. 谈一谈spring bean的自动装配8. Spring…

如何在jupyter notebook 中下载第三方库

在anconda 中找到&#xff1a; Anaconda Prompt 进入页面后的样式&#xff1a; 在黑色框中输入&#xff1a; 下载第三方库的命令 第三方库&#xff1a; 三种输入方式 标准保证正确 pip instsall 包名 -i 镜像源地址 pip install pip 是 Python 包管理工具&#xff0c;…

在排序数组中查找元素的第一个和最后一个位置[中等]

优质博文IT-BLOG-CN 一、题目 给你一个按照非递减顺序排列的整数数组nums&#xff0c;和一个目标值target。请你找出给定目标值在数组中的开始位置和结束位置。 如果数组中不存在目标值target&#xff0c;返回[-1, -1]。 你必须设计并实现时间复杂度为O(log n)的算法解决此问…

Cookie 探秘:了解 Web 浏览器中的小甜饼

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

华为智慧教室3.0的晨光,点亮教育智能化变革

“教室外有更大的世界&#xff0c;但世界上没有比教室更伟大的地方。” 我们在求学阶段&#xff0c;都听说过这句话&#xff0c;但往往是在走出校园之后&#xff0c;才真正理解了这句话。为了让走出校园的孩子能够有能力&#xff0c;有勇气探索广阔的世界。我们应该准备最好的教…

【Leetcode】1588.所有奇数长度子数组的和

题目描述 思路 题目要求我们求解所有奇数长度数组的和。若暴力循环求解&#xff0c;时间复杂度过高。所以&#xff0c;我们可以采用前缀和优化。 如上图输入arr数组&#xff0c;sum[i]用于计算arr数组中前i个数的和。(在程序中&#xff0c;先给sum[0]赋值&#xff0c;等于arr[0…

平台总线式驱动开发

一、总线、设备、驱动 硬编码式的驱动开发带来的问题&#xff1a; 垃圾代码太多 结构不清晰 一些统一设备功能难以支持 开发效率低下 1.1 初期解决思路&#xff1a;设备和驱动分离 struct device来表示一个具体设备&#xff0c;主要提供具体设备相关的资源&#xff08;如…

Java项目:37 springboot003图书个性化推荐系统的设计与实现

作者主页&#xff1a;源码空间codegym 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 项目介绍 springboot003图书个性化推荐系统的设计与实现 管理员&#xff1a;首页、个人中心、学生管理、图书分类管理、图书信息管理、图书预约管理、退…

阿里二面,redis宕机了,如何快速恢复数据

背景 有个同学阿里二面&#xff0c;面试官问&#xff1a;redis宕机了&#xff0c;如何恢复数据&#xff1f; 这位同学当时一脸懵&#xff0c;不知道如何回答。 分析分析这个问题&#xff0c;redis宕机&#xff0c;要想恢复数据&#xff0c;首先redis的数据有没有做持久化&…

基于Java springboot+VUE+redis实现的前后端分类版网上商城项目

基于Java springbootVUEredis实现的前后端分类版网上商城项目 博主介绍&#xff1a;多年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 央顺技术团队 Java毕设项目精品实战案例《1000套》 欢迎点赞 收藏 ⭐留言…