MyBatis

MyBatis概述

框架

  1. 在文献中看到framework被翻译为框架。
  2. Java常用框架:
    1. SSM三大框架:Spring + SpringMVC + MyBatis
    2. SpringBoot
    3. SpringCloud
    4. 等…
  3. 框架其实就是对通用代码的封装,提前写好了一堆类和接口,我们在做项目的时候可以直接引入这些类和接口(引入框架),基于这些现有的接口和类进行开发,可以大大提高开发效率。
  4. 框架一般都以jar包的形式存在。(jar包中有class文件以及各种配置文件等。)

三层架构

  1. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sUcVuna7-1685102541180)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230422212456271.png)]
  2. 表现层:直接根前端打交互(一是接收前端AJAX请求,二是返回json数据给前端)
  3. 业务逻辑层:一是处理表现层转发过来的前端请求(也就是具体业务),二是将从持久层获取的数据返回到表现层。
  4. 数据访问层:DAO对象直接数据库完成CRUD,并将获得的数据返回到上一层(也就是业务逻辑层)。
  5. Java的持久层框架:
    1. MyBatis
    2. Hibernate

JDBC的不足

  1. SQL语句直接写死在Java代码中,不灵活。该SQL的话就要改Java代码。违背了OCP。
  2. 给占位符“?”传值是繁琐的。能不能自动化?能,使用MyBatis
  3. 结果集封装成Java对象是比较繁琐的,能不能自动化?能,使用MyBatis

了解MyBatis

  1. MyBatis本质上就是对JDBC的封装,通过MyBatis完成CRUD。
  2. MyBatis在三层架构中是负责持久层的,属于持久层框架。
  3. ORM:对象关系映射
    1. O(Object):JVM中的Java对象
    2. R(Relational):关系型数据库
    3. M(Mapping):将JVM中的Java对象映射到数据库表中的一条记录,或者是将数据库表中的一行记录映射成JVM中的一个Java对象。【映射可以理解为转换,也可以理解为对应关系】
  4. MyBatis框架是为Java专门准备的一个SQL映射框架。
    1. MyBatis是一个基于DAO层的ORM框架(ORM:Object Relational Mapping 对象关系映射 在mybatis中指的是sql语句与实体对象之间的映射)
  5. ORM是一种双向的数据交换技术,它不仅可以将对象中的数据存储到数据库中,也可以反过来将数据库中的数据提取到对象中。
  6. ORM图示:
    1. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WJDuue2W-1685102541181)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230509205228366.png)]
    2. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-80ZPv9KN-1685102541182)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230509205301748.png)]
  7. MyBatis框架实现了ORM功能。【MyBatis实现了对象和数据之间相互转换的功能】。【ORM就是对象和数据之间的相互转换,MyBatis就是做ORM的一个框架】
  8. ORM是一种思想:主要内容是面向对象的编程语言和关系型数据库之间应该存在映射关系,简单地说就是对象和数据应该是可以相互转换,这就是ORM思想。【这种思想可以支持两个不同系统之间的相互通信】
    • MyBatis实现了这种思想。
    • MyBatis能干啥?
      • Java对象<----->数据库表中的一条记录,相互转换,相互映射。
  9. MyBatis是一个半自动化的ORM,因为MyBatis框架中的SQL语句需要程序员自己编写。
  10. Hibernate框架是全自动化ORM框架,使用Hibernate框架的时候,不需要程序员手动编写SQL语句,SQL语句自动生成。
  11. 对象-关系映射(ORM)系统一般以中间件的形式存在,主要实现程序对象到关系数据库数据的映射。
    1. MyBatis就是别人写好的一种对象关系映射系统。我们可以直接拿来用,方便我们完成对象和数据之间的相互转换。
  12. MyBatis框架的特点
    1. 支持定制化SQL、存储过程、基本映射以及高级映射。
    2. 避免了几乎所有的JDBC代码中手动配置参数以及获取结果集封装成对象。
      1. 手动配置参数就是给"?"传值。
    3. 支持XML开发,也支持注解式开发。【为了保证sql语句的灵活,所以mybatis大部分是采用XML方式开发。】
    4. 将接口和 Java 的 POJOs映射成数据库中的记录。
    5. 体积小好学:两个jar包,两个XML配置文件。
    6. 完全做到sql解耦合。
    7. 提供了基本映射标签。
    8. 提供了高级映射标签。
    9. 提供了XML标签,支持动态SQL的编写。

MyBatis入门程序

  1. 第一步:设置打包方式为:jar包。(因为MyBatis封装的是JDBC,JDBC不需要打成war包,我们在学习JDBC的时候就不需要部署到web服务器中,所以直接打包成jar包就可以)

    <packaging>jar</packaging>
    
  2. 第二步:引入依赖(MyBatis依赖 + MySQL驱动依赖)

    <dependencies>
        <!--mybatis依赖-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.3.0</version>
        </dependency>
        <!--mysql驱动依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.20</version>
        </dependency>
    </dependencies>
    
  3. 第三步:在resources根目录下新建MyBatis的核心配置文件,核心配置文件大家都起名叫做“mybatis-config.xml”(可以参考mybatis手册:https://mybatis.org/mybatis-3/zh/index.html), mybatis-config.xml文件的内容如下:[从xml中构建SqlSessionFactory,SqlSessionFactory的构建需要这个xml文件,这个xml文件就是MyBatis的核心配置文件]

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "https://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
        <environments default="development">
            <environment id="development">
                <transactionManager type="JDBC"/>
                <dataSource type="POOLED">
                    <!--这里是配置数据库的参数-->
                    <property name="driver" value="com.mysql.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql://localhost:3306/powernode"/>
                    <property name="username" value="root"/>
                    <property name="password" value="123456"/>
                </dataSource>
            </environment>
        </environments>
        <mappers>
            <!--这里是SQL映射文件的路径-->
            <!--resource属性:是从类的根路径下开始查找资源-->
            <!--url属性:从绝对路径中加载资源,不建议,因为移植性差
    		语法格式:file:///绝对路径-->
            <mapper resource="CarMapping.xml"/>
        </mappers>
    </configuration>
    
    1. MyBatis的核心配置文件的文件名不一定是mybatis-config.xml,可以是其他的。

    2. 解决数据库乱码问题,可以在url后面拼接上: ?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false

    3. SqlSessionFactory是通过核心配置xml文件构建的,一个数据库对应一个SqlSessionFactory对象,如果需要使用多个数据库,就需要使用多个environment标签,在获取SqlSessionFactory对象的时候,需要指定environment标签的id属性值,来构建属于哪个数据库的SqlSessionFactory对象。

    4. MyBatis核心配置文件的存放的位置可以是随意的。

      • 如果写道其他位置再获取SqlSessionFactory对象的时候需要传入一个MyBatis的核心配置文件的输入流对象,在获取输入流的时候只需要将路径指向其他位置即可,但是为了移植性更好,一般没有这么做的,我们只需要知道能这么用即可。

      • 如果写在类的根路径下我们再获取核心配置文件的输入流的时候就更加容易了,可以直接通过:

        // Resources.getResourceAsStream方法就刚好是从类的根路径下开始查找的,所以我们一般都写再类根路径下
        Resources.getResourceAsStream("mybatis-config.xml");
        
  4. 第四步:在resources根目录下新建SQL映射文件,在这里以CarMapping.xml为例(可以参考MyBatis手册【从参考手册拷贝下来的时候https改成http这样SQL语句就不是纯灰色了】),CarMapping.xml文件的内容如下所示:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="asdbbdsa">
        <!--id是这条Sql语句的身份证号,insertCar就代表了这条SQL语句.在同一个命名空间下id是唯一的-->
        <insert id="insertCar">
            insert into t_car(car_num,brand,guide_price,produce_time,car_type)
            values('1003','雅阁',30.0,'2020-10-11','燃油车')
        </insert>
    </mapper>
    
    1. SQL语句最后的分号可以写,也可以省略“;”。
    2. CarMapping.xml的文件名不是固定的,可以随意起名【规范一般都是:表名+Mapping.xml】。
    3. 一般都是一张表一个SQL映射文件。
    4. SQL映射文件的位置也是随意的。
      1. 如果要是放在其他位置,那么就需要在MyBatis核心配置文件配置“mapper”标签的时候使用url属性,从绝对路径中加载资源,不建议这么用,因为移植性差.
      2. 如果放在类的根路径下,MyBatis的核心配置文件在使用mapper查找SQL映射文件的位置的时候,直接使用resource这个属性,默认就是从类的根路径下开始查找,移植性强.
    5. 将SQL映射文件需要配置到MyBatis的核心配置文件的meppers标签中.
  5. 编写MyBatis程序。(使用mybatis的类库,编写mybatis程序,连接数据库,做增删改查就行了。)

    package com.powernode;
    
    import org.apache.ibatis.io.Resources;
    import org.apache.ibatis.session.SqlSession;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.apache.ibatis.session.SqlSessionFactoryBuilder;
    
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    
    public class Main2 {
        public static void main(String[] args) {
            SqlSession sqlSession = null;
            try {
                // 1.创建SqlSessionFactoryBuilder对象
                SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
                // 获取MyBatis核心配置文件的输入流以下这四种方式都可以
                InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
                //InputStream is = ClassLoader.getSystemResourceAsStream("mybatis-config.xml");
                //InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml");
                //InputStream is = new FileInputStream("D:\\JavaCode\\mybatis\\mubatis-001-introduction\\src\\main\\resources\\mybatis-config.xml");
                // 创建SqlSessionFactory对象,一般是一个数据库一个SqlSessionFactory对象
                SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
                // 创建SqlSession对象,专门用来执行sql语句的对象
                sqlSession = sqlSessionFactory.openSession();
                // 返回值是影响的数据库表中的记录的条数
                int count = sqlSession.insert("insertCar");
                System.out.println("成功影响了:" + count + "条数据!");
                // MyBatis获取的SqlSession对象不支持自动提交,需要手动提交以下才能持久化保存到数据库中
                sqlSession.commit();
            } catch (IOException e) {
                /*遇到异常回滚事务*/
                if (sqlSession != null) {
                    sqlSession.rollback();
                }
                e.printStackTrace();
            } finally {
                if (sqlSession != null) {
                    /*关闭资源*/
                    sqlSession.close();
                }
            }
        }
    }
    
    1. 在MyBatis当中,负责执行SQL语句的那个对象叫做什么呢?
      1. SqlSession是专门用来执行SQL语句的,是JVM和DBMS之间的一次会话.【参考Servlet中的HttpSession会话机制学习】一次会话可能执行很多次Sql语句。SqlSession是会话对象。
      2. 想要获取SqlSession对象首先得获取SqlSessionFactory对象,通过SqlSessionFactory工厂来生产SqlSession对象.
      3. 怎么获取SqlSessionFactory对象呢?
        • 需要首先获取SqlSessionFactoryBuilder对象。通过SqlSessionFactoryBuilder对象的build方法,来获取一个SqlSessionFactory对象。
        • build()的时候传入核心配置文件的输入流.
      4. MyBatis的核心对象包括:
        1. SqlSessionFactoryBuilder
        2. SqlSessionFactory (一个数据库对应一个)
        3. SqlSession
      5. SqlSessionFactoryBuilder --> SqlSessionFactory --> SqlSession
  6. MyBatis中有两个主要的配置文件:

    1. 其一:MyBatis的核心配置文件:mybatis-config.xml,主要配置连接数据库等信息
    2. 第二:XxxxMapping.xml,这个文件时专门用来编写SQL语句的配置文件(一般一个表一个)
      1. t_user表,一般会对应一个UserMapper.xml
      2. t_student表,一般会对应一个StudentMapper.xml
  7. 关于第一个程序的小细节

    1. Resources.getResourceAsStream
      • 小技巧:以后凡是遇到resource这个单词,大部分情况下,这种加载资源的方式就是从类的根路径下开始加载(开始查找).
      • 优点:采用这种方式,从类路径下加载资源,项目的可移植性很强.项目从windows移植到linux,代码不需要修改,因为这个资源文件一直都在类路径当中。
    2. InputStream is = new FileInputStream(“d:\mybatis-config.xml”);
      • 采用这种方式也可以。
      • 缺点:可移植性太差,程序不够健壮。可能会移植到其他的操作系统当中。导致以上路径无效,还需要修改java代码中的路径。这样违背了OCP原则。
    3. InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream(“mybatis-config.xml”);
      • ClassLoader.getSystemClassLoader() 获取系统的类加载器。类加载器的种类有很多,这个是获取系统类加载器.系统类加载器有一个方法叫做:getResourceAsStream它就是从类路径当中加载资源的。
      • 通过源代码分析发现:
        InputStream is = Resources.getResourceAsStream(“mybatis-config.xml”);
        底层的源代码其实就是:
        InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream(“mybatis-config.xml”);
    4. XxxMapping.xml文件的名字是固定的吗?CarMapper.xml文件的路径是固定的吗?
      1. 都不是固定的。
        1. resource属性:这种方式是从类路径当中加载资源。
        2. url属性:这种方式是从绝对路径当中加载资源。

补充内容

  1. 在Maven规范目录中:

    • src 
          main [主程序]
          	java [类的根目录]
          	resources [类的根目录]
          test [测试程序]
              java [类的根目录]
      	    resources [类的根目录]
      
    • resources目录

      • 放在这个目录当中的,一般都是资源文件,配置文件。直接放在resources下的资源等同于放到了类的根路径下。
      • 一个模块只能有一个类的根路径。
    • 看似main和test下的java没有在同一个文件下下,但是都是类的根目录,都是相通的。

      • 虽然都是相通的但是可以在这两个根目录文件夹下定义同一个类。使用时遵循就近原则。
  2. 使用lambda(拉姆达)表达式遍历集合:

    // 拉姆达表达式只能遍历集合,不能遍历数组
    List<String> list = new ArrayList<>();
    list.add("123");
    list.add("456");
    list.add("789");
    // l1:表示集合中的一项内容,这个只是一个标识符,自定义的
    list.forEach(l1-> {System.out.println(l1);});/*....如果有多个Java语句,这里使用大括号括起来,如果只有一个Java语句大括号可以省略*/
    Map<String,Object> map = new HashMap<>();
    map.put("111","111");
    map.put("222","222");
    // Map集合中有两个参数:多个参数之间使用()括起来,一个参数可以省略小括号
    // l1和l2分别代表集合的key和value
    map.forEach((l1,l2) -> System.out.println(l1 + "=" + l2));
    

关于MyBatis的事务管理机制

  1. 在MyBatis的核心配置文件中,可以通过以下的配置进行MyBatis的事务管理

    <transactionManager type="JDBC"></transactionManager>
    <!--
    	type属性有两个值:
    		JDBC(jdbc):JDBC事务管理器
    		MANAGED(managed):MANAGED事务管理器
    		type后面的值,只有以上两个值可选,不区分大小写。
    -->
    
  2. JDBC事务管理器

    1. MyBatis框架自己管理事务,自己采用原生的JDBC代码去管理事务:

      conn.setAutoCommit(false);//关闭SQL语句自动提交机制,开启事务。
      ......//业务逻辑
      conn.commit();//提交事务
      
    2. 我们编写的mybatis与原生jdbc代码的对应关系

      try {
          SqlSession sqlSession = sqlSessionFactory.openSession();      //相当于:conn.setAutoCommit(false) ,关闭自动提交机制,开启事务
          int count = sqlSession.Xxx("idName");//...执行DML语句
          // ....可能有很多条DML语句
          sqlSession.commit();//提交事务,相当于conn.commit()
      } catch(Exception e) {
          // 发生异常,回滚事务
          sqlSession.rollback();//发生异常回滚事务
          e.printStackTrace();//打印堆栈异常信息
      }
      
    3. 上述《2》中使用的是SqlSessionFactory对象的openSession()无参数重载方法获取的SqlSession对象,

      SqlSession sqlSession = sqlSessionFactory.openSession(); 
      /*使用openSession的无参数重载方法底层调用的是:*/
      public SqlSession openSession() {
          return 		    this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false); // 这里传过去的是fasle,相当于conn.setAutoCommit(false),开启事务。
      }
      

      常用的重载方法还有openSession(boolean autoCommit)

      SqlSession sqlSession = sqlSessionFactory.openSession(true);
      /*使用openSession的这个重载方法底层调用的是:*/
      public SqlSession openSession(boolean autoCommit) {
              return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, autoCommit); 
      	//调用openSession的时候
      	//1.如果传过来的是true这里的autoCommit就是true,相当于conn.setAutoCommit(true),关闭事务。只要执行任意一条DML语句就提交一次。
          //2.如果传过来的是false这里的autoCommit就是false,相当于conn.setAutoCommit(false),开启事务。这样就跟调用openSession的无参数构造方法是一个效果了。
      }
      
    4. JDBC默认情况下是没有事务的,只要不执行conn.setAutoCommit(false),那么就没有关闭自动提交机制,每执行一条DML语句就提交一次。【JDBC默认是自动提交的】

    5. MANAGED事务管理器:

      1. mybatis不在负责事务管理了,事务管理交给其他容器来负责。例如:spring。
      2. 对于我们当前的单纯的只有mybatis的情况下,如果配置为:MANAGED。那么事务这块是没人管的。没有人管理事务表示事务压根没有开启。和autoCommit是true是一个效果,每执行一条DML语句就提交一次。

引入Junit

  1. JUnit是一个开放源代码的Java测试框架,用于编写和运行可重复的测试。他是用于单元测试框架体系xUnit的一个实例(用于java语言)。

  2. 测试过程中设计到两个概念:期望值和实际值相同则表示测试通过,期望值和实际值不同则单元测试执行时会报错。

    1. 期望值:我们期望的程序运行结果,程序运行之后的结果应该是什么。
    2. 实际值:程序实际运行的结果。
  3. 实现步骤:

    1. 第一步:在Maven工程中引入junit依赖

      <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.13.2</version>
          <scope>test</scope> <!--因为junit是用于测试的,所以junit依赖的作用域一般都定义为test,只能在test文件夹下使用-->
      </dependency>
      
    2. 第二步:编写单元测试类【测试用例】,测试用例中每一个测试方法上都是用@Test注解进行标注。

      1. 测试类【测试用例】的名字:XxxTest,测试用例和被测试的类一般都在相同的包名下,只不过一个是在“main/java”下,一个是在“test/java”下。【一般是一个业务类对应一个测试用例,业务类中每一个方法都对应一个实际的业务,一个实际的业务对应一个测试方法】

      2. 测试用例中可能有多个测试方法,一般是被测试的类中一个业务对应一个测试方法。测试方法的方法头必须这么写

        public void testXxx() {}
        // 访问权限修饰符必须是public修饰,不是public的抛出异常
        // 不能是静态的方法,方法头带有static运行测试时抛出异常
        // 方法的返回值类型必须是void修饰的
        // 方法名可以随意但是规范命名一般是“test+被测试的方法的方法名”
        // 方法体中一般实例化一个被测试的业务类,调用业务类的方法,使用Assert.assertEquals(expected,actual)方法进行判断,expected是期望值,actual是实际值。
        
    3. 第三步:可以在测试类上执行也可以在测试方法上执行:

      1. 在类上执行时:该类中所有的测试方法都会执行。
      2. 在方法上执行时,只执行当前测试的方法。
  4. 如果一个配置文件“main/resources”下面也有,“test/resources”中也有:遵循就近原则:

    1. 如果在main中写的测试类,那么就去找“main/resources”下面的配置文件。
    2. 如果在test中写的测试类,那么就去找“test/resources”下面的配置文件。
  5. 那么test中编写的测试类可以使用“main/resources”里面的资源。

    main中的测试类不能使用“test/resources”里面的资源。

MyBatis集成的日志组件

  1. 在MyBits核心配置文件中有个settings标签:

    1. 这是MyBatis非常重要的调整设置,他会改变MyBatis的运行时行为。其中有一项设置名是logImpl,用来指定MyBatis所用日志的具体实现,未指定的时候自动查找(从我们引入的依赖中自动查找有那些日志框架)

      <settings>
          <setting name="logImpl" value="STDOUT_LOGGING"/>
          <!--指定 MyBatis 所用日志的具体实现,未指定时将自动查找。-->
      </settings>
      
  2. MyBatis常见的集成的日志组件有哪些?

    1. SLF4J(沙拉风):沙拉风是一个日志标准,其中有一个框架叫做LogBack,他实现了沙拉风规范。
    2. LOG4J
    3. LOG4J2
    4. STDOUT_LOGGING
  3. 其中STDOUT_LOGGING是标准日志,MyBatis已经实现了这种标准日志,mybatis框架本身已经实现了这种标准。只要开启即可。怎么开启呢?在mybatis-config.xml文件中使用settings标签进行配置开启。

    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
        <!--指定 MyBatis 所用日志的具体实现,未指定时将自动查找。使用的是“STDOUT_LOGGING”才需要配置这句话,如果使用的是第三方的日志框架,不用配置这句话,MyBatis自动查找我们引入的第三方日志框架-->
    </settings>
    
    1. 标准日志也可以用,但是配置不够灵活,可以集成其他的日志组件,例如:log4j,logback等。
  4. 为什么使用的是STDOUT_LOGGING标准日志才需要配置setting,使用的是第三方的日志框架MyBatis就可以自动查找呐?

    1. 因为MyBatis自己就实现了STDOUT_LOGGING这种标准日志,你用不用是你说了算,只要你在核心配置文件中配置了setting这样的属性就代表你要开启日志框架的使用,没有配置就代表你不想使用日志框架,因为有些人不想使用日志框架,就不需要开启MyBatis的日志框架,即使MyBatis查找到了自己有中日志框架,也不能直接强制性的给你用上。
    2. 一但你引入了第三方的日志框架,MyBatis可一旦查找到了你引入的日志框架,MyBatis就认为你要开启并使用日志框架,不然你也不会费那个事引入框架的,所以在Mybatis核心配置文件中配置不配置就没有那么重要了,但是使用的是“STDOUT_LOGGING”日志框架一定要在MyBatis核心配置文件中进行配置。
  5. 继承LohBack日志框架:

    1. logback日志框架实现了slf4j标准。(沙拉风:日志门面。日志标准。)

      1. 第一步:引入logback的依赖。

        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.11</version>
        </dependency>
        
      2. 第二步:引入logback所必须的xml配置文件。

        • 这个配置文件的名字必须叫做:logback.xml或者logback-test.xml,不能是其它的名字。
        • 这个配置文件必须放到类的根路径下。不能是其他位置。
        • 主要配置日志输出相关的级别以及日志具体的格式。【这个配置文件也可以不写,logback有一个默认的日志格式,如果我们想要规定自己的日志格式就必须写logback的配置文件】
  6. logback.xml配置文件的内容:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <configuration debug="false">
        <!-- 控制台输出 -->
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
                <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            </encoder>
        </appender>
        <!-- 按照每天生成日志文件 -->
        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <!--日志文件输出的文件名-->
                <FileNamePattern>${LOG_HOME}/TestWeb.log.%d{yyyy-MM-dd}.log</FileNamePattern>
                <!--日志文件保留天数-->
                <MaxHistory>30</MaxHistory>
            </rollingPolicy>
            <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
                <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            </encoder>
            <!--日志文件最大的大小-->
            <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
                <MaxFileSize>100MB</MaxFileSize>
            </triggeringPolicy>
        </appender>
    
        <!--mybatis log configure-->
        <logger name="com.apache.ibatis" level="TRACE"/>
        <logger name="java.sql.Connection" level="DEBUG"/>
        <logger name="java.sql.Statement" level="DEBUG"/>
        <logger name="java.sql.PreparedStatement" level="DEBUG"/>
    
        <!-- 日志输出级别,logback日志级别包括五个:TRACE < DEBUG < INFO < WARN < ERROR -->
        <root level="DEBUG">
            <appender-ref ref="STDOUT"/>
            <appender-ref ref="FILE"/>
        </root>
    
    </configuration>
    

    MyBatis工具类SqlSessionUtil的封装

    1. 每一次获取SqlSession对象代码太繁琐,封装一个工具类。

    2. 工具类代码:

      package com.powernode;
      
      import org.apache.ibatis.io.Resources;
      import org.apache.ibatis.session.SqlSession;
      import org.apache.ibatis.session.SqlSessionFactory;
      import org.apache.ibatis.session.SqlSessionFactoryBuilder;
      
      import java.io.IOException;
      
      public class SqlSessionUtil {
          private SqlSessionUtil () {}
          private static SqlSessionFactory sqlSessionFactory;
          static {
              try {
                  sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
      
          /**
           * 每调用一次openSession方法获取一个SqlSession对象
           * 该SqlSession对象,不支持自动提交,是开启事务的状态
           * @return 会话对象
           */
          public static SqlSession openSession() {
              return sqlSessionFactory.openSession();
          }
      }
      
      
    3. 工具类的测试用例:

      package com.powernode;
      
      import org.apache.ibatis.session.SqlSession;
      import org.junit.Test;
      
      public class SqlSessionTest {
          @Test
          public void testOpenSession() {
              SqlSession sqlSession = SqlSessionUtil.openSession();
              int insertCar = sqlSession.insert("insertCar");
              System.out.println("===>" + insertCar);
              sqlSession.commit();
              sqlSession.close();
          }
      }
      

使用MyBatis完成CRUD

insert(Create)

  1. 分析以下SQL映射文件中的SQL存在的问题:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="car">
        <insert id="insertCar">
            insert into t_car(car_num,brand,guide_price,produce_time,car_type) values('103', '奔驰E300L', 50.3, '2022-01-01', '燃油车')
        </insert>
    </mapper>
    
  2. 存在的问题是:SQL语句中的值不应该写死,值应该是用户提供的。之前的JDBC代码是这样写的:

    // JDBC中使用 ? 作为占位符。那么MyBatis中会使用什么作为占位符呢?
    String sql = "insert into t_car(car_num,brand,guide_price,produce_time,car_type) values(?,?,?,?,?)";
    // ......
    // 给 ? 传值。那么MyBatis中应该怎么传值呢?
    ps.setString(1,"103");
    ps.setString(2,"奔驰E300L");
    ps.setDouble(3,50.3);
    ps.setString(4,"2022-01-01");
    ps.setString(5,"燃油车");
    
  3. 在MyBatis中可以这样做:用户填写表单提交了数据之后,后端Java程序获取到用户提交的数据之后,将用户提交的数据封装成一个对象,然后将对象中封装的数据动态映射成一条SQL语句,所以在MyBatis中指的是sql语句与实体对象之间的映射,这个实体对象就是封装数据的对象。

    1. 在Java程序中可以将数据封装到Map集合中,也可以将数据封装到pojo对象中。这个pojo对象通过SqlSession对象的“insert(String sqlId,Object pojo对象)”方法传递给MyBatis,MyBatis获取到这个对象后,将这个对象中封装的数据映射成一条SQL语句。【这就是MyBats中Java对象到Sql语句的映射】
    2. insert方法有两个重载的方法:
      1. sqlSession.insert(String sqlId):用于执行sql语句这种sql语句中没有占位符,数据都在sql语句中写死了,sql语句的执行结果返回影响的数据库中的记录的条数。
      2. sqlSession.insert(String sqlId,Object pojo对象 | map集合):用于执行sql语句,将“pojo对象”映射成一条sql语句,sql语句的执行结果返回影响的数据库中的记录的条数。
        • 第一个参数是执行的insert语句的sql语句的id属性值。
        • 第二个参数数封装数据的对象,将这个对象中封装的数据映射成一条SQL语句。
          • 如果sql语句中只有一个占位符的话:#{SuiBianXie},第二个参数直接吧这个值传过去就行,不用封装成pojo对象也不用存到Map集合中。
    3. 不仅仅是insert方法,往后的delete、update都是有这两个构造方法(DML语句)。但是select就不一样了。
    4. 一般都是一张表对应一个XxxMapper.xml文件,Mapper文件中写的都是SQL语句,SQL语句中有占位符,在JDBC中SQL语句的占位符使用的是“?”,在MyBatis中SQL语句的占位符使用的是“#{这个里面写的是pojo对象的属性名 | Map集合的key}”。【严格意义上来说pojo对象的属性名是get方法去掉get首字母小写,不管有没有这个属性,只要pojo类中只要又这个方法就可以】
  4. 在MyBatis中给Mapping文件中的sql语句的占位符传值:

    1. java程序中使用POJO类给SQL语句的占位符传值:#{pojo对象的属性名}
      1. 如果写的属性名是“email”:#{email},实际上MyBatis底层调用的是getEmail()或者getemail()方法为SQL的占位符传值。
      2. 如果写的属性名是“aaa”:#{aaa},pojo对象中本来就没有这个属性,MyBatis底层去这个pojo对象中找getAaa()或者getaaa()方法的时候,找不到这个方法所以就会抛出异常。【定义getXxx()时,首字母必须大小写都可以,但是在Mapping文件中首字母必须小写】
      3. 所以最终${这里}写的是什么?
        • 严格意义上来说 这里写的不是 p o j o 对象的属性名,而是 g e t t e r 方法去掉 g e t ,然后剩下的单词首字母小写放 {这里}写的不是pojo对象的属性名,而是getter方法去掉get,然后剩下的单词首字母小写放 这里写的不是pojo对象的属性名,而是getter方法去掉get,然后剩下的单词首字母小写放{这里}(例如:getAge对应的是#{age},getUserName对应的是#{userName})。【这里首字母必须小写
        • 所以一般的符合规范的pojo类都是的get方法都是“get+属性名首字母大写”,那么我们在写占位符的时候,直接去掉get之后首字母小写放到${这里},所以符合规范的pojo类#{这里写的是属性名}
    2. java程序中使用Map集合给SQL语句的占位符传值:${Map集合中的key}:
      1. 如果写的key是“email”,${email},实际上MyBatis底层调用的是map.get(“email”)方法为SQL占位符赋值。
      2. 如果写的key是“aaa”, a a a , 实际上 M y B a t i s 底层调用的是 m a p . g e t ( " a a a " ) 方法为 S Q L 占位符赋值 , 这时集合中没有 k e y 值为 a a a 的属性,所以就返回 n u l l ,这里跟 {aaa},实际上MyBatis底层调用的是map.get("aaa")方法为SQL占位符赋值,这时集合中没有key值为aaa的属性,所以就返回null,这里跟 aaa,实际上MyBatis底层调用的是map.get("aaa")方法为SQL占位符赋值,这时集合中没有key值为aaa的属性,所以就返回null,这里跟{不存在的属性}不一样,这里不会抛出异常。
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="asdasdasd">
        <insert id="insertCar">
            insert into t_car(car_num,brand,guide_price,produce_time,car_type)
            values(#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
            <!--carNum可能是map集合中的key,也可能是pojo对象中的属性名-->
            <!--给SQL的占位符传值的时候只能是Map集合或者POJO对象,不能是List集合也不能是数组-->
            <!--只有一个占位符的时候可以直接只给一个:值,不使用pojo对象封装也不适用Map集合封装-->
        </insert>
    </mapper>
    

Delete

  1. delete方法有两个重载的方法:

    1. sqlSession.delete(String sqlId):用于执行sql语句这种sql语句中没有占位符,数据都在sql语句中写死了,sql语句的执行结果返回影响的数据库中的记录的条数。

    2. sqlSession.delete(String sqlId,Object pojo对象 | map集合):用于执行sql语句,将“pojo对象”映射成一条sql语句,sql语句的执行结果返回影响的数据库中的记录的条数。

      • 第一个参数是执行的delete语句的sql语句的id属性值。

      • 第二个参数数封装数据的对象,将这个对象中封装的数据映射成一条SQL语句。

        • 如果sql语句中只有一个占位符的话(SQL语句中的占位符${SuiBianXie}),第二个参数直接把这个值传过去就行,不用封装成pojo对象也不用存到Map集合中。
        <!--SQL语句这样写,Sql语句只有一个占位符并且delete方法的第二个参数没有使用pojo对象封装数据也没有使用集合封装数据给sql传值的时候,#{这里}可以随便写,但是虽好见名知意 -->
        <delete id="deleteByCarNum">
          delete from t_car where id = #{SuiBianXie}
        </delete>
        
        //Java程序这样写
        SqlSession sqlSession = SqlSessionUtil.openSession();
        //第二个参数直接把"占位符应该得到的这个值"传过去就行不用封装成pojo对象也不用存到Map集合中。代表将65传到“#{SuiBianXie}”位置。
        int a = sqlSession.delete("deleteCarById",65);
        //第二种方式:就是把数据放到对象中把对象映射成sql语句(板板正真的写)SQL语句中只有一个占位符的时候没有必要这么写(所有DML语句都包括在内),这么些比较麻烦
        //Map<String,Object> map = new HashMap<>();
        //map.put("SuiBianXie",64);
        //int a = sqlSession.delete("deleteCarById",map);
        System.out.println(a);
        sqlSession.commit();
        sqlSession.close();
        

Update

  1. update方法有两个重载的方法:

    1. sqlSession.update(String sqlId):用于执行sql语句这种sql语句中没有占位符,数据都在sql语句中写死了,sql语句的执行结果返回影响的数据库中的记录的条数。

    2. sqlSession.update(String sqlId,Object pojo对象 | map集合):用于执行sql语句,将“pojo对象”映射成一条sql语句,sql语句的执行结果返回影响的数据库中的记录的条数。

      • 第一个参数是执行的update语句的sql语句的id属性值。
      • 第二个参数数封装数据的对象,将这个对象中封装的数据映射成一条SQL语句。
        • 如果sql语句中只有一个占位符的话(SQL语句中的占位符${SuiBianXie}),第二个参数直接把这个值传过去就行,不用封装成pojo对象也不用存到Map集合中。
  2. sql语句如下:

    <update id="updateCarByPOJO">
      update t_car set 
        car_num = #{carNum}, brand = #{brand}, 
        guide_price = #{guidePrice}, produce_time = #{produceTime}, 
        car_type = #{carType} 
      where id = #{id}
    </update>
    

    java代码如下:

    //这里使用的是pojo对象传值的,当然使用map集合传值也是可以的
    Car car = new Car(2l,new Long(8888),"越野车686",88.8,"2015-09-08","燃油车11");
    SqlSession sqlSession = SqlSessionUtil.openSession();
    sqlSession.update("updateCarByPOJO",car);
    sqlSession.commit();
    sqlSession.close();
    
  3. 总结:给SQL语句的传值规则:

    1. 所欲Mapping配置文件中sql语句的占位符都是这样写:${pojo对象属性名 | Map集合的key | SuiBianXie}
      1. ${pojo对象属性名} :MyBatis调用pojo对象该属性的getter方法,给占位符传值。
      2. ${Map集合的key} : MyBatis调用map.get(“key”)方法给占位符传值,给占位符传值。
      3. ${SuiBianXie} : 说明sql语句中只有一个占位符,xxx(“sqlId”,这里也只传过来一个值)

Select

查询一个

  1. SqlSession对象的selectOne方法有两个重载的方法:

    1. sqlSession.selectOne(String sqlId):用于执行sql语句这种sql语句中没有占位符,数据都在sql语句中写死了。sql语句的执行结果是查询结果集,MyBatis得到查询结果集,将其封装成一个pojo对象(pojo对象的类型是在select标签的resultType属性指定的)。【这就是MyBatis的sql语句到对象的映射】

    2. sqlSession.selectOne(String sqlId,Object pojo对象 | map集合):用于执行sql语句,将“pojo对象”映射成一条sql语句。sql语句的执行结果是查询结果集,MyBatis得到查询结果集,将其封装成一个pojo对象(pojo对象的类型是在select标签的resultType属性指定的)。【这就是MyBatis的sql语句到对象的映射】

      • 第一个参数是执行的select语句的sql语句的id属性值。
      • 第二个参数数封装数据的对象,将这个对象中封装的数据映射成一条SQL语句。
        • 如果sql语句中只有一个占位符的话(SQL语句中的占位符${SuiBianXie}),第二个参数直接把这个值传过去就行,不用封装成pojo对象也不用存到Map集合中。
  2. resultType这个属性是不能省略的:虽然可以确定返回的这个类型肯定是一个Object类型的数据,通过selectOne方法的返回值也可以看到,但是MyBatis获取到数据库中的数据之后,需要给我们封装成一个对象,这个对象中有查询结果集中的字段属性,这样的对象才有意义,显然Object类型中没有查询结果集中的字段属性,所以resultType这个属性是不能省略的,必须由我们规定将数据封装到哪一个对象中。

  3. 使用selectOne查询的时候,如果查询结果集中只有一条记录,那么就直接帮我们封装成select标签中resultType属性规定的类型的数据了,如果结果集中有多条数据,那么MyBatis会抛出异常。

  4. 实现过程:

    1. 写Mapping映射文件中的sql语句:

      <!--selectCarOne是sql语句的id-->
      <!--resultType属性指定的是:让MyBatis通过反射机制将这条sql语句执行结果包装成一个什么类型的对象(resultType="这里是全限定类名")-->
      <select id="selectCarOne" resultType="com.xyh.pojo.Car">
          select * from t_car where id = #{id} 
      </select>
      
    2. Java测试程序:

      SqlSession sqlSession = SqlSessionUtil.openSession();
      // 执行DQL语句,查询。根据ID查询,返回结果一定是一条。
      // MyBatis底层执行了select语句之后,一定会返回一个结果集对象,在JDBC中叫ResultSet,接下来就是MyBatis从结果集中取出数据,封装成java对象(在resultType中指定封装成什么类型的对象),如果resultType中指定的类型的属性一个也没有查询结果集的字段,那么返回占位符的值,也就是selectOne的第二个参数
      Object obj = sqlSession.selectOne("selectCarOne",1);//sql语句中只有一个占位符,直接将这个要传进去的数据写到这里,不用封装成pojo对象也不用封装成map集合。
      System.out.println(obj);
      // 查询属于DQL语句,没有事务,不用提交,直接关闭会话就行
      sqlSession.close();
      
    3. 运行结果:Car{id=1, carNum=null, brand=‘宝马520Li’, guidePrice=null, produceTime=‘null’, carType=‘null’},

      1. 为什么有这么多null?因为查询结果集中的字段名和要封装成的对象的类型的属性名对应不上。

        # 分析上述SQL语句的查询结果
        mysql> select * from t_car where id =1;
        +----+---------+-------------+-------------+-------------+-----------+
        | id | car_num | brand       | guide_price | produce_time | car_type |
        +----+---------+-------------+-------------+-------------+-----------+
        |  1 | 1001    | 宝马520Li   |       10.00 | 2020-10-11   | 燃油车   |
        +----+---------+-------------+-------------+-------------+-----------+
        # 以汽车编号为例:查询结果集的字段名是“car_num”,Mybatis会不区分大小写的去找对象中的“car_num”属性,发现对象中没有这个属性,所以也就没能赋上值,MyBatis他不知道与“car_num”字段对应的属性名对象中的“carNum”属性,所以“carNum”属性也没有被赋值,所以就是null了。
        
      2. 解决方法:使用as重命名查询结果集的字段名,让查询结果集的字段名和对象的属性名保持一致:

        <select id="selectCarOne" resultType="com.xyh.pojo.Car">
            <!--查询结果集的大小写无所谓,因为MySql本身就不区分大小写,brand写成BrAnD也可以得到查询结果集-->
            select
                id,car_num as carNum,brand,guide_price as guidePrice,
                produce_time as produceTime,
                car_type as carType
            from
                t_car
            where
                id = #{id}
        </select>
        <!--sql语句的查询结果
        +----+--------+-------------+------------+-------------+-----------+
        | id | carNum | brand       | guidePrice | produceTime | carType   |
        +----+--------+-------------+------------+-------------+-----------+
        |  1 | 1001   | 宝马520Li   |      10.00 | 2020-10-11  | 燃油车     |
        +----+--------+-------------+------------+-------------+-----------+
        以汽车编号为例:查询结果集的字段名是“carNum”,Mybatis会不区分大小写的去找对象中的“carNum”属性,找到了这个属性,那么就会去找“carNum”属性对应的“setter”方法,set方法必须以set开头才能找到,至于set后面跟的是什么东西,只要是属性名就可以“不用区分大小写”【可以是setCarNum、setcarNuM、setCarNUm】,如果找到了对应的set方法那么就通过调用set方法给属性赋值。如果set方法没有找到那么就通过反射机制打破封装的方式给属性赋值。
        
        关系到对象的映射:
        MyBatis需要把数据库中的记录封装成对象的时候,需要调用set方法,MyBatis能找到set方法肯定是调用set方法给对象的属性赋值,哪怕set方法是私有的,打破封装也要通过set方法给属性赋值,因为MyBatis想让用户用上这个set方法给添加一些过滤条件,实在是不能set了,再通过别的方式直接给属性赋值。
        
        对象到关系的映射:因为MyBatis是半自动化的ORM框架,在MyBatis中是对象到sql语句的映射:
        MyBatis在给sql语句的占位符传值的时候,如果使用的是Bean的方式给属性传值的话,MyBatis通过调用Bean的get方法将封装在Bean中的属性取出来传递给sql语句的占位符。如果Bean中有get方法那么(方法名只允许get后面第一个字母大小写不一样,后面的字母大小写必须讲究,MyBatis视为Bean的该属性没有get方法)MyBatis肯定调用个体方法获取Bean中封装的数据(哪怕get方法是私有的),如果没有get方法,那么Mybatis通过反射机制,打破封装把属性值取出来。
        -->
        
      3. 运行结果:Car{id=1, carNum=1001, brand=‘宝马520Li’, guidePrice=10.0, produceTime=‘2020-10-11’, carType=‘燃油车’}

  5. 总结:MyBatis找get方法的时候,get方法的方法名get后面的单词只能是首字母不区分大小写【给SQL映射文件中的占位符传值的时候MyBatis用到了get方法】。

    ​ MyBatis找set方法的时候,set后面的单词所有的字母都可以不区分大小写。

    ​ MyBatis找属性的时候,也是所有的单字母可也不区分大小写。

查所有

  1. 一种方法是使用SqlSession对象的selectList方法,selectList方法有三种重载形式:

    1. sqlSession.selectList(String sqlId):用于执行sql语句这种sql语句中没有占位符,数据都在sql语句中写死了。sql语句的执行结果是查询结果集,MyBatis得到查询结果集,将其封装成一个pojo对象(pojo对象的类型是在select标签的resultType属性规定的)。【这就是MyBatis的sql语句到对象的映射】
    2. sqlSession.selectList(String sqlId,Object pojo对象 | map集合):用于执行sql语句,将“pojo对象”映射成一条sql语句。sql语句的执行结果是查询结果集,MyBatis得到查询结果集,将其封装成一个pojo对象(pojo对象的类型是在select标签的resultType属性指定的)。【这就是MyBatis的sql语句到对象的映射】
    3. sqlSession.selectList(String sqlId,Object pojo对象 | map集合,RowBounds rowBounds):在《2》的基础之上规定查询出记录的范围。第三个参数:new RowBounds(0,3)就代表从查询结果集的第0条记录开始,获取3条。
  2. 实现过程

    <!--SQL语句这样写-->
    <!--如果使用selectList方法查询,resultType="这里"写的是List集合中元素的类型,因为MyBatis已经知道selectList方法的返回值是一个List集合,返回多条数据,但是不知道List集合中的元素的类型-->
    <select id="selectAllCar" resultType="com.xyh.pojo.Car">
        select
            id,car_num as carNum,BrAnD,guide_price as guidePrice,
            produce_time as produceTime,
            car_type as carType
        from
            t_car
    </select>
    
  3. //java代码是这么写的
    SqlSession sqlSession = SqlSessionUtil.openSession();
    List<Car> cars = sqlSession.selectList("selectAllCar",null,new RowBounds(0,3));//没有占位符第二个参数可以传null,表示获取查询结果集中的前三条
    cars.forEach(car -> System.out.println(car));//使用拉姆达表达式遍历集合
    sqlSession.close();
    

总结:

  1. 给映射文件中的SQL语句传值的话,可以通过三种方式:
    1. 通过Map集合,#{map集合的key},MyBatis调用“map.get(key)”
    2. 通过pojo对象,#{pojo对象的属性名 | getter方法名去掉get首字母小写}
      • “属性”和“getter方法”有一个就行。
    3. 通过一个字面量,当sql语句中只有一个占位符的时候,#{随便写}

SQL Mapper的namespace

  1. 在SQL Mapper配置文件中标签的namespace属性可以翻译为“命名空间”,这个命名空间只要是为了防止SqlId冲突的。

  2. 如下实例:

    <!--这是CarMapping.xml文件中Sql语句-->
    <mapper namespace="t_car1">
        <select id="selectAllCar" resultType="com.xyh.pojo.Car">
            select * from t_car
        </select>
    </mapper>    
    <!--这是CarMapping2.xml文件中Sql语句-->
    <mapper namespace="t_car2">
        <select id="selectAllCar" resultType="com.xyh.pojo.Car">
            select * from t_car
        </select>
    </mapper>    
    
    SqlSession sqlSession = SqlSessionUtil.openSession();
    // 执行这条SQL语句的时候并不知道“selectAllCar”是CarMapping.xml文件中的SQL语句还是CarMapping2.xml文件中Sql语句,会报错。
    //List<Object> stus = sqlSession.selectList("selectAllCar");
    // 更改方案:所以需要在SQLID前面加上命名空间完整的写法是:命名空间.SqlId【namespace.SqlId】
    List<Object> stus = sqlSession.selectList("t_car1.selectAllCar" | "t_car2.selectAllCar");
    stus.forEach(stu -> System.out.println(stu));
    sqlSession.close();
    
  3. sql语句的Id完整写法是:namespace.SqlId

    • 当在所有的SQL Mapper文件中SqlId都不一样的时候可以省略“命名空间.”。

MyBatis核心配置文件详解

<?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>
    <!--使用build方法创建SqlSessionFactory对象的时候,第二个参数不指定使用那个环境,默认就是使用powernode这个环境-->
    <environments default="powernode">
        <environment id="powernode">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/powernode"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
        <environment id="xyh">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/xyh"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="CarMapper.xml"/>
        <mapper resource="CarMapper2.xml"/>
    </mappers>
</configuration>
  1. configuration:根标签,表示配置信息。

  2. environments:环境(多个),以‘s’结尾表示复数,也就是说MyBatis的运行环境可以配置多个数据源。environments标签可以有多个environment子标签。【以下是environments标签的属性和子标签】

    1. default属性:表示默认使用的是那个环境,default的值可以是一个environment标签的id属性值。default的值只需要和environment的id值一致即可

      • 什么叫做默认?在使用会话工厂构建对象构建会话工厂对象的时候需要调用SqlSessionFactoryBuilder对象的build方法

      • SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build("核心配置文件的输入流","使用的数据库环境的id值");//如果第二个参数没有规定那么,使用的就是默认的数据库环境
        
      • 会话工厂对象(SqlSessionFactory):一般是一个数据库对象一个,在build的时候第二个参数指定数据库,就会创建出来这个数据库的会话工厂对象,会话工厂对象创建出来之后可以开启多个会话对象,多个会话对象可以对数据库(包括数据库中所有的表)进行多种不同的操作,所以没有必要以对一个数据库创建多个会话工厂对象。

    2. environment(子标签):具体的环境(主要包括:事务管理器的配置+数据源的配置)。【以下是environment标签的属性和子标签】

      1. id属性:给当前环境一个唯一的标识,该标识应用于environment的default后面,用来指定默认环境的选择。
      2. transactionManager(子标签):配置事务管理器【以下是transactionManager的属性】
        1. type属性:指定事务管理器具体使用什么方法,可以选择的值包括如下两个
          1. JDBC(jdbc):MyBatis使用原生的JDBC事务管理机制自己管理事务,底层工作过程如下:
            • 开启事务:conn.setAutoCommit(false)
            • …处理业务
            • 提交事务:conn.commit()
          2. MANAGED:MyBatis将事务交给其他容器来管理事务,比如WebLogic、JBOSS等。如果没有管理事务的容器,则没有事务,只要执行一条DML语句,就提交一次。
      3. dataSource(子标签):指定数据源,数据源就是提供Connection对象的源头,数据源也可以叫做数据库连接池。【以下是dataSource的属性和子标签】
        1. type属性:用来指定具体使用的数据库连接池的策略,可选值包括以下三个
          1. UNPOOLED:采用传统获取连接的方式,虽然也实现了javax.sql.DataSource接口,但是并没有使用池的思想。那为什么没有使用数据库连接池还要实现javax.sql.DataSource接口,因为要面向接口编程,调用这个接口中的getConnection方法可以获取Connection连接对象。
            • property的name属性可以是:
              1. driver:这是jdbc驱动的Java类全限定类名。
              2. url:这里是数据库的url地址
              3. username:登录数据库的用户名
              4. password:登录数据库的密码
              5. defaultTransactionIsolationLevel 默认的连接事务隔离级别。
          2. POOLED:采用传统的javax.sql.DataSource规范中的连接池。什么是javax.sql.DataSource规范中的连接池,实现javax.sql.DataSource规范的连接池。MyBatis中自身就有针对javax.sql.DataSource规范的实现。所以type属性值写POOLED就代表使用的是MyBatis自身的数据库连接池。
            • property的name属性可以是(除了包含UNPOOLED中之外):
              1. poolMaximumActiveConnections:连接池当中最多的正在使用的连接对象的数量上限。最多有多少个连接可以活动。默认值10,【需要根据系统的并发情况,来合理调整连接池最大连接数以及最多空闲数量。充分发挥数据库连接池的性能。【可以根据实际情况进行测试,然后调整一个合理的数量。】】
              2. poolTimeToWait:每隔2秒打印日志,并且尝试获取连接对象,让程序员知道此时正在等待连接对象并不是程序已经卡死了。
              3. poolMaximumCheckoutTime:强行让某个连接空闲,超时时间的设置
              4. poolMaximumIdleConnections:最多的空闲数量【假设连接池的最多空闲数量是5个,假设目前已经空闲了5个,马上第六个就要空闲了,假设第6个空闲下来,此时连接池为了保证最多空间数量为5个,会真正关闭多余的空闲连接对象,来减少数据库的开销。】
            • 连接池的优点:
              1. 每一次获取连接都从连接池中拿,效率高
              2. 因为每一次的连接对象都是从池中得到的,所以连接对象的创建数量是可控的,避免了连接对象数量过多导致的数据库服务器压力过大。
          3. JNDI:采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所能拿到DataSource是不一样。如果不是web或者maven的war工程,JNDI是不能使用的。这种方式:表示对接JNDI服务器中的连接池。这种方式给了我们可以使用第三方连接池的接口。如果想使用dbcp、c3p0、druid(德鲁伊)等,需要使用JNDI这种方式。【JNDI可以被用来定位任何类型的对象,不仅仅是命名服务对象。它还提供了一种机制来将名称与对象相关联,并使得这些对象能够被在分布式环境中进行访问和共享。JNDI规范还定义了一系列API,用于管理命名和目录服务中的对象,包括创建、更新、删除和搜索等操作。】
            1. property可以是(最多只包含以下两个属性):
              1. initial_context 这个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))这是个可选属性,如果忽略,那么将会直接从 InitialContext 中寻找 data_source 属性。
              2. data_source 这是引用数据源实例位置的上下文路径。提供了 initial_context 配置时会在其返回的上下文中进行查找,没有提供时则直接在 InitialContext 中查找。
        2. property(子标签):用来配置连接数据库环境的具体参数。【以下是property标签的属性】
          1. nama属性:规定属性名
          2. value属性:规定属性值
  3. mappers:在mappers标签中可以配置多个sql映射文件的路径【mappers标签的子标签如下】

    1. mapper:配置某个sql映射文件的路径【mapper标签的属性如下】
      1. resource属性:使用相对于类路径的资源引用方式,从类路径下开始查找。
      2. url属性:使用绝对路径
  4. mybatis提供了更加灵活的配置,连接数据库的信息可以单独写到一个属性资源文件中,假设在类的根路径下创建jdbc.properties文件,配置如下:

    jdbc.url=jdbc:mysql://localhost:3306/powernode
    
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!--
	  使用properties标签引入外部属性资源文件:两种方式
		1.resource=“从类路径下开始查找属性资源文件”
		2.url=“属性资源文件的绝对路径”
	-->
    <properties resource="jdbc.properties">
        <!--还可以使用这种方式直接将配置的参数写到xml文件当中,这是Maven提供的方式,就相当于在Maven工程中定义了一个变量,变量名是“jdbc.driver”-->
        <property name="jdbc.driver" value="com.mysql.jdbc.Driver"/>
    </properties>

    <environments default="powernode">
        <environment id="powernode">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <!--使用:
				${“这里写的是什么”}?
					1.<property name="写这个" value="返回这个"/> 
					2.或者属性配置文件中的key【属性配置文件都是key=value....的形式:${key}】
				-->
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="CarMapping.xml"/>
        <mapper resource="StudentMapping.xml"></mapper>
    </mappers>
</configuration>
  1. properties两个属性:
    1. resource:这个属性从类的根路径下开始加载。【常用的,可移植性好】
    2. url:从执行的url加载,从绝对路径开始加载。【file:///属性配置文件的绝对路径】

手写MyBatis框架

Dom4j解析XML文件

  1. 引入Dom4j依赖

    <dependencies>
        <!--dom4j依赖-->
        <dependency>
            <groupId>org.dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>2.1.3</version>
        </dependency>
        <!--jaxen依赖-->
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.2.0</version>
        </dependency>
    </dependencies>
    
  2. 第二步:编写配置文件godbatis-config.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    
    <configuration>
        <environments default="dev">
            <environment id="dev">
                <transactionManager type="JDBC"/>
                <dataSource type="POOLED">
                    <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql://localhost:3306/powernode"/>
                    <property name="username" value="root"/>
                    <property name="password" value="root"/>
                </dataSource>
            </environment>
            <mappers>
                <mapper resource="sqlmapper.xml"/>
            </mappers>
        </environments>
    </configuration>
    
  3. 第三步:解析godbatis-config.xml

    import org.dom4j.Document;
    import org.dom4j.Element;
    import org.dom4j.Node;
    import org.dom4j.io.SAXReader;
    import org.junit.Test;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    /**
     * 使用dom4j解析XML文件
     */
    public class ParseXMLByDom4j {
        @Test
        public void testGodBatisConfig() throws Exception{
    
            // 读取xml,获取document对象
            SAXReader saxReader = new SAXReader();
            Document document = saxReader.read(Thread.currentThread().getContextClassLoader().getResourceAsStream("godbatis-config.xml"));
    
            // 获取<environments>标签的default属性的值
            Element environmentsElt = (Element)document.selectSingleNode("/configuration/environments");
            String defaultId = environmentsElt.attributeValue("default");
            System.out.println(defaultId);
    
            // 获取environment标签
            Element environmentElt = (Element)document.selectSingleNode("/configuration/environments/environment[@id='" + defaultId + "']");
    
            // 获取事务管理器类型
            Element transactionManager = environmentElt.element("transactionManager");
            String transactionManagerType = transactionManager.attributeValue("type");
            System.out.println(transactionManagerType);
    
            // 获取数据源类型
            Element dataSource = environmentElt.element("dataSource");
            String dataSourceType = dataSource.attributeValue("type");
            System.out.println(dataSourceType);
    
            // 将数据源信息封装到Map集合
            Map<String,String> dataSourceMap = new HashMap<>();
            dataSource.elements().forEach(propertyElt -> {
                dataSourceMap.put(propertyElt.attributeValue("name"), propertyElt.attributeValue("value"));
            });
    
            dataSourceMap.forEach((k, v) -> System.out.println(k + ":" + v));
    
            // 获取sqlmapper.xml文件的路径
            Element mappersElt = (Element) document.selectSingleNode("/configuration/environments/mappers");
            mappersElt.elements().forEach(mapper -> {
                System.out.println(mapper.attributeValue("resource"));
            });
        }
    }
    
    1. SAXReader对象是dom4j中解析xml的核心对象,调用SAXReader对象的read方法获取整个文档对象

      InputStream resourceAsStream = ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml");
      //document就是整个文档
      Document document = reader.read(resourceAsStream);
      
    2. Document的selectSingleNode方法用于获取单个节点。

      document.selectSingletonNode(String xpath);
      /*
      xpath:如果以“/”开始代表从根路径开始获取元素,如果以“//”开始代表从任意位置开始获取元素。
      */
      document.getRootElement();//获取xml文档的根元素
      
    3. Element对象的element方法用于获取该元素的特定子元素

      element.element(String tagName);//获取标签名是“tagName”的子标签
      element.elements();//获取所有的子标签
      element.getElementTrim();//获取标签中的内容,并且去除前后空格
      
    4. Element对象的attributeValue方法用于获取元素的属性值

      element.attributeValue(String attrName);//获取attrName属性值
      

使用javassist生成类

Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。

Javassist的使用

  1. 我们要使用javassist,首先要引入它的依赖。

    <dependency>
        <groupId>org.javassist</groupId>
        <artifactId>javassist</artifactId>
        <version>3.20.0-GA</version>
    </dependency>
    
  2. 样例代码:

    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.CtMethod;
    import javassist.Modifier;
    
    import java.lang.reflect.Method;
    
    public class JavassistTest {
        public static void main(String[] args) throws Exception {
            // 获取类池
            ClassPool pool = ClassPool.getDefault();
            // 创建类
            CtClass ctClass = pool.makeClass("com.powernode.javassist.Test");
            // 创建方法
            // 1.返回值类型 2.方法名 3.形式参数列表 4.所属类
            CtMethod ctMethod = new CtMethod(CtClass.voidType, "execute", new CtClass[]{}, ctClass);
            // 设置方法的修饰符列表
            ctMethod.setModifiers(Modifier.PUBLIC);
            // 设置方法体
            ctMethod.setBody("{System.out.println(\"hello world\");}");
            // 给类添加方法
            ctClass.addMethod(ctMethod);
            // 调用方法
            Class<?> aClass = ctClass.toClass();
            Object o = aClass.newInstance();
            Method method = aClass.getDeclaredMethod("execute");
            method.invoke(o);
        }
    }
    
  3. 高版本的JDK运行的时候需要加入两个参数:

    --add-opens java.base/java.lang=ALL-UNNAMED
    --add-opens java.base/sun.net.util=ALL-UNNAMED
    

    img

  4. 使用MyBatis框架自带的工具类可以帮我们动态生成Dao实现类:我们就不需要写MyBatis的实现类了,但是MyBatis的工具类在生成DaoProxy字节码的时候不知道我们的Dao层的实现类的方法体中要执行的sql语句是什么,所以就需要通过sqlid来指定要执行的的sql语句是什么,sqlid是程序员写的,具有多变性,那怎么帮呐?所以MyBatis框架的开发者就出台了一个规定:凡是使用GenerateDaoPeoxy机制的,SqlId都不能随便写,namespace必须是dao层接口的全限定类名,id必须是dao接口中的方法名。【疑问:万一一个Dao实现类中的方法中需要执行两个sql语句,那么SqlId不久重复了吗,怎么办?】

使用Javassist生成DaoImpl类

使用Javassist动态生成DaoImpl类

package com.powernode.bank.utils;

import org.apache.ibatis.javassist.CannotCompileException;
import org.apache.ibatis.javassist.ClassPool;
import org.apache.ibatis.javassist.CtClass;
import org.apache.ibatis.javassist.CtMethod;
import org.apache.ibatis.session.SqlSession;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;

public class GenerateDaoByJavassist {

    /**
     * 根据dao接口生成dao接口的代理对象
     *
     * @param sqlSession   sql会话
     * @param daoInterface dao接口
     * @return dao接口代理对象
     */
    public static Object getMapper(SqlSession sqlSession, Class daoInterface) {
        ClassPool pool = ClassPool.getDefault();
        // 生成代理类
        CtClass ctClass = pool.makeClass(daoInterface.getPackageName() + ".impl." + daoInterface.getSimpleName() + "Impl");
        // 接口
        CtClass ctInterface = pool.makeClass(daoInterface.getName());
        // 代理类实现接口
        ctClass.addInterface(ctInterface);
        // 获取所有的方法
        Method[] methods = daoInterface.getDeclaredMethods();
        Arrays.stream(methods).forEach(method -> {
            // 拼接方法的签名
            StringBuilder methodStr = new StringBuilder();
            String returnTypeName = method.getReturnType().getName();
            methodStr.append(returnTypeName);
            methodStr.append(" ");
            String methodName = method.getName();
            methodStr.append(methodName);
            methodStr.append("(");
            Class<?>[] parameterTypes = method.getParameterTypes();
            for (int i = 0; i < parameterTypes.length; i++) {
                methodStr.append(parameterTypes[i].getName());
                methodStr.append(" arg");
                methodStr.append(i);
                if (i != parameterTypes.length - 1) {
                    methodStr.append(",");
                }
            }
            methodStr.append("){");
            // 方法体当中的代码怎么写?
            // 获取sqlId(这里非常重要:因为这行代码导致以后namespace必须是接口的全限定接口名,sqlId必须是接口中方法的方法名。)
            String sqlId = daoInterface.getName() + "." + methodName;
            // 获取SqlCommondType
            String sqlCommondTypeName = sqlSession.getConfiguration().getMappedStatement(sqlId).getSqlCommandType().name();
            if ("SELECT".equals(sqlCommondTypeName)) {
                methodStr.append("org.apache.ibatis.session.SqlSession sqlSession = com.powernode.bank.utils.SqlSessionUtil.openSession();");
                methodStr.append("Object obj = sqlSession.selectOne(\"" + sqlId + "\", arg0);");
                methodStr.append("return (" + returnTypeName + ")obj;");
            } else if ("UPDATE".equals(sqlCommondTypeName)) {
                methodStr.append("org.apache.ibatis.session.SqlSession sqlSession = com.powernode.bank.utils.SqlSessionUtil.openSession();");
                methodStr.append("int count = sqlSession.update(\"" + sqlId + "\", arg0);");
                methodStr.append("return count;");
            }
            methodStr.append("}");
            System.out.println(methodStr);
            try {
                // 创建CtMethod对象
                CtMethod ctMethod = CtMethod.make(methodStr.toString(), ctClass);
                ctMethod.setModifiers(Modifier.PUBLIC);
                // 将方法添加到类
                ctClass.addMethod(ctMethod);
            } catch (CannotCompileException e) {
                throw new RuntimeException(e);
            }
        });
        try {
            // 创建代理对象
            Class<?> aClass = ctClass.toClass();
            Constructor<?> defaultCon = aClass.getDeclaredConstructor();
            Object o = defaultCon.newInstance();
            return o;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

启动过程中显示,tomcat服务器自动添加了以下的两个运行参数。所以不需要再单独配置。

img

MyBatis中接口代理机制及使用

  1. MyBatis 的接口代理机制是指,当我们定义一个接口并使用 MyBatis 进行配置时,MyBatis 会为这个接口动态地生成一个代理对象,该代理对象实现了这个接口,并且在执行方法时会调用 MyBatis 的 SQL 映射语句。
  2. 换而言之,当我们使用 MyBatis 进行配置后,MyBatis 会根据接口的定义动态地生成一个代理对象,这个代理对象可以将接口的方法调用转化为对应的 SQL 映射语句的执行。这使得我们可以在代码中直接调用接口方法,而不需要编写任何与数据库操作相关的代码。
  3. 这种代理机制的好处在于,它可以使得我们的代码更加简洁、易于维护和扩展,同时也可以提高程序的性能。

4.直接调用以下代码即可获取dao接口的代理类:[MyBatis可以帮助我们自动生成Dao层接口的实现类]

AccountDao accountDao = (AccountDao)sqlSession.getMapper(AccountDao.class);
/*MyBatis中的getMapper()方法是用来获取Mapper接口的实例对象,这个实例对象是通过MyBatis框架自动生成的。Mapper接口中定义了一系列的查询语句,通过调用这些查询语句可以对数据库进行各种操作。

在MyBatis中,Mapper接口并没有具体的实现类,而是由框架在运行时动态生成的代理类。当我们调用getMapper()方法时,MyBatis会根据传入的Mapper接口类型,使用JDK动态代理技术动态生成一个该Mapper接口的实例。

这个实例会拦截所有方法调用,并将调用转发给MyBatis内部的SqlSession对象去执行具体的SQL语句。这样就可以很方便地进行数据库操作,而无需手动编写SQL语句和结果集映射代码。*/

namespace必须和dao接口的全限定名称一致,id必须和dao接口中方法名一致。

将service中获取dao对象的代码再次修改,如下:

img

这种方式仅适用于Dao层的实现类不用写了,因为Dao层的实现类代码固定并且没有业务,所以可以根据固定的方法生成,但是Service层的实现类不能使用这种方式生成实现类【MyBatis实际上采用了代理模式。在内存中通过javassist框架生成dao接口的代理类,然后创建代理类的实例】

如果使用MyBatis的话一般后端的dao层使用mapper层替代

dao层和mapper层里面的东西都是一样的,只不过一个叫做dao一个叫做mapper,别人一看mapper包就知道使用了MyBatis框架,可读性更强。比如Dao层中的一个接口叫做“CarDao”那么mapper层中的接口叫做“CarMapper”,只是名字不同而已。

总结使用MyBatis的接口代理机制的步骤:

  1. 编写MyBatis的核心配置文件。
  2. 编写Mapper层的接口(映射器接口,实现这个接口的类就是映射器)
  3. 编写MyBatis的SQL映射文件。
    1. namespace是Mapper接口的全限定接口名
    2. sqlid是Mapper接口中的方法名
  4. 编写测试程序:
    1. 调用SqlSession的getMapper方法让MyBatis为我们创建接口的代理对象,这个代理对象就是Mapper接口的实现类的对象。我们只需要通过这个代理对象调用接口中的方法。我们只需要面向接口编程,面向接口调方法。【getMapper方法需要一个参数,这个参数就是Mapper接口的Class对象,比如想要为AMapper接口创建代理对象,为AMapper创建实现类,那么这个参数就需要写AMapper.class】
      • 接口的实现者是MyBatis,接口的调用这是我们。

如果一个接口方法对应两条不同的 SQL 语句,而这些 SQL 语句在映射文件中的 ID 仍然使用了方法名,那么就会导致 SQL ID 的重复,这时需要显式地为这些 SQL 语句指定不同的 ID。

一种解决方案是采用 MyBatis 提供的 @SelectProvider 注解来动态生成 SQL 语句。使用 @SelectProvider 可以将查询语句的构建逻辑放到一个单独的类中,这个类可以根据传入参数的不同来生成不同的 SQL 语句,并为每个 SQL 语句指定不同的 ID。

另一种解决方案是在 XML 映射文件中采用 <select> 标签的形式来定义 SQL 语句,而不是直接使用 ID。这样可以避免 ID 重复的问题,同时也可以更加灵活地定义 SQL 语句的内容和参数。

如果只是编写了映射器接口,没有编写与映射器接口中方法对应的SQL语句(或者namespace没有使用接口的全限定接口名,sqlid没有使用方法名),使用MyBatis接口代理机制的话,代理类通过namespace+sqlid找不到对应的sql语句就会抛出异常。
但是接口中的默认方法可以没有与之对应的SQL映射文件中的SQL语句也可以执行

MyBatis小技巧

#{}和${}

  1. #{}:先编译sql语句,再给占位符传值,底层是PreparedStatement实现。可以防止sql注入,比较常用。

  2. ${}:先进性sql语句的拼接,然后再编译sql语句,底层是Statement实现。存在sql注入现象,只有在需要进行sql语句关键字拼接的情况下才会用到。

  3. ${}是先把pojo对象中的值取出来然后再拼接成一条SQL语句,最后再编译这条SQL语句,执行sql语句。

    #{}是先把sql语句编译了,有占位符?,然后再把pojo对象中的值取出来给占位符传值。zhi行sql语句。

  4. 同一条SQL语句中,KaTeX parse error: Expected 'EOF', got '#' at position 4: {}和#̲{}都有的时候先执行{},后执行#{}

  5. 如果pojo对象的属性(map集合的value值)是“字符串”:给SQL映射文件中的两种占位符传值的时候

    1. ${}:直接把这个字符串拼接到SQL语句中然后再进行sql语句的编译,把这个字符串拼接到SQL语句的时候并没有带单引号。
    2. #{}:先把sql语句进行编译,然后再把这个字符串传递给占位符,传递过去的时候这个字符串是自带单引号的。
  6. 无论是${}也好还是#{}也好都是从对象中取数据的。都是MyBatis中的占位符。

    • 可以从pojo对象中取数据

      • 如果是从pojo对象中取数据,那么花括号中放的是pojo对象的属性名
    • 可以从map即可中取数据

      • 如果是从map集合中取数据,那么胡括号中放的是map集合的key值
  7. 为什么${}可以写在单引号里面而#{}就不可以呐?

    • mybatis在检查sql映射文件中的sql语句的时候不管是否在字符串里面只要有占位符就进行处理:

      • ${}:把值取出来,拼接到sql语句中。
      • #{}:把占位符替换成?。如果#{}在单引号里面替换之后的?也在单引号里面,dbms无法检测到单引号里的占位符,所以#{}就不能出现在单引号里面。
    • 核心逻辑:${}先拼接后编译,#{}先替换后传值。

使用#{}

  1. 依赖

    <?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.powernode</groupId>
        <artifactId>mybatis-005-antic</artifactId>
        <version>1.0-SNAPSHOT</version>
        <packaging>jar</packaging>
    
        <dependencies>
            <!--mybatis依赖-->
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis</artifactId>
                <version>3.5.10</version>
            </dependency>
            <!--mysql驱动依赖-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.30</version>
            </dependency>
            <!--junit依赖-->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.13.2</version>
                <scope>test</scope>
            </dependency>
            <!--logback依赖-->
            <dependency>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-classic</artifactId>
                <version>1.2.11</version>
            </dependency>
        </dependencies>
    
        <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
        </properties>
    
    </project>
    
  2. jdbc.properties放在类的根路径下

    jdbc.driver=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/powernode
    jdbc.username=root
    jdbc.password=root
    
  3. utils

    import org.apache.ibatis.io.Resources;
    import org.apache.ibatis.session.SqlSession;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.apache.ibatis.session.SqlSessionFactoryBuilder;
    
    public class SqlSessionUtil {
        private static SqlSessionFactory sqlSessionFactory;
    
        static {
            try {
                SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
                sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        private static ThreadLocal<SqlSession> local = new ThreadLocal<>();
    
        public static SqlSession openSession() {
            SqlSession sqlSession = local.get();
            if (sqlSession == null) {
                sqlSession = sqlSessionFactory.openSession();
                local.set(sqlSession);
            }
            return sqlSession;
        }
    
        public static void close(SqlSession sqlSession){
            if (sqlSession != null) {
                sqlSession.close();
            }
            local.remove();
        }
    }
    
  4. pojo

    public class Car {
        private Long id;
        private String carNum;
        private String brand;
        private Double guidePrice;
        private String produceTime;
        private String carType;
        // 构造方法
        // set get方法
        // toString方法
    }
    
  5. mapper接口

    import java.util.List;
    public interface CarMapper {
    
        List<Car> selectByCarType(String carType);
    
    }
    
  6. mybatis-config.xml,放在类的根路径下

    <?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>
        <properties resource="jdbc.properties"/>
        <environments default="dev">
            <environment id="dev">
                <transactionManager type="JDBC"/>
                <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="CarMapper.xml"/>
        </mappers>
    </configuration>
    
  7. CarMapper.xml,SQL映射文件,放在类的根路径下:注意namespace必须和接口名一致,id必须和接口中的方法名一致。【注意:因为MyBatis在内存中文给我们生成的Mapper层的实现类也需要调用SQL映射文件中的sql语句完成功能,所以MyBatis就必须知道sql语句的id是什么才能找到对应的sql语句并且执行sql语句。又因为SQL语句的id是使用MyBatis框架的程序员提供的具有多变性,所以MyBatis规定如果要是使用MyBatis的接口代理机制,namespace就必须是接口的全限定接口名,sqlid必须是接口中的方法名,这样MyBatis就确定下来了SQL映射文件中的sql语句的id】

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="com.powernode.mybatis.mapper.CarMapper">
        <select id="selectByCarType" resultType="com.powernode.mybatis.pojo.Car">
            select
                id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
            from
                t_car
            where
                car_type = #{carType}
        </select>
    </mapper>
    
  8. 测试程序

    import com.powernode.mybatis.mapper.CarMapper;
    import com.powernode.mybatis.pojo.Car;
    import com.powernode.mybatis.utils.SqlSessionUtil;
    import org.junit.Test;
    
    import java.util.List;
    
    public class CarMapperTest {
    
        @Test
        public void testSelectByCarType(){
            //调用SqlSession对象的getMapper(Class c)方法,MyBatis就可以帮助我们生成接口的代理类,也就是以前的Dao层接口的实现类,传进去的参数是接口类型的字节码
            CarMapper mapper = (CarMapper) SqlSessionUtil.openSession().getMapper(CarMapper.class);
            List<Car> cars = mapper.selectByCarType("燃油车");
            cars.forEach(car -> System.out.println(car));
        }
    }
    
  9. 运行结果

    img

    1. 通过执行可以清除看到,sql语句中是带有?的,这个?就是大家在JDBC中学的占位符,专门用来接收值的。
    2. 把“燃油车”以String类型的值,传给?
  10. 这就是#{}他会先编译SQL语句,然后再给占位符传值。

使用${}

  1. 同样的需求,我们使用${}来完成

  2. CarMapper.xml文件修改如下

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="com.powernode.mybatis.mapper.CarMapper">
        <select id="selectByCarType" resultType="com.powernode.mybatis.pojo.Car">
            select
                id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
            from
                t_car
            where
                <!--car_type = #{carType}-->
                car_type = ${carType}
        </select>
    </mapper>
    
  3. 再次运行测试程序:

    img

    出现异常了,这是为什么呢?看看生成的sql语句:

    img

    很显然, 是先进行 s q l 语句的拼接,然后再编译,出现语法错误是正常的,因为燃油车是一个字符串,在 s q l 语句中应该添加单引号, {} 是先进行sql语句的拼接,然后再编译,出现语法错误是正常的,因为 燃油车 是一个字符串,在sql语句中应该添加单引号, 是先进行sql语句的拼接,然后再编译,出现语法错误是正常的,因为燃油车是一个字符串,在sql语句中应该添加单引号,{}取出来的值进行sql语句拼接的时候,不带单引号。

    修改:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="com.powernode.mybatis.mapper.CarMapper">
        <select id="selectByCarType" resultType="com.powernode.mybatis.pojo.Car">
            select
                id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
            from
                t_car
            where
                <!--car_type = #{carType}-->
                <!--car_type = ${carType}-->
                car_type = '${carType}'
        </select>
    </mapper>
    

    再执行测试程序:

    img

    img

    通过以上测试,可以看出,对于以上这种需求来说,还是建议使用 #{} 的方式。

    原则:能用 #{} 就不用 ${}

什么情况下必须使用${}

  1. 当需要进行sql语句关键字的拼接的时候。必须使用:${}
  2. 需求:通过向sql语句中注入asc或者desc关键字,来完成数据的升序或者降序排列。

先使用#{}尝试:

  1. CarMapper接口:

    List<Car> selectAll(String ascOrDesc);
    
  2. CarMapper.xml文件:

    <select id="selectAll" resultType="com.powernode.mybatis.pojo.Car">
      select
      id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
      from
      t_car
      order by carNum #{key}
    </select>
    
  3. 测试程序

    @Test
    public void testSelectAll(){
        CarMapper mapper = (CarMapper) SqlSessionUtil.openSession().getMapper(CarMapper.class);
        List<Car> cars = mapper.selectAll("desc");
        cars.forEach(car -> System.out.println(car));
    }
    
  4. 运行结果

    img

    报错的原因是sql语句不合法,因为采用这种方式传值,最终sql语句会是这样:

    select id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType from t_car order by carNum ‘desc’

    因为使用#{}方式传值的话,会把String类型的属性带上双引号已经编译过的sql语句。

    desc是一个关键字,不能带单引号的,所以在进行sql语句关键字拼接的时候,必须使用${}

使用${}改造

  1. CarMapper.xml

    <select id="selectAll" resultType="com.powernode.mybatis.pojo.Car">
      select
      id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
      from
      t_car
      <!--order by carNum #{key}-->
      order by carNum ${key}
    </select>
    
  2. 再次运行测试程序

    img

批量删除

  1. 业务背景:一次删除多条记录

  2. 对应的sql语句

    #当然完全可以弄三个占位符来给id赋值
    delete from t_user where id = 1 or id = 2 or id = 3;
    #这里为了描述#{}和${}的区别就先使用这种方式
    delete from t_user where id in (1,2,3)
    
  3. 假设现在使用in的这种处理方法,前端传过来的字符串是:1,2,3.如果使用mybatis处理,应该使用#{} 还是 ${}

  4. 使用#{} :delete from t_user where id in(‘1,2,3’) 执行错误:1292 - Truncated incorrect DOUBLE value: ‘1,2,3’

  5. 使用${} :delete from t_user where id in(1, 2, 3)

  6. 代码

    //Mapper接口中的方法
    int deleteBatch(String ids);
    //SQL映射文件中的SQL语句
    <delete id="deleteBatch">
      // 这里必须使用 ${id}
      delete from t_car where id in(${ids})
    </delete>
    //测试程序
    @Test
    public void testDeleteBatch(){
        CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
        int count = mapper.deleteBatch("1,2,3");
        System.out.println("删除了几条记录:" + count);
        SqlSessionUtil.openSession().commit();
    }
    
  7. 运行结果:

    img

模糊查询

  1. 需求:查询奔驰系列的汽车。【只要品牌brand中含有奔驰两个字的都查询出来。】

使用${}

//使用MyBatis接口代理机制,接口中的方法
List<Car> selectLikeByBrand(String likeBrank);
//MyBatis的SQL映射文件的sql语句
<select id="selectLikeByBrand" resultType="Car">
  select
  id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
  from
  t_car
  where
  brand like '%${brand}%' //先将${brand}的值从pojo对象中取出来${brand}的值也就是“奔驰”,拼接之后的sql语句就是 ... brand like '%奔驰%'。
</select>
//测试程序
@Test
public void testSelectLikeByBrand(){
    CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
    List<Car> cars = mapper.selectLikeByBrand("奔驰");
    cars.forEach(car -> System.out.println(car));
}

运行结果:

img

使用#{}

第一种方式:concat函数
<!--SQL映射文件中的sql语句-->
<select id="selectLikeByBrand" resultType="Car">
  select
  id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
  from
  t_car
  where
  brand like concat('%',#{brand},'%') 
    <!--编译之后的sql语句是  brand like concat('%',?,'%') -->
	<!--传值之后的sql语句是:brand like concat('%','奔驰','%') -->
    <!--拼接之后的结果就是: ... brand like '%奔驰%'-->
</select>

运行结果:

img

第二种:双引号方式
<select id="selectLikeByBrand" resultType="Car">
  select
  id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
  from
  t_car
  where
  brand like "%"#{brand}"%" 
    <!--一定要把#{brand}暴露在字符串之外,这是因为把sql语句传递给MyBatis之后,MyBatis首先进行占位符的替换操作,先将MyBatis的占位符替换成JDBC的占位符(?),替换之后的占位符如果在字符串的里面,那么底层的JDBC就没有办法给占位符传值了,JDBC检测不到占位符了,JDBC只认为字符串里面的?就是一个普通的?,并不是占位符。-->
    <!--如果sql语句写成  brand like "%#{brand}%" ,那么替换之后是brand like "%?%"的DBMS检测不到字符串里面的?,所以就不能给占位符传值 -->
</select>

MySQL原理:

select * from t_car where car_type like “%” “油” “%”,如果like后面有多个字符串(多个字符串之间可以使用空格隔开,也可以不使用空格隔开),那么DBMS就会把like后面的多个字符串看成是一个字符串。上面这句sql,效果等同于select * from t_car where car_type like “%油%”

typeAliases

  1. 我们先来观察一下MyBatis的SQL映射文件CarMapper.xml中的配置信息:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="com.powernode.mybatis.mapper.CarMapper">
    
        <select id="selectAll" resultType="com.powernode.mybatis.pojo.Car">
            select
                id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
            from
                t_car
            order by carNum ${key}
        </select>
    
        <select id="selectByCarType" resultType="com.powernode.mybatis.pojo.Car">
            select
                id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
            from
                t_car
            where
                car_type = '${carType}'
        </select>
    </mapper>
    
  2. resultType属性用来指定查询结果集的封装类型,这个名字太长,可以起别名。

  3. 在mybatis-config.xml文件中使用typeAliases标签来起别名,包括两种方式:

第一种方式:typeAlias

<typeAliases>
  <typeAlias type="com.powernode.mybatis.pojo.Car" alias="Car"/>
</typeAliases>
  1. 首先要注意typeAlias标签的放置位置,如果放错了,可以看看错误提示信息。

  2. typeAliases标签中的typeAlias可以写多个。

    <typeAliases>
        <!--type:指定给哪个类起别名,
    	   alias:别名。alias不是必须的可以省略,如果缺省的话,type属性指定的类型名的简类名作为别名。alias是大小写不敏感的。也就是说假设alias="Car",再用的时候,可以CAR,也可以car,也可以Car,都行。-->
        <typeAlias type="com.xyh.pojo.Car" alias="Car"></typeAlias>
        <typeAlias type="com.xyh.pojo.Student" alias="Student"></typeAlias>
    </typeAliases>
    

第二种方式:package

  1. 如果一个包下类太多,每个类都要起别名,会导致typeAlias标签配置较多,所以MyBatis提供了package的配置方式,只需要指定包名,该包下所有的类都自动起别名,别名就是简类名。并且别名都不区分大小写。

    <typeAliases>
        <!--com.powernode.mybatis.pojo包下的所有的类都有别名了,别名就是简类名-->
    	<package name="com.powernode.mybatis.pojo"/>
    </typeAliases>
    
  2. package也可以配置多个的。

  3. 在SQL映射文件中用一下

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <!--namespace属性这里不可以使用别名。-->
    <mapper namespace="com.powernode.mybatis.mapper.CarMapper">
    
        <select id="selectAll" resultType="CAR">
            select
                id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
            from
                t_car
            order by carNum ${key}
        </select>
    
        <select id="selectByCarType" resultType="car">
            select
                id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
            from
                t_car
            where
                car_type = '${carType}'
        </select>
    </mapper>
    
  4. namespace属性不能使用别名,必须是Mapper接口的全限定接口名。

mappers

  1. SQL映射文件的配置方式包括四种:
    1. resource:从类的根路径中加载。
    2. url:从绝对路径中加载。
    3. class:使用映射器接口实现类的全限定类名。
    4. package:将包内的所有映射器接口的实现类全部注册为映射器。

resource

  1. 这种方式是从类路径中加载配置文件,所以这种方式要求SQL映射文件必须放在resources目录下。

    <mappers>
     <!--org/mybatis/builder/AuthorMapper.xml路径之间使用“/”分割,不是使用“.”分割-->   
      <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
      <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
      <mapper resource="org/mybatis/builder/PostMapper.xml"/>
    </mappers>
    

url

这种方式显然使用了绝对路径的方式,这种配置对SQL映射文件存放的为止没有要求,随意。这种方式使用较少,移植性差。

<mappers>
  <!--url属性指向SQL映射文件在计算机硬盘的绝对路径。以file:///开始-->
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>

class

如果使用这种方式必须满足以下条件:

  • SQL映射文件和mapper接口放在同一个目录下。
  • SQL映射文件的名字必须和mapper接口名一致。
<mappers>
  <!--class属性写的是接口的全限定接口名-->
  <!--如果配置的是class属性那么MyBatis会去同一个包下找同名的接口和SQL映射文件。
	例如:如果配置的class属性值是org.mybatis.builder.AuthorMapper,那么MyBatis就会去org.mybatis.builder包下找AuthorMapper.class接口的字节码文件给接口生成一个实现类(也叫做代理类或者映射器)和AuthorMapper.xml SQL映射文件-->
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>

放到Java下和放到resource下都是放到了类的根路径下,SQL映射文件放resource下,Mapper接口放到java下。

在resource下新建目录的时候使用“/”隔开,在java下新建目录的时候使用“.”隔开

为什么使用class属性这种方式即需要SQL映射文件的文件名和接口的简类名一致有需要和接口放到同一个包下这么多约束要求,还要用这种方式?
答:因为这种方式是为了后续使用package机制作准备的。class属性名规定的是接口的全限定名,一旦使用package指定了某个包,这个包下所有的接口都被class属性指定是一个效果。因为resource属性和url属性都没有package机制,所以即使有这么多约束这么多要有我们也要使用class属性也要使用package机制。

package

<?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>
    <typeAliases>
        <!--实际开发过程中:可能有很多个类需要起别名,如果要是有成千上万行代码起别名的代码肯定完犊子,所以我们需要使用package机制-->
        <typeAlias type="" alias=""></typeAlias>
        <typeAlias type="" alias=""></typeAlias>
        <typeAlias type="" alias=""></typeAlias>
        ....
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/${数据库的名字}"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <!--实际开发过程中:这里可能有成千上万行指定映射器接口的代码那肯定完犊子,所以我们需要使用package机制-->
        <mapper class=""></mapper>
        <mapper class=""></mapper>
        <mapper class=""></mapper>
        .....
    </mappers>
</configuration>

下面是使用package机制的代码:

这里使用了包扫描机制:如果name属性值是“com.xyh”那么MyBatis会扫描“com.xyh”包以及“com.xyh”包的子包。

<?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>
    <typeAliases>
        <!--name属性指定的是包名,一旦指定包名之后,整个包下以及包下所有的子包所有的类全部都有别名了,其别名就是“简类名”-->
        <package name=""/>
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/${数据库的名字}"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <!--name属性指定的是包名,一旦指定包名之后整个包下以及包下所有的子包中的所有接口的实现将被注册为映射器
		实现原理:MyBatis首先去name属性规定的这个包中去寻找接口,找到某接口之后,去同包下找同名的SQL映射文件,
 如果找到了调用getMapper方法获取接口的代理类(映射器)的时候,MyBatis检测namespace属性是不是全限定接口名,如果是正常,如果不是则抛出异常
 如果没找到调用getMapper方法获取接口的代理类(映射器)的时候也不会报错,底层可能是MyBatis为其创建了一个SQL映射文件,这个映射文件的namespace就是全限定接口名-->
        <package name=""/>
    </mappers>
</configuration>
  1. 哪怕不使用MyBatis的接口代理机制,只要是mapper标签使用的是class属性或者是package标签那么:与接口同名同包的SQL映射文件的namespace必须是全限定接口名,如果不是抛出异常。

idea配置文件模板

mybatis-config.xml和SqlMapper.xml文件可以在IDEA中提前创建好模板,以后通过模板创建配置文件。

img

插入数据时获取自动生成的主键

  1. 前提是:主键是自动生成的。
  2. 第一种方式:可以先插入用户数据,再写一条查询语句获取id,然后再插入user_id字段。【比较麻烦】
  3. 第二种方式:mybatis提供了一种方式更加便捷。
//Mapper接口中的方法
void insertUseGeneratedKeys(Car car);
//SQL映射文件中共的SQL语句
/*useGeneratedKeys="true"表示使用生成的主键,keyProperty="id"表示生成的主键放到pojo对象的id属性中*/
<insert id="insertUseGeneratedKeys" useGeneratedKeys="true" keyProperty="id">
  insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
//测试程序
@Test
public void testInsertUseGeneratedKeys() {
    CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
    Car car = new Car();
    car.setCarNum("5262");
    car.setBrand("BYD汉");
    car.setGuidePrice(30.3);
    car.setProduceTime("2020-10-11");
    car.setCarType("新能源");
    mapper.insertUseGeneratedKeys(car);
    SqlSessionUtil.openSession().commit();
    System.out.println(car.getId());
}
在MyBatis中什么是映射器?

映射器就是Mapper接口的实现类。如果使用MyBatis的接口代理机制的话,这个实现类(映射器)是MyBatis帮我们实现的,我们只需要调用SqlSession对象的getMapper(Mapper接口的Class对象),MyBatis就可以帮我们把映射器创建出来
【UserMapper mapper = sqlSession.getMapper(UserMapper.class);】mapper映射器可以帮我们完成java对象和SQL语句之间的相互转换(映射)。

MyBatis参数处理

表:t_student

img

表中现有数据:

img

pojo类:

import java.util.Date;

public class Student {
    private Long id;
    private String name;
    private Integer age;
    private Double height;
    private Character sex;
    private Date birth;
    // constructor
    // setter and getter
    // toString
}

单个简单类型参数

  1. 简单类型包括:

    • byte short int long float double char
    • Byte Short Integer Long Float Double Character
    • String
    • java.util.Date
    • java.sql.Date
  2. MyBatis框架实际上内置了很多别名。可以参考开发手册。

  3. 需求:根据name查、根据id查、根据birth查、根据sex查

    import com.powernode.mybatis.pojo.Student;
    
    import java.util.Date;
    import java.util.List;
    
    //映射器接口
    public interface StudentMapper {
        /**
         * 根据name查询
         * @param name
         * @return
         */
        List<Student> selectByName(String name);
    
        /**
         * 根据id查询
         * @param id
         * @return
         */
        Student selectById(Long id);
    
        /**
         * 根据birth查询
         * @param birth
         * @return
         */
        List<Student> selectByBirth(Date birth);
    
        /**
         * 根据sex查询
         * @param sex
         * @return
         */
        List<Student> selectBySex(Character sex);
    }
    //SQL映射文件
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    //namespace必须是接口全限定名
    <mapper namespace="com.powernode.mybatis.mapper.StudentMapper">
        <select id="selectByName" resultType="student">
            select * from t_student where name = #{name}
        </select>
        <select id="selectById" resultType="student">
            select * from t_student where id = #{id}
        </select>
        <select id="selectByBirth" resultType="student">
            select * from t_student where birth = #{birth}
        </select>
        <select id="selectBySex" resultType="student">
            select * from t_student where sex = #{sex}
        </select>
    </mapper>
    //测试程序
    package com.powernode.mybatis.test;
    
    import com.powernode.mybatis.mapper.StudentMapper;
    import com.powernode.mybatis.pojo.Student;
    import com.powernode.mybatis.utils.SqlSessionUtil;
    import org.junit.Test;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.List;
    
    public class StudentMapperTest {
    
        StudentMapper mapper = SqlSessionUtil.openSession().getMapper(StudentMapper.class);
    
        @Test
        public void testSelectByName(){
            List<Student> students = mapper.selectByName("张三");
            students.forEach(student -> System.out.println(student));
        }
        @Test
        public void testSelectById(){
            Student student = mapper.selectById(2L);
            System.out.println(student);
        }
        @Test
        public void testSelectByBirth(){
            try {
                Date birth = new SimpleDateFormat("yyyy-MM-dd").parse("2022-08-16");
                List<Student> students = mapper.selectByBirth(birth);
                students.forEach(student -> System.out.println(student));
            } catch (ParseException e) {
                throw new RuntimeException(e);
            }
        }
        @Test
        public void testSelectBySex(){
            List<Student> students = mapper.selectBySex('男');
            students.forEach(student -> System.out.println(student));
        }
    }
    

    通过测试得知,简单类型对于mybatis来说都是可以自动类型识别的:

    也就是说对于mybatis来说,它是可以自动推断出ps.setXxxx()方法的。ps.setString()还是ps.setInt()。它可以自动推断。因为MyBatis肯定能通过接口的字节码文件拿到方法的参数的类型,通过方法中参数的类型确定调用哪个setXxx给占位符传值。

  4. 其实SQL映射文件中的配置比较完整的写法是:

    <select id="selectByName" resultType="student" parameterType="java.lang.String">
      select * from t_student where name = #{name, javaType=String, jdbcType=VARCHAR}
    </select>
    

    其中sql语句中的javaType,jdbcType,以及select标签中的parameterType属性,都是用来帮助mybatis进行类型确定的。不过这些配置多数是可以省略的。因为mybatis它有强大的自动类型推断机制。

    • javaType:可以省略,用于指定占位符的这个数据在Java中的类型
    • jdbcType:可以省略,用于指定占位符的这个数据在jdbc中的类型
    • parameterType:可以省略,用于指定接口中方法的参数的类型

    如果参数只有一个的话,#{} 里面的内容就随便写了。对于 ${} 来说,注意加单引号。

Map参数

  1. 需求:根据name和age查询

    //映射器接口中的方法
    List<Student> selectByParamMap(Map<String,Object> paramMap);
    //SQL映射文件
    <select id="selectByParamMap" resultType="student">
        //#{nameKey}:大括号中写的是map集合中的key,取出来的是map集合中的value,到目前为止#{}和${}都是为了从map集合或者pojo对象中取数据
        //#{这里可以写中文}
      select * from t_student where name = #{nameKey} and age = #{ageKey}
    </select>
    //测试程序
    @Test
    public void testSelectByParamMap(){
        // 准备Map
        Map<String,Object> paramMap = new HashMap<>();
        paramMap.put("nameKey", "张三");
        paramMap.put("ageKey", 20);
    
        List<Student> students = mapper.selectByParamMap(paramMap);
        students.forEach(student -> System.out.println(student));
    }
    

    **这种方式是手动封装Map集合,将每个条件以key和value的形式存放到集合中。然后在使用的时候通过#{map集合的key}来取值。**目前map集合的key必须是String类型的数据,实际上MyBatis底层再处理多参数问题的时候底层的map集合的key也是String类型的,再#{这里}写的时候去掉String类型的数据的“双引号”

    实体类参数

    需求:插入一条Student数据

    //映射器接口中的方法
    int insert(Student student);
    //SQL映射文件中的SQL语句
    <insert id="insert">
      //#{name}:大括号中写的是pojo对象的属性名或者是getter方法去掉get首字母小写的英文单词。pojo对象中getter方法和属性只要有其中一个#{name}就可以取到值
      insert into t_student values(null,#{name},#{age},#{height},#{birth},#{sex})
    </insert>
    //测试程序
    @Test
    public void testInsert(){
        Student student = new Student();
        student.setName("李四");
        student.setAge(30);
        student.setHeight(1.70);
        student.setSex('男');
        student.setBirth(new Date());
        int count = mapper.insert(student);
        SqlSessionUtil.openSession().commit();
    }
    

    这里需要注意的是:#{} 里面写的是属性名字。这个属性名其本质上是:set/get方法名去掉set/get之后的名字。

多参数

  1. 需求:通过name和sex查询Student

  2. //映射器接口中的方法
    List<Student> selectByNameAndSex(String name, Character sex);
    //测试程序
    @Test
    public void testSelectByNameAndSex(){
        List<Student> students = mapper.selectByNameAndSex("张三", '女');
        students.forEach(student -> System.out.println(student));
    }
    //SQL映射文件中的SQL语句
    <select id="selectByNameAndSex" resultType="student">
      select * from t_student where name = #{name} and sex = #{sex}
    </select>
    
  3. 执行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rPVtHPU3-1685102541189)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230522171518541.png)]

    • 异常信息描述了:name参数找不到,可用的参数包括[arg1, arg0, param1, param2]
  4. 修改StudentMapper.xml配置文件:尝试使用[arg1, arg0, param1, param2]去参数

  5. <select id="selectByNameAndSex" resultType="student">
      <!--select * from t_student where name = #{name} and sex = #{sex}-->
      select * from t_student where name = #{arg0} and sex = #{arg1}
    </select>
    
    • select * from t_student where name = #{arg0} and sex = #{arg1},arg是从0开始的

    • select * from t_student where name = #{param1} and sex = #{param2},param是从1开始的

    • select * from t_student where name = #{arg0} and sex = #{param2}也可以这样混合着用

    • 运行结果:

      img

  6. 再次尝试修改StudentMapper.xml文件

    <select id="selectByNameAndSex" resultType="student">
      <!--select * from t_student where name = #{name} and sex = #{sex}-->
      <!--select * from t_student where name = #{arg0} and sex = #{arg1}-->
      <!--select * from t_student where name = #{param1} and sex = #{param2}-->
      select * from t_student where name = #{arg0} and sex = #{param2}
    </select>
    

    通过测试可以看到:

    • arg0 是第一个参数
    • param1是第一个参数
    • arg1 是第二个参数
    • param2是第二个参数
  7. 实现原理:实际上在mybatis底层会创建一个map集合,以arg0/param1为key,以方法上的参数值为value,例如以下代码:

    Map<String,Object> map = new HashMap<>();
    map.put("arg0", name);
    map.put("arg1", sex);
    map.put("param1", name);
    map.put("param2", sex);
    // 所以可以这样取值:#{arg0} #{arg1} #{param1} #{param2}
    // 其本质就是#{map集合的key}
    

    注意:**使用mybatis**3.4.2之前的版本时:要用#{0}和#{1}这种形式。

@Param注解(命名参数)

  1. 可以不用arg0 arg1 param1 param2吗?这个map集合的key我们自定义可以吗?当然可以。使用@Param注解即可。这样可以增强可读性。

  2. 需求:根据name和age查询

    //映射器接口中的方法,第一个参数key值定义为“name”,第二个参数key值定义为“age”
    List<Student> selectByNameAndAge(@Param(value="name") String name, @Param("age") int age);
    @Test
    public void testSelectByNameAndAge(){
        List<Student> stus = mapper.selectByNameAndAge("张三", 20);
        stus.forEach(student -> System.out.println(student));
    }
    <select id="selectByNameAndAge" resultType="student">
    //取出来的时候可以使用name和age也可以使用param1和param2,也可以混合着用,如果使用@Param注解之后就不能使用arg0...了。
    select * from t_student where name = #{name} and age = #{age}
    </select>
    
  3. 核心:@Param(“这里填写的其实就是map集合的key”)

  4. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OCxtVGww-1685102541190)(D:\Users\XueYingHao\MyBatis\老杜MyBatis\document\004-Param注解源码分析.png)]

  5. @Param源码分析

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xa7R7XxS-1685102541190)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230522171139630.png)]

  6. 如果映射器接口方法中参数只有一个的话

    1. 如果参数的类型是pojo对象或者map集合的话,#{这里}和${这里}就是pojo对象的属性名或者map集合的key。
    2. 如果参数的类型是简单类型的话,#{这里}和${这里}可以随便写。
  7. 但是只有一个参数的时候也可以使用@Param注解,一旦使用了这个注解:

    1. 如果参数类型是pojo对象或者map集合的话,#{这里}和${这里}就得写“@Param中的内容.pojo对象的属性名”或者“paramX.pojo对象的属性名”。
    2. 如果参数类型是一个简单类型的参数的话#{这里}和${这里}可以随便写。
  8. 如果有多个参数的话:多个参数其中有pojo类型或者map类型的参数那么可以使用:以下这几种方式区分开:

    1. #{@Param中的值.POJO对象的属性名}
    2. #{paramX.pojo对象的属性名}

MyBatis查询语句专题

返回Car

  1. 如果查询结果集是一条,用List用多个的容器去接收那没问题。
    如果查询结果集是多条,用Car类型的对象去接收会报错。

  2. 当查询的结果,有对应的实体类。并且查询结果只有一条的时:

    映射器接口文件

    public interface CarMapper {
        Car selectById(Long id);
    }
    

    SQL映射文件

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="com.powernode.mybatis.mapper.CarMapper">
        <select id="selectById" resultType="Car">
            select id,car_num carNum,brand,guide_price guidePrice,produce_time produceTime,car_type carType from t_car where id = #{id}
        </select>
    </mapper>
    

    测试程序:

    import com.powernode.mybatis.mapper.CarMapper;
    import com.powernode.mybatis.pojo.Car;
    import com.powernode.mybatis.utils.SqlSessionUtil;
    import org.junit.Test;
    
    public class CarMapperTest {
    
        @Test
        public void testSelectById(){
            CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
            Car car = mapper.selectById(35L);
            System.out.println(car);
        }
    }
    

    运行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tdzg3HI6-1685102541191)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230522175119712.png)]

返回List

查询结果是一条的话可以使用List集合接收吗?当然可以

//映射器接口文件
List<Car> selectByIdToList(Long id);

SQL映射文件

<select id="selectByIdToList" resultType="Car">
  select id,car_num carNum,brand,guide_price guidePrice,produce_time produceTime,car_type carType from t_car where id = #{id}
</select>

测试程序:

@Test
public void testSelectByIdToList(){
    CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
    List<Car> cars = mapper.selectByIdToList(35L);
    System.out.println(cars);
}

执行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7CnWapkm-1685102541191)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230522175255010.png)]

当查询的记录条数是多条的时候,必须使用集合接收。如果使用单个实体类接收会出现异常。

//映射器接口文件
List<Car> selectAll();

SQL映射文件

<select id="selectAll" resultType="Car">
  select id,car_num carNum,brand,guide_price guidePrice,produce_time produceTime,car_type carType from t_car
</select>

测试程序

@Test
public void testSelectAll(){
    CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
    List<Car> cars = mapper.selectAll();
    cars.forEach(car -> System.out.println(car));
}

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j7DvVwOc-1685102541191)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230522175422206.png)]

返回Map

  1. 当查询结果返回的数据,没有合适的实体类对应的话,可以采用Map集合来接收。查询结果字段名叫做map集合的key,字段值叫做map集合的value。

  2. 查询如果可以保证只有一条数据,则返回一个Map集合即可。

  3. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JvBAbDK4-1685102541192)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230522180844393.png)]

  4. 映射器接口文件

    Map<String, Object> selectByIdRetMap(Long id);
    
  5. SQL映射文件

    <!--resultMap="map",这是因为mybatis内置了很多别名。【参见mybatis开发手册】-->
    <select id="selectByIdRetMap" resultType="map">
      select id,car_num carNum,brand,guide_price guidePrice,produce_time produceTime,car_type carType from t_car where id = #{id}
    </select>
    
  6. 测试程序:

    @Test
    public void testSelectByIdRetMap(){
        CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
        Map<String,Object> car = mapper.selectByIdRetMap(35L);
        System.out.println(car);
    }
    
  7. 运行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lAsGN20R-1685102541192)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230522181059542.png)]

  8. 如果返回一个Map集合,可以将Map集合放到List集合中吗?当然可以,在这里map集合充当的就是pojo类。

  9. 如果返回多条记录的话,只采用单个Map接收,这样同样会出现异常,一个Map对应一条记录,这里我们可以把Map放到List集合当中。SQL映射文件中共的resultType类型指定的还是"map"【List集合中存放的数据】。

查询结果条数大于等于1条数据,则可以返回一个存储Map集合的List集合。List等同于List

  1. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2a4xghek-1685102541192)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230522181700884.png)]

  2. //映射器接口文件
    List<Map<String,Object>> selectAllRetListMap();
    

    SQL映射文件

    <select id="selectAllRetListMap" resultType="map">
      select id,car_num carNum,brand,guide_price guidePrice,produce_time produceTime,car_type carType from t_car
    </select>
    

    测试程序:

    @Test
    public void testSelectAllRetListMap(){
        CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
        List<Map<String,Object>> cars = mapper.selectAllRetListMap();
        System.out.println(cars);
    }
    

    运行结果:

    [
      {carType=燃油车, carNum=103, guidePrice=50.30, produceTime=2020-10-01, id=33, brand=奔驰E300L}, 
      {carType=电车, carNum=102, guidePrice=30.23, produceTime=2018-09-10, id=34, brand=比亚迪汉}, 
      {carType=燃油车, carNum=103, guidePrice=50.30, produceTime=2020-10-01, id=35, brand=奔驰E300L}, 
      {carType=燃油车, carNum=103, guidePrice=33.23, produceTime=2020-10-11, id=36, brand=奔驰C200},
      ......
    ]
    

返回Map<String,Map>

  1. 拿Car的id做key,以后取出对应的Map集合时更方便。

  2. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H3QMd3qP-1685102541193)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230522204639823.png)]

  3. 实现过程

    映射器接口中的方法:

    @MapKey("id") //这里必须加上MapKey注解,不加的话那么就MyBatis就会以为我们要返回一条数据,就按照返回一条数据的逻辑帮我们生成Mapper接口的代理类,如果返回多条数据,就会抛出异常
    //@MapKey("id") 中的id是数据库查询结果集中的字段名,并不是pojo对象的属性名,如果要是指定的查询结果集的列明不存在那么返回null,这个大map集合的key就是null。查出来多条记录,后面的记录会覆盖前面的记录。 
    Map<Long,Map<String,Object>> selectAllRetMap();
    

    SQL映射文件:

    <select id="selectAllRetMap" resultType="map">
      select id,car_num carNum,brand,guide_price guidePrice,produce_time produceTime,car_type carType from t_car
    </select>
    

    测试程序:

    @Test
    public void testSelectAllRetMap(){
        CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
        Map<Long,Map<String,Object>> cars = mapper.selectAllRetMap();
        System.out.println(cars);
    }
    

    运行结果:

    {
    64={carType=燃油车, carNum=133, guidePrice=50.30, produceTime=2020-01-10, id=64, brand=丰田霸道}, 
    66={carType=燃油车, carNum=133, guidePrice=50.30, produceTime=2020-01-10, id=66, brand=丰田霸道}, 
    67={carType=燃油车, carNum=133, guidePrice=50.30, produceTime=2020-01-10, id=67, brand=丰田霸道}, 
    69={carType=燃油车, carNum=133, guidePrice=50.30, produceTime=2020-01-10, id=69, brand=丰田霸道},
    ......
    }
    

resultMap结果映射

  1. 查询结果的列名和java对象的属性名对应不上怎么办?
    • 第一种方式:as 给列起别名
    • 第二种方式:使用resultMap进行结果映射
    • 第三种方式:是否开启驼峰命名自动映射(配置settings)

使用resultMap进行结果映射

//映射器接口中的方法
List<Car> selectAllByResultMap();

SQL映射文件

<!--
	resultMap
		id属性:这个结果映射的标识,作为select标签的resultMap属性值
		type:结果集要映射的类。可以使用别名。[查询结果要封装成什么类型的数据]
-->
<resultMap id="carResultMap" type="car">
  <!--对象的唯一标识,官方解释是:为了提高mybatis的性能。建议写上。-->
  <id property="id" column="id"/>
  <result property="carNum" column="car_num"/>
  <!--当属性名和数据库列名一致时,可以省略。但建议都写上。-->
  <!--javaType用来指定属性类型。jdbcType用来指定列类型。一般可以省略。-->
  <result property="brand" column="brand" javaType="string" jdbcType="VARCHAR"/>
  <result property="guidePrice" column="guide_price"/>
  <result property="produceTime" column="produce_time"/>
  <result property="carType" column="car_type"/>
</resultMap>

原来select标签使用的是“resultType属性”,如果查询结果集的字段名和pojo对象的属性名对不上那么可以使用“resultMap属性”

测试程序:

@Test
public void testSelectAllByResultMap(){
    CarMapper carMapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
    List<Car> cars = carMapper.selectAllByResultMap();
    System.out.println(cars);
}

是否开启驼峰命名自动映射

  1. 使用这种方式的前提是:属性遵循Java命名规范,数据库表名遵循SQL命名规范。

    1. java命名规范:首字母小写,后面每个单词首字母大写,遵循驼峰命名方式。
    2. SQL命名规范:全部小写,单词之间采用下划线分割。

    比如以下的对应关系:

    实体类中的属性名数据库表的列名
    carNumcar_num
    carTypecar_type
    produceTimeproduce_time

如何启用该功能,在mybatis-config.xml文件中进行配置:

<!--放在properties标签后面-->
<settings>
  <setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

映射器接口中的方法

List<Car> selectAllByMapUnderscoreToCamelCase();

SQL映射文件

<select id="selectAllByMapUnderscoreToCamelCase" resultType="Car">
  select * from t_car
</select>

这里还是接着用resultType就可以,因为开启驼峰命名自动映射之后,MyBatis知道我们的命名规则是标准的,它可以帮助我们自动完成字段到属性的映射,不需要我们再resultMap标签中写映射规则,所以这里不需要使用resultMap属性。

测试程序:

@Test
public void testSelectAllByMapUnderscoreToCamelCase(){
    CarMapper carMapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
    List<Car> cars = carMapper.selectAllByMapUnderscoreToCamelCase();
    System.out.println(cars);
}

返回总记录条数

  1. 需求:查询总记录条数

  2. 实现过程

    映射器接口中的方法:

    Long selectTotal();
    

    SQL映射文件

    <!--long是别名,可参考mybatis开发手册。-->
    <select id="selectTotal" resultType="long">
      select count(*) from t_car
    </select>
    

    测试程序:

    @Test
    public void testSelectTotal(){
        CarMapper carMapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
        Long total = carMapper.selectTotal();
        System.out.println(total);
    }
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d6obUh6E-1685102541193)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230522211746717.png)]

动态SQL

有的业务场景,需要sql语句进行动态拼接,例如:

  • 批量删除

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HsCS9EOF-1685102541194)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230523171004329.png)]

    delete from t_car where id in(1,2,3,4,5,6,.....这里的值是动态的,根据用户选择的id不同,值是不同的)
    
  • 多条件查询

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H8X9P387-1685102541194)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230523171047044.png)]

if标签

  1. 需求:多条件查询
    可能包括的条件:品牌(brand)、指导价格(guide_price)、汽车类型(car_type)

  2. 映射器接口文件

    package com.powernode.mybatis.mapper;
    
    import com.powernode.mybatis.pojo.Car;
    import org.apache.ibatis.annotations.Param;
    
    import java.util.List;
    
    public interface CarMapper {
    
        /**
         * 根据多条件查询Car
         * @param brand  品牌
         * @param guidePrice  指导价格
         * @param carType  汽车类型
         * @return
         */
        List<Car> selectByMultiCondition(@Param("brand") String brand, @Param("guidePrice") Double guidePrice, @Param("carType") String carType);
    }
    

    SQL映射文件

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="com.powernode.mybatis.mapper.CarMapper">
    
        <select id="selectByMultiCondition" resultType="car">
            select * from t_car where
    <!--
    	如果映射器接口中方法的参数是多个简单类型的参数的时候:
            <if test="方法的参数名 != null and 方法的参数名 != ''">
                数据库中的字段名 like "%"#{方法的参数名}"%"
            </if>
    	当映射器接口中的方法的参数是一个pojo类型对象的时候:
    		<if test="对象的属性名 != null and 对象的属性名 != ''">
                数据库中的字段名 like "%"#{对象的属性名}"%"
            </if>
    	当映射器接口中的方法的参数是一个map集合的时候:
            <if test="map集合的key != null and map集合的key != ''">
                数据库中的字段名 like "%"#{map集合的key}"%"
            </if>
    -->
           <!--如果用户传过来的汽车品牌不是null并且不是空字符串那么就将这个if标签中的sql语句拼接到“select * from t_car where”sql语句之后-->
            <if test="brand != null and brand != ''">
                brand like "%"#{brand}"%"
            </if>
            <if test="guidePrice != null and guidePrice != ''">
                and guide_price >= #{guidePrice}
            </if>
            <if test="carType != null and carType != ''">
                and car_type = #{carType}
            </if>
        </select>
    
    </mapper>
    
    1. if标签中的test标签是必须的。
    2. if标签中的test属性值必须是布尔类型的true或者false。
    3. 如果test是true,则if标签中的sql语句就会直接拼接。反之则不会拼接。
    4. test属性中可以使用的是:
      1. 当使用了@Param注解的时候,那么test中出现的是@Param注解指定的参数名。比如再本例中的第一个条件是汽车品牌“brand”使用@Param(“brand”),那么这里只能使用brand,因为他是第一个参数所以还可以使用“param1”,但是一旦使用了@Param就不能使用“arg0”
        • 虽然说有且仅有一个参数的时候KaTeX parse error: Expected 'EOF', got '#' at position 6: {这里}和#̲{这里}可以随便写,但是如果使…{这里}和#{这里}就只能写@Param中的内容或者“paramX”,X代表第几个参数。
      2. 当没有使用@Param注解,那么test中要出现的是:param1、param2、param3、arg0、arg1、arg2…
      3. 当使用了pojo,test中出现的是pojo类的属性名。
      4. 再MyBatis的动态SQL当中,不能使用&&,只能使用“and”,这里可以使用的逻辑运算符如下:
        • and
        • or 、|、||

    测试程序:

    package com.powernode.mybatis.test;
    
    import com.powernode.mybatis.mapper.CarMapper;
    import com.powernode.mybatis.pojo.Car;
    import com.powernode.mybatis.utils.SqlSessionUtil;
    import org.junit.Test;
    
    import java.util.List;
    
    public class CarMapperTest {
        @Test
        public void testSelectByMultiCondition(){
            CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
            List<Car> cars = mapper.selectByMultiCondition("丰田", 20.0, "燃油车");
            System.out.println(cars);
        }
    }
    

    运行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4tb44zV1-1685102541194)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230523184952746.png)]

  3. 如果第一个条件为空,剩下两个条件不为空,会是怎样呢?

    List<Car> cars = mapper.selectByMultiCondition("", 20.0, "燃油车");
    

    执行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-499KhLZf-1685102541195)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230523185037864.png)]

    报错了,SQL语法有问题,where后面出现了and。这该怎么解决呢?

    • 可以在where后面添加一个恒成立的条件。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NZBUKy5X-1685102541196)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230523185357734.png)]

    执行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GdGGVDoQ-1685102541196)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230523185415194.png)]

where标签

  1. 因为只有if标签的话,总是需要在where后面添加一个恒成立的条件【比如上例中的1=1】,所以出现了where标签,where标签的作用:让where子句更加动态智能。

    • 所有条件都为空的时,where标签保证不会生成where字句。
    • 自动去除某些条件前面多余的and或者or
  2. 需求:多条件查询:查询的条件是:汽车品牌、指导价格、汽车类型

  3. 映射器接口中的方法:

    List<Car> selectByMultiConditionWithWhere(@Param("brand") String brand, @Param("guidePrice") Double guidePrice, @Param("carType") String carType);
    

    SQL映射文件

    <select id="selectByMultiConditionWithWhere" resultType="car">
      select * from t_car
      <!--当where标签中的if语句没有一个需要拼接到上面的sql语句后面的话,也就是所有的if标签都不成立那么where就不会生成where关键字了-->
      <where>
        <!--如果这条if语句是第一个成立的sql语句那么where标签将自动去除前面多余的and,但是如果sql语句后面又and或者or他不会去除-->
        <if test="brand != null and brand != ''">
          and brand like #{brand}"%"
        </if>
        <if test="guidePrice != null and guidePrice != ''">
          and guide_price >= #{guidePrice}
        </if>
        <if test="carType != null and carType != ''">
          and car_type = #{carType}
        </if>
      </where>
    </select>
    

    测试程序:

    @Test
    public void testSelectByMultiConditionWithWhere(){
        CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
        List<Car> cars = mapper.selectByMultiConditionWithWhere("丰田", 20.0, "燃油车");
        System.out.println(cars);
    }
    

    运行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fr5FOgij-1685102541197)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230523202322679.png)]

  4. 如果所有条件都是空呢?

    List<Car> cars = mapper.selectByMultiConditionWithWhere("", null, "");
    

    运行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WxNiAJXE-1685102541198)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230523202352874.png)]

  5. 它可以自动去掉前面多余的and,那可以自动去掉后面多余的and吗?

    <select id="selectByMultiConditionWithWhere" resultType="car">
      select * from t_car
      <where>
        <if test="brand != null and brand != ''">
          brand like #{brand}"%" and
        </if>
        <if test="guidePrice != null and guidePrice != ''">
          guide_price >= #{guidePrice} and
        </if>
        <if test="carType != null and carType != ''">
          car_type = #{carType}
        </if>
      </where>
    </select>
    // 让最后一个条件为空
    List<Car> cars = mapper.selectByMultiConditionWithWhere("丰田", 20.0, "");
    

    运行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DfzkXPZo-1685102541199)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230523202449355.png)]

    很显然他不会去除后面多余的and或者or

trim标签

  1. 因为where标签只能去掉sql语句前面多余的and或者or,但是不能去除sql语句后面多余的and或者or,where标签可以动态的添加where字句。

    • trim标签既可以去掉sql语句前面多余的and或者or也可以去掉后面多余的and或者or,而且还可以根据需要动态的在前面或者后面动态的添加and或者or等关键字。
  2. trim标签的属性:下面写四个属性都是对整个trim中的内容进行操作的,并不是某一个if分支或者是其他分支,是对整个trim标签中的内容进行操作的。

    • prefix:在trim标签中sql语句之前添加内容
    • suffix:在trim标签中的sql语句之后添加内容
    • prifixOverrides:智能判断是否需要去掉某些前缀
    • suffixOverrides:智能判断是否需要去掉某些后缀
  3. 映射器接口文件:

    List<Car> selectByMultiConditionWithTrim(@Param("brand") String brand, @Param("guidePrice") Double guidePrice, @Param("carType") String carType);
    

    SQL映射文件

    <select id="selectByMultiConditionWithTrim" resultType="car">
      select * from t_car
    <!--prefix="where" 如果trim标签中的内容不为空,是在trim标签所有内容前面添加上 where,如果trim标签中的内容为空那么where自动不加-->
    <!--suffixOverrides="and|or":如果trim标签中的内容不为空,并且sql语句后面有多余的and或者or那么就会自动去掉-->
      <trim prefix="where" suffixOverrides="and|or">
        <if test="brand != null and brand != ''">
          brand like #{brand}"%" and
        </if>
        <if test="guidePrice != null and guidePrice != ''">
          guide_price >= #{guidePrice} and
        </if>
        <if test="carType != null and carType != ''">
          car_type = #{carType}
        </if>
      </trim>
    </select>
    

    测试程序:

    @Test
    public void testSelectByMultiConditionWithTrim(){
        CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
        List<Car> cars = mapper.selectByMultiConditionWithTrim("丰田", 20.0, "");
        System.out.println(cars);
    }
    

    运行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R5B9pfq7-1685102541199)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230523210315849.png)]

  4. 如果所有条件为空,where会被加上吗?

    List<Car> cars = mapper.selectByMultiConditionWithTrim("", null, "");
    

    执行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P3bso0jJ-1685102541200)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230523210353944.png)]

set标签

  1. 主要使用在update语句当中,用来生成set关键字,同时去掉整个set标签中的内容最后多余的“,”,或者整个set标签中所有的内容最前面多余的“,”

  2. 比如我们只更新前端提交的不为空的字段,如果提交的数据是空或者"",那么这个字段我们将不更新。

  3. 映射器接口的中的方法:

    int updateWithSet(Car car);
    

    SQL映射文件:

    <update id="updateWithSet">
      update t_car
    <!--如果set标签中的内容不为空,那么自动生成set关键字,并且去除整个set标签中的内容后面多余的“,”-->
    <!--如果set标签中的内容为空,那么也不会为我们生成SET关键字,但是这条SQL语句也是错误的-->
      <set>
        <if test="carNum != null and carNum != ''">car_num = #{carNum},</if>
        <if test="brand != null and brand != ''">brand = #{brand},</if>
        <if test="guidePrice != null and guidePrice != ''">guide_price = #{guidePrice},</if>
        <if test="produceTime != null and produceTime != ''">produce_time = #{produceTime},</if>
        <if test="carType != null and carType != ''">car_type = #{carType},</if>
      </set>
      where id = #{id}
    </update>
    

    测试程序:

    @Test
    public void testUpdateWithSet(){
        CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
        Car car = new Car(38L,"1001","丰田霸道2",10.0,"",null);
        int count = mapper.updateWithSet(car);
        System.out.println(count);
        SqlSessionUtil.openSession().commit();
    }
    

    执行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ANlMo2lV-1685102541200)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230523212430864.png)]

choose when otherwise标签

  1. choose、when、otherwise这三个标签实在一起使用的:

    <choose>
    	<when></when>
        <when></when>
    	<when></when>
        <otherwise></otherwise>
    </choose>
    <!--也可以是下面的两种形式-->
    <choose>
    	<when></when>
        <when></when>
    	<when></when>
    </choose>
    
    <choose>
        <otherwise></otherwise>
    </choose>
    

    等同于:

    if(){
        
    }else if(){
        
    }else if(){
        
    }else if(){
        
    }else{
    
    }
    
  2. 只有一个分支会被选择!!!

  3. 需求:先根据品牌查询,如果没有提供品牌,再根据指导价格查询,如果没有提供指导价格,就根据生产日期查询。

  4. 映射器接口中的方法 :

    /**
    * 使用choose when otherwise标签查询
    * @param brand 汽车品牌
    * @param guidePrice 指导价格
    * @param produceTime 生产日期
    * @return 符合条件的汽车列表
    */
    List<Car> selectWithChoose(@Param("brand") String brand, @Param("guidePrice") Double guidePrice, @Param("produceTime") String produceTime);
    

    SQL映射文件:

    <select id="selectWithChoose" resultType="car">
      select * from t_car
      <where>
        <choose>
          <when test="brand != null and brand != ''">
            brand like #{brand}"%"
          </when>
          <when test="guidePrice != null and guidePrice != ''">
            guide_price >= #{guidePrice}
          </when>
          <otherwise>
            produce_time >= #{produceTime}
          </otherwise>
        </choose>
      </where>
    </select>
    

    测试程序:

    @Test
    public void testSelectWithChoose(){
        CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
        //List<Car> cars = mapper.selectWithChoose("丰田霸道", 20.0, "2000-10-10");
        //List<Car> cars = mapper.selectWithChoose("", 20.0, "2000-10-10");
        //List<Car> cars = mapper.selectWithChoose("", null, "2000-10-10");
        List<Car> cars = mapper.selectWithChoose("", null, "");
        System.out.println(cars);
    }
    

    运行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ns4Lgtq-1685102541201)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230524120151213.png)]

foreach标签

  1. 循环数组或者集合,生成动态sql,比如这样的sql

    delete from t_car where id in(1,2,3);
    delete from t_car where id = 1 or id = 2 or id = 3;
    insert into t_car values
      (null,'1001','凯美瑞',35.0,'2010-10-11','燃油车'),
      (null,'1002','比亚迪唐',31.0,'2020-11-11','新能源'),
      (null,'1003','比亚迪宋',32.0,'2020-10-11','新能源')
    

批量删除

  1. 使用in来删除

映射器接口中的方法:

/**
* 通过foreach完成批量删除
* @param ids  需要删除的记录的id数组
* @return
*/
int deleteBatchByForeach(@Param("ids") Long[] ids);

SQL映射文件:

<delete id="deleteBatchByForEach">
    delete from t_car
    <where><!--如果where标签中有内容则生成where,并且去除前面多余的and或者or-->
  <!--如果trim标签中有内容则在trim标签中所有的内容之前加上“id in”-->
        <trim prefix="id in">
  <!--collection=“这里”写的是要遍历的集合或者数组,也就是映射器接口的中的方法的参数,
如果参数是个数组并且没有使用@Param注解那么collection属性值就是array或者arg0,如果使用了@Param注解那么collection里面的值就是param1或者@Param中的值
如果参数是个集合并且只有一个参数的话没有使用@Param注解:[arg0, collection, list]-->
            <foreach collection="ids" item="id" separator="," open="(" close=")">
                #{id}
            </foreach>
        </trim>
    </where>
</delete>
  1. collection:集合或者数组,一般是映射器接口中的方法的参数
  2. item:从集合或者数组中取出来的每一项
  3. separator:分隔符,遍历完每一次之后都会根据需要加一个分隔符,如果最后一项不需要添加分隔符,他会自动不添加
  4. open:在foreach标签中所有内容的开始添加一个内容
  5. close:在foreach标签中所有内容的结束位置添加内容
  6. index:下标

测试程序:

@Test
public void testDeleteBatchByForeach(){
    CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
    int count = mapper.deleteBatchByForeach(new Long[]{40L, 41L, 42L});
    System.out.println("删除了几条记录:" + count);
    SqlSessionUtil.openSession().commit();
}

执行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MaO98gZs-1685102541201)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230524125807034.png)]

  1. 用or来删除
int deleteBatchByForeach2(@Param("ids") Long[] ids);
<delete id="deleteBatchByForeach2">
  delete from t_car where
  <foreach collection="ids" item="id" separator="or">
    id = #{id}
  </foreach>
</delete>
@Test
public void testDeleteBatchByForeach2(){
    CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
    int count = mapper.deleteBatchByForeach2(new Long[]{40L, 41L, 42L});
    System.out.println("删除了几条记录:" + count);
    SqlSessionUtil.openSession().commit();
}

执行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2AJk9rGT-1685102541202)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230524125919266.png)]

批量添加

/**
* 批量添加,使用foreach标签
* @param cars
* @return
*/
int insertBatchByForeach(@Param("cars") List<Car> cars);
<insert id="insertBatchByForeach">
  insert into t_car values 
  <foreach collection="cars" item="car" separator=",">
    (null,#{car.carNum},#{car.brand},#{car.guidePrice},#{car.produceTime},#{car.carType})
  </foreach>
</insert>
@Test
public void testInsertBatchByForeach(){
    CarMapper mapper = SqlSessionUtil.openSession().getMapper(CarMapper.class);
    Car car1 = new Car(null, "2001", "兰博基尼", 100.0, "1998-10-11", "燃油车");
    Car car2 = new Car(null, "2001", "兰博基尼", 100.0, "1998-10-11", "燃油车");
    Car car3 = new Car(null, "2001", "兰博基尼", 100.0, "1998-10-11", "燃油车");
    List<Car> cars = Arrays.asList(car1, car2, car3);
    int count = mapper.insertBatchByForeach(cars);
    System.out.println("插入了几条记录" + count);
    SqlSessionUtil.openSession().commit();
}

执行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o6kfd5wC-1685102541202)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230524130015596.png)]

总结参数:

  1. 映射器接口中的参数:不管是简单类型的还是pojo对象还是map集合,还是list集合,还是数组,都遵循以下规矩:
    1. 一旦使用了@Param注解的话,#{这里}和${这里}就只能写@Param和paramX这样的东西了。
    2. 如果没有使用@Param注解的话:
      1. 如果是多个参数:
        1. #{这里}和${这里}可以写arg0、arg1、param0、param1…
      2. 如果是一个参数:
        1. 参数类型是集合
        2. 参数的类型是数组
          • #{这里}和${这里}可以写:[arg0, array]
        3. 参数的类型是map集合
          • #{这里}和${这里}可以下:集合的key
        4. 参数的类型是list集合
          • #{这里}和${这里}可以写:[arg0, collection, list]
        5. 参数的类型是pojo对象
          • #{这里}和${这里}可以写:对象的属性名

sql标签与include标签

  1. sql标签用来生命sql片段

  2. include标签用来将声明的sql片段包含到某个sql语句当中

  3. 作用:代码复用。容易维护。

    <sql id="carCols">id,car_num carNum,brand,guide_price guidePrice,produce_time produceTime,car_type carType</sql>
    
    <select id="selectAllRetMap" resultType="map">
      select <include refid="carCols"/> from t_car
    </select>
    
    <select id="selectAllRetListMap" resultType="map">
      select <include refid="carCols"/> carType from t_car
    </select>
    
    <select id="selectByIdRetMap" resultType="map">
      select <include refid="carCols"/> from t_car where id = #{id}
    </select>
    

MyBatis的高级映射及延迟加载

准备数据库表:一个班级对应多个学生。班级表:t_clazz。学生表:t_student

创建pojo类student类和class类

package com.powernode.mybatis.pojo;

/**
 * 学生类
 * @author 老杜
 * @version 1.0
 * @since 1.0
 */
public class Student {
    private Integer sid;
    private String sname;
    //......
}
package com.powernode.mybatis.pojo;

/**
 * 班级类
 * @author 老杜
 * @version 1.0
 * @since 1.0
 */
public class Clazz {
    private Integer cid;
    private String cname;
    //......
}

创建mapper接口:StudentMapper、ClazzMapper

创建mapper映射文件:StudentMapper.xml、ClazzMapper.xml

多对一映射

  1. 多对一映射MyBatis常见的有以下三种处理方式:
    1. 第一种:一条SQL语句,级联属性映射。
    2. 第二种:一条SQL语句,association。
    3. 第三种:两条SQL语句,分步查询。

第一种方式:级联属性映射

  1. pojo类Student中添加一个属性:Clazz clazz;表示学生关联的班级对象。

    public class Student {
        private Integer sid;
        private String sname;
        private Clazz clazz;
    	//......
    }
    

    SQL映射文件

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="com.powernode.mybatis.mapper.StudentMapper">
    <!--
    MyBatis高级映射必须使用resultMap标签规定映射关系,不能使用简单的resultType属性规定返回类型就好。
    column属性:指定的都是查询结果集的字段名
    property属性:指定的都是java对象的属性名
    <result property="clazz.cid" column="cid"/>代表Student对象的clazz属性是另一个java对象,“clazz.cid”表示另一个Java对象的cid属性。
    -->
        <resultMap id="studentResultMap" type="Student">
            <id property="sid" column="sid"/>
            <result property="sname" column="sname"/>
            <result property="clazz.cid" column="cid"/>
            <result property="clazz.cname" column="cname"/>
        </resultMap>
    
        <select id="selectBySid" resultMap="studentResultMap">
            select s.*, c.* from t_student s join t_clazz c on s.cid = c.cid where sid = #{sid}
        </select>
    
    </mapper>
    

    测试程序:

    package com.powernode.mybatis.test;
    
    import com.powernode.mybatis.mapper.StudentMapper;
    import com.powernode.mybatis.pojo.Student;
    import com.powernode.mybatis.utils.SqlSessionUtil;
    import org.junit.Test;
    
    public class StudentMapperTest {
        @Test
        public void testSelectBySid(){
            StudentMapper mapper = SqlSessionUtil.openSession().getMapper(StudentMapper.class);
            Student student = mapper.selectBySid(1);
            System.out.println(student);
        }
    }
    

    运行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dnfLaTsB-1685102541202)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230524193000725.png)]

第二种方式:association

  1. 其他位置都不需要修改,只需要修改resultMap中的配置:association即可。

    <resultMap id="studentResultMap" type="Student">
      <id property="sid" column="sid"/>
      <result property="sname" column="sname"/>
      <!--
    	property:提供要映射的pojo类的属性名【被关联的对象的属性名】
    	JavaType:提供被关联的pojo对象的类型【这个属性可以省略,MyBatis可以做类型自动推断】
      -->
      <association property="clazz" javaType="Clazz">
        <id property="cid" column="cid"/>
        <result property="cname" column="cname"/>
      </association>
    </resultMap>
    
    1. association:翻译为关联。一个Student对象关联一个Clazz对象。

第三种方式:分步查询

  1. 其他位置不需要修改,只需要修改以及添加以下三处:

  2. 第一处:association中的select位置填写sqlid。sqlid=namespace+id。其中column属性作为这条子sql语句的条件。

    <resultMap id="studentResultMap" type="Student">
      <id property="sid" column="sid"/>
      <result property="sname" column="sname"/>
      <association property="clazz"
                   select="com.powernode.mybatis.mapper.ClazzMapper.selectByCid"
                   column="cid"/>
    </resultMap>
    
    <select id="selectBySid" resultMap="studentResultMap">
      select s.* from t_student s where sid = #{sid}
    </select>
    
  3. 第二处:在ClazzMapper接口中添加方法

    public interface ClazzMapper {
    
        /**
         * 根据cid获取Clazz信息
         * @param cid
         * @return
         */
        Clazz selectByCid(Integer cid);
    }
    
  4. 第三处:在ClazzMapper.xml文件中进行配置

    <mapper namespace="com.powernode.mybatis.mapper.ClazzMapper">
        <select id="selectByCid" resultType="Clazz">
            select * from t_clazz where cid = #{cid}
        </select>
    </mapper>
    
  5. 执行结果,可以很明显看到先后有两条sql语句执行:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2N0Fporf-1685102541203)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230525111200259.png)]

  6. 分步的优点:

    1. 代码复用性增强。
    2. 支持延迟加载。【暂时访问不到的数据可以先不查询。提高程序的执行效率】

多对一延迟加载

  1. 什么是延迟加载?

    • 用到的时候在执行sql语句,暂时用不到的数据先不查询,提高程序的执行效率。
  2. 想要支持延迟加载,非常简单。只需要在association标签中添加fetchType="lazy"即可。

  3. 修改StudentMapper.xml文件:

    <resultMap id="studentResultMap" type="Student">
      <id property="sid" column="sid"/>
      <result property="sname" column="sname"/>
      <association property="clazz" select="com.powernode.mybatis.mapper.ClazzMapper.selectByCid"
                   column="cid"
                   fetchType="lazy"/>
    </resultMap>
    
    1. fetchType属性有两个属性值:

      1. lazy:打开懒加载,默认情况下是没有开启懒加载的。
      2. eager:关闭懒加载。
    2. 在association标签中配置的fetchType=“lazy”,是局部的设置,只是对当前的association关联的sql语句起作用。

    3. 在实际开发中大部分是需要进行延迟加载的,所以建议开启全局的延迟加载机制:在MyBatis核心配置文件中添加全局配置:

      <settings>
          <!--延迟加载全局开关,默认值为false不开启-->
          <!--所有但凡有分布查询的,都采用延迟加载-->
          <setting name="lazyLoadingEnabled" value="true"/>
      </settings>
      
    4. 实际开发中把全局的延迟加载打开,如果某一步不需要进行延迟加载,请在association标签中设置:fetchType=“eager”

  4. 我们现在只查询学生名字,修改测试程序:

    public class StudentMapperTest {
        @Test
        public void testSelectBySid(){
            StudentMapper mapper = SqlSessionUtil.openSession().getMapper(StudentMapper.class);
            Student student = mapper.selectBySid(1);
            //System.out.println(student);
            // 只获取学生姓名
            String sname = student.getSname();
            System.out.println("学生姓名:" + sname);
        }
    }
    

    运行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KF60quab-1685102541203)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230525130018230.png)]

  5. 如果后续需要使用到学生所在班级的名称,这个时候才会执行关联的sql语句,修改测试程序:

    public class StudentMapperTest {
        @Test
        public void testSelectBySid(){
            StudentMapper mapper = SqlSessionUtil.openSession().getMapper(StudentMapper.class);
            Student student = mapper.selectBySid(1);
            //System.out.println(student);
            // 只获取学生姓名
            String sname = student.getSname();
            System.out.println("学生姓名:" + sname);
            // 到这里之后,想获取班级名字了
            String cname = student.getClazz().getCname();
            System.out.println("学生的班级名称:" + cname);
        }
    }
    

    运行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T00NHF1e-1685102541204)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230525130044031.png)]

  6. 通过以上的执行结果可以看到,只有当使用到班级名称之后,才会执行关联的sql语句,这就是延迟加载。

  7. 在mybatis中如何开启全局的延迟加载呢?需要setting配置,如下:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E575To5S-1685102541205)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230525130128433.png)]

一对多

  1. 一对多的实现,通常是在一的一方中有一个List集合属性。

  2. 在Clazz类中添加List stus;属性

    public class Clazz {
        private Integer cid;
        private String cname;
        private List<Student> stus;
        // set get方法
        // 构造方法
        // toString方法
    }
    
  3. 一对多的实现通常包括两种方式:

    1. 第一种方式:collection
    2. 第二种方式:分步查询【常用的】

第一种方式:collection

  1. 映射器接口中的方法:

    public interface ClazzMapper {
    
        /**
         * 根据cid获取Clazz信息
         * @param cid
         * @return
         */
        Clazz selectByCid(Integer cid);
    
        /**
         * 根据班级编号查询班级信息。同时班级中所有的学生信息也要查询。
         * @param cid
         * @return
         */
        Clazz selectClazzAndStusByCid(Integer cid);
    }
    

    SQL映射文件:

    <resultMap id="clazzResultMap" type="Clazz">
      <id property="cid" column="cid"/>
      <result property="cname" column="cname"/>
      <!--一对多这里是collection。collection是集合的意思【多对一是association】-->
      <!--ofType属性用来规定集合当中的元素的类型-->
      <collection property="stus" ofType="Student">
        <id property="sid" column="sid"/>
        <result property="sname" column="sname"/>
      </collection>
    </resultMap>
    <select id="selectClazzAndStusByCid" resultMap="clazzResultMap">
      select * from t_clazz c join t_student s on c.cid = s.cid where c.cid = #{cid}
    </select>
    
    1. collection:中使用ofType属性规定集合中的元素类型
    2. association:中使用javaType属性规定封装成什么类型的java对象。

    测试程序:

    @Test
    public void testSelectClazzAndStusByCid() {
        ClazzMapper mapper = SqlSessionUtil.openSession().getMapper(ClazzMapper.class);
        Clazz clazz = mapper.selectClazzAndStusByCid(1001);
        System.out.println(clazz);
    }
    

    执行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ImISNMPg-1685102541205)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230525154218889.png)]

第二种方式:分步查询

  1. 修改以下三个位置即可:

    <resultMap id="clazzResultMap" type="Clazz">
      <id property="cid" column="cid"/>
      <result property="cname" column="cname"/>
      <!--主要看这里-->
      <collection property="stus"
                  select="com.powernode.mybatis.mapper.StudentMapper.selectByCid"
                  column="cid"/>
    </resultMap>
    
    <!--sql语句也变化了-->
    <select id="selectClazzAndStusByCid" resultMap="clazzResultMap">
      select * from t_clazz c where c.cid = #{cid}
    </select>
    
  2. StudentMapper.java文件中的方法

    /**
    * 根据班级编号获取所有的学生。
    * @param cid
    * @return
    */
    List<Student> selectByCid(Integer cid);
    

    StudentMapper.xml文件中的sql语句

    <select id="selectByCid" resultType="Student">
      select * from t_student where cid = #{cid}
    </select>
    

    执行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jkFDLKwQ-1685102541206)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230525155602064.png)]

一对多延迟加载

  1. 一对多延迟加载和多对一是一样的:同样是通过两种方式:
    1. 第一种:fetchType=“lazy”
    2. 第二种:修改全局配置setting,lazyLoadingEnabled=true,如果开启全局的延迟加载,但是不想让某个sql使用延时加载可以添加局部的“fetchType=eager”

总结:

  1. 查询多个元素就是用collection标签
  2. 查询一个元素就是用association标签
  3. result、id、association、collection都有property属性,都是pojo对象的属性名。都有column属性,colunm属性规定的是查询结果集的字段名。
  4. 查询结果封装成什么对象都是用什么属性来指定?
    1. resultMap:type
    2. association:javaType
    3. collection:ofTyep

MyBatis的缓存

  1. 缓存:cache
  2. 缓存的作用:通过减少IO的方式,来提高程序的执行效率。
  3. MyBatis的缓存:将select语句的查询结果放到缓存(内存)当中,下一次还是这条select语句的话,直接从缓存中取,不在查找数据库。一方面是减少IO,另一方面不再执行繁琐的查找算法,执行效率大大提升。
    • 如果第一次查询将数据缓存到了内存中,接下来有一条DML语句更改了数据库中的数据,第二次使用同样的select语句进行查询的时候,就不再从缓存中取数据了,因为一旦执行一条DML语句,MyBatis就会自动帮我们把缓存清空。
  4. MyBatis缓存包括:
    1. 一级缓存:将查询到的数据存储到SqlSession中。
    2. 二级缓存:将查询到的数据封装到SqlSessionFactory中。
    3. 或者集成第三方的缓存:比如EhCache【Java语言开发的】、Memcache【C语言开发的】等。
  5. 缓存值针对于DQL语句,也就是缓存只针对于select语句

一级缓存(存储在SqlSession对象中)

  1. 一级缓存默认是开启的。不需要做任何配置。

  2. 原理:只要使用相同的SqlSession对象执行同一条SQL语句,就会走缓存。

  3. 映射器接口中的方法:

    import com.powernode.mybatis.pojo.Car;
    
    public interface CarMapper {
    
        /**
         * 根据id获取Car信息。
         * @param id
         * @return
         */
        Car selectById(Long id);
    }
    

    SQL映射文件

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="com.powernode.mybatis.mapper.CarMapper">
    
      <select id="selectById" resultType="Car">
        select * from t_car where id = #{id}
      </select>
    
    </mapper>
    

    测试程序:

    @Test
        public void testSelectById() throws Exception{
            // 注意:不能使用我们封装的SqlSessionUtil工具类。
            SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
            SqlSessionFactory sqlSessionFactory = builder.build(Resources.getResourceAsStream("mybatis-config.xml"));
    
            SqlSession sqlSession1 = sqlSessionFactory.openSession();
    
            CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class);
            Car car1 = mapper1.selectById(83L);
            System.out.println(car1);
    
            CarMapper mapper2 = sqlSession1.getMapper(CarMapper.class);
            Car car2 = mapper2.selectById(83L);
            System.out.println(car2);
    	   //不是同一个SqlSession对象,是不同的缓存区域
            SqlSession sqlSession2 = sqlSessionFactory.openSession();
    
            CarMapper mapper3 = sqlSession2.getMapper(CarMapper.class);
            Car car3 = mapper3.selectById(83L);
            System.out.println(car3);
    
            CarMapper mapper4 = sqlSession2.getMapper(CarMapper.class);
            Car car4 = mapper4.selectById(83L);
            System.out.println(car4);
    
        }
    

    运行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C1kAQXve-1685102541206)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230525203839608.png)]

  4. 什么情况下不走缓存?

    1. 第一种:不同的SqlSession对象。
    2. 第二种:不同的sql语句。
  5. 一级缓存失效包括两种方式:

    1. 第一种:两次查询之间手动清空了一级缓存。

      sqlSession.clearCache();
      
    2. 第二种:两次查询之间执行了DML语句。【执行DML语句跟那张表没关系只要是DML语句执行了就会清空一级缓存】

二级缓存(存储在SqlSessionFactory对象中)

  1. 二级缓存的范围是:SqlSessionFactory。

  2. 使用二级缓存需要具备以下四个条件:

    1. 全局性地开启所有映射器配置文件中已配置的任何缓存。默认就是true,开启状态,无需配置。

      <settings>
          <setting name="cacheEnabled" value="true"/>
      </settings>
      
    2. 在需要使用二级缓存的SqlMapper.xml文件中添加配置:

    3. 使用二级缓存的实体类必须是可序列化的,也就是必须实现java.io.Serializable接口

    4. SqlSession对象关闭或者提交之后,一级缓存中的数据才会被写入二级缓存。此时二级缓存才可以使用。

  3. 测试二级缓存:

    SQL映射文件中添加cache标签

    <cache></cache>
    

    pojo类实现可序列化接口

    public class Car implements Serializable {
    	//......
    }
    

    测试程序:

    @Test
    public void testSelectById2() throws Exception{
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
    
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class);
        Car car1 = mapper1.selectById(83L);
        System.out.println(car1);
    
        // 关键一步,关闭了之后才会放到二级缓存当中
        sqlSession1.close();
    
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        CarMapper mapper2 = sqlSession2.getMapper(CarMapper.class);
        Car car2 = mapper2.selectById(83L);
        System.out.println(car2);
    }
    

    运行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GmXQcPLD-1685102541207)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230525205110785.png)]

  4. 二级缓存的失效:只要两次查询之间出现了增删改操作。二级缓存就会失效。【一级缓存也会失效】

  5. 二级缓存的相关配置:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lHVS6kWf-1685102541207)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230525205215343.png)]

    1. eviction:指定从缓存中移除某个对象的淘汰算法。默认采用LRU策略。
      1. LRU:Least Recently Used。最近最少使用。优先淘汰在间隔时间内使用频率最低的对象。(其实还有一种淘汰算法LFU,最不常用。)
      2. FIFO:First In First Out。一种先进先出的数据缓存器。先进入二级缓存的对象最先被淘汰。
      3. SOFT:软引用。淘汰软引用指向的对象。具体算法和JVM的垃圾回收算法有关。
      4. WEAK:弱引用。淘汰弱引用指向的对象。具体算法和JVM的垃圾回收算法有关。
    2. flushInterval:二级缓存的刷新时间间隔。单位毫秒。如果没有设置。就代表不刷新缓存,只要内存足够大,一直会向二级缓存中缓存数据。除非执行了增删改。
    3. readOnly:
      1. true:多条相同的sql语句执行之后返回的对象是共享的同一个。性能好。但是多线程并发可能会存在安全问题。
      2. false:多条相同的sql语句执行之后返回的对象是副本,调用了clone方法。性能一般。但安全。
    4. size:
      1. 设置二级缓存中最多可存储的java对象数量。默认值1024。

MyBatis集成EhCache(了解)

  1. 集成EhCache是为了代替mybatis自带的二级缓存。一级缓存是无法替代的。

  2. mybatis对外提供了接口,也可以集成第三方的缓存组件。比如EhCache、Memcache等。都可以。

  3. EhCache是Java写的。Memcache是C语言写的。所以mybatis集成EhCache较为常见,按照以下步骤操作,就可以完成集成:

  4. 第一步:引入mybatis整合ehcache的依赖。

    <!--mybatis集成ehcache的组件-->
    <dependency>
      <groupId>org.mybatis.caches</groupId>
      <artifactId>mybatis-ehcache</artifactId>
      <version>1.2.2</version>
    </dependency>
    

    第二步:在类的根路径下新建echcache.xml文件,并提供以下配置信息。

    <?xml version="1.0" encoding="UTF-8"?>
    <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
             updateCheck="false">
        <!--磁盘存储:将缓存中暂时不使用的对象,转移到硬盘,类似于Windows系统的虚拟内存-->
        <diskStore path="e:/ehcache"/>
      
        <!--defaultCache:默认的管理策略-->
        <!--eternal:设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断-->
        <!--maxElementsInMemory:在内存中缓存的element的最大数目-->
        <!--overflowToDisk:如果内存中数据超过内存限制,是否要缓存到磁盘上-->
        <!--diskPersistent:是否在磁盘上持久化。指重启jvm后,数据是否有效。默认为false-->
        <!--timeToIdleSeconds:对象空闲时间(单位:秒),指对象在多长时间没有被访问就会失效。只对eternal为false的有效。默认值0,表示一直可以访问-->
        <!--timeToLiveSeconds:对象存活时间(单位:秒),指对象从创建到失效所需要的时间。只对eternal为false的有效。默认值0,表示一直可以访问-->
        <!--memoryStoreEvictionPolicy:缓存的3 种清空策略-->
        <!--FIFO:first in first out (先进先出)-->
        <!--LFU:Less Frequently Used (最少使用).意思是一直以来最少被使用的。缓存的元素有一个hit 属性,hit 值最小的将会被清出缓存-->
        <!--LRU:Least Recently Used(最近最少使用). (ehcache 默认值).缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存-->
        <defaultCache eternal="false" maxElementsInMemory="1000" overflowToDisk="false" diskPersistent="false"
                      timeToIdleSeconds="0" timeToLiveSeconds="600" memoryStoreEvictionPolicy="LRU"/>
    
    </ehcache>
    

    第三步:修改SqlMapper.xml文件中的标签,添加type属性。

    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
    

    第四步:编写测试程序使用。

    @Test
    public void testSelectById2() throws Exception{
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
        
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class);
        Car car1 = mapper1.selectById(83L);
        System.out.println(car1);
        
        sqlSession1.close();
        
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        CarMapper mapper2 = sqlSession2.getMapper(CarMapper.class);
        Car car2 = mapper2.selectById(83L);
        System.out.println(car2);
    }
    

    运行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9XD7cFGE-1685102541207)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230525205619742.png)]

MyBatis的逆向工程

  1. 逆向工程就是:根据数据库表逆向生成java的pojo类,SqlMapper.xml文件,以及Mapper接口文件。

  2. 要完成这个工作,需要借助别人写好的逆向工程插件。

  3. 在Maven中插件和依赖都有gav,插件和依赖有什么区别和关系?

    • 依赖(Dependencies)通常指的是项目运行时需要的外部库或框架,例如Apache Commons、Hibernate等。通过在pom.xml中配置依赖,Maven会自动下载相应的jar包并将其加入到项目的classpath中,以供编译和运行使用。依赖的GAV信息(groupId、artifactId、version)用来唯一标识一个依赖,Maven根据这些信息来确定依赖之间的关系和版本管理,以保证项目能够正确构建和运行。

      插件(Plugins)则是用来扩展Maven构建过程的工具,通常用于执行某些特定的任务,例如编译代码、打包成jar/war文件、运行测试、生成文档等等。插件同样需要在pom.xml中进行配置,并且也有对应的GAV信息来标识。不同的插件可以在不同的生命周期阶段(如compile、test、package等)被调用执行,以达到构建目标的效果。

      总体而言,依赖和插件都是通过GAV信息来进行标识和管理的,但二者的作用和影响范围是不同的。依赖用于程序运行时所需要的类库,而插件则扩展Maven构建过程的功能。同时,它们也有一定的关系,例如某些插件需要依赖特定的库才能正常运行,或者使用插件可以自动管理项目依赖等。

  4. 思考:使用这个插件的话,需要给这个插件配置哪些信息,插件才可以帮助我们逆向生成类?

    1. pojo类的类名、包名、以及生成位置
    2. SqlMapper.xml文件名,以及生成位置
    3. Mapper接口名以及生成位置
    4. 连接数据库的信息。
    5. 指定那些表参与逆向工程。

逆向工程配置与生成

第一步:基础环境准备

新建模块:mybatis-011-generator

打包方式jar

第二步:在pom中添加逆向工程的插件

<!--定制构建过程-->
<build>
  <!--可配置多个插件-->
  <plugins>
    <!--其中的一个插件:mybatis逆向工程插件-->
    <plugin>
      <!--插件的GAV坐标-->
      <groupId>org.mybatis.generator</groupId>
      <artifactId>mybatis-generator-maven-plugin</artifactId>
      <version>1.4.1</version>
      <!--允许覆盖-->
      <configuration>
        <overwrite>true</overwrite>
      </configuration>
      <!--插件的依赖-->
      <dependencies>
        <!--mysql驱动依赖-->
        <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>8.0.30</version>
        </dependency>
      </dependencies>
    </plugin>
  </plugins>
</build>

如果没有设置允许覆盖则生成的就是这种情况:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DakbJCk2-1685102541208)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230526150218610.png)]

第三步:配置generatorConfig.xml

该文件名是逆向工程插件的配置文件,该文件名必须叫做: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:生成的是基础版,只有基本的增删改查。
            MyBatis3:生成的是增强版,除了基本的增删改查之外还有复杂的增删改查。
    -->
    <context id="DB2Tables" targetRuntime="MyBatis3">
        <!--防止生成重复代码-->
        <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin"/>
      
        <commentGenerator>
            <!--是否去掉生成日期-->
            <property name="suppressDate" value="true"/>
            <!--是否去除注释-->
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>

        <!--连接数据库信息-->
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/powernode"
                        userId="root"
                        password="root">
        </jdbcConnection>

        <!-- 生成pojo包名和位置 -->
        <javaModelGenerator targetPackage="com.powernode.mybatis.pojo" targetProject="src/main/java">
            <!--是否开启子包-->
            <property name="enableSubPackages" value="true"/>
            <!--是否去除字段名的前后空白-->
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>

        <!-- 生成SQL映射文件的包名和位置 -->
        <sqlMapGenerator targetPackage="com.powernode.mybatis.mapper" targetProject="src/main/resources">
            <!--是否开启子包-->
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>

        <!-- 生成Mapper接口的包名和位置 -->
        <javaClientGenerator
                type="xmlMapper"
                targetPackage="com.powernode.mybatis.mapper"
                targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>

        <!--表名和对应的实体类名-->
        <table tableName="t_car" domainObjectName="Car"/>
        <!--如果有多个表则需要配置多个table标签-->
        
    </context>
</generatorConfiguration>

第四步:运行插件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dKDVH46J-1685102541208)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230526143437600.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qMmJy2Ni-1685102541209)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230526151216100.png)]

每个pojo类对应一个XXXExample类,是用来为Car添加复杂条件的。

测试逆向工程生成的代码是否好用

@Test
public void test002 () {
    SqlSession sqlSession = SqlSessionUtil.openSession();
    CarMapper mapper = sqlSession.getMapper(CarMapper.class);
    CarExample carExample = new CarExample();//创建条件对象
    //添加类型中包含燃油车的模糊查询条件
    carExample.createCriteria().andCarTypeLike("燃油车")
            //添加指导价格在0到100之间的查询条件
                    .andGuidePriceBetween(new BigDecimal(0),new BigDecimal(100));
    //添加或者关系的条件,以后添加的条件跟前面所欲的条件时或者的关系
    //添加汽车品牌不为空的查询条件
    carExample.or().andBrandIsNull();
    System.out.println(carExample);
    List<Car> cars = mapper.selectByExample(carExample);
    for (Car car : cars) {
        System.out.println(car);
    }
    sqlSession.close();
}

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HQGnc1Z0-1685102541209)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230526152228723.png)]

MyBatis使用PageHelper

limit分页

  1. MySQL的limit后面两个数字

    1. 第一个数字:startIndex(起始下标,下标从0开始,如果省略起始下标默认也是从0开始)
    2. 第二个数字:pageSize(每页显示的记录条数)【从startIndex下标这条记录开始显示接下来的pageSize条记录】
  2. 假设已知页码pageNum,还有煤业显示的记录条数pageSize,limit第一个数字可以动态计算:

    • startIndex = (pageNum - 1) * pageSize;

    • 通用的MySQL分页sql

      select 
        * 
      from 
        tableName ...... 
      limit 
        (pageNum - 1) * pageSize, pageSize
      
  3. 使用MyBatis应该怎么做?

  4. 模块名:mybatis-012-page

  5. 映射器接口中的方法:

    public interface CarMapper {
        
        /**
        * 通过分页的方式获取Car列表
        * @param startIndex 页码
        * @param pageSize 每页显示记录条数
        * @return
        */
        List<Car> selectAllByPage(@Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
    }
    

    SQL映射文件

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="com.powernode.mybatis.mapper.CarMapper">
    
        <select id="selectAllByPage" resultType="Car">
            select * from t_car limit #{startIndex},#{pageSize}
        </select>
    </mapper>
    

    测试程序:

    @Test
    public void testPage()throws Exception{
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
        SqlSession sqlSession = sqlSessionFactory.openSession();
        CarMapper mapper = sqlSession.getMapper(CarMapper.class);
    
        // 页码
        Integer pageNum = 2;
        // 每页显示记录条数
        Integer pageSize = 3;
        // 起始下标
        Integer startIndex = (pageNum - 1) * pageSize;
    
        List<Car> cars = mapper.selectAllByPage(startIndex, pageSize);
        cars.forEach(car -> System.out.println(car));
    
        sqlSession.commit();
        sqlSession.close();
    }
    

    执行结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hNE0RYgq-1685102541210)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230526165949546.png)]

  6. 获取数据不难。难的时获取分页相关的数据比较难。可以借助PageHelper插件。

PageHelper插件

  1. MyBatis本身并不提供分页功能,但可以使用第三方的插件来实现分页功能。PageHelper是一个比较流行的分页插件,可以通过Maven或Gradle等构建工具引入到项目中。
  2. PageHelper翻译为:分页助手
  3. 使用PageHelper插件进项分页,更加便捷。

第一步:引入PageHelper依赖

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

第二步:在mybatis-config.xml文件中配置插件

<plugins>
	<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
  1. 在这段代码中,<plugins>元素是MyBatis配置文件中的一个标签,表示要配置的插件列表。<plugin>元素则表示一个具体的插件,并有一个名为interceptor的属性,指定了该插件所使用的拦截器类。在这个例子中,com.github.pagehelper.PageInterceptor就是PageHelper分页插件实际使用的拦截器类。

  2. 通过配置这个插件和拦截器,我们就可以在MyBatis中直接使用PageHelper提供的分页方法来进行分页查询,而无需手动编写繁琐的SQL语句。

  3. 拦截器有什么功能,在什么时候发挥作用?

    • com.github.pagehelper.PageInterceptor是PageHelper提供的拦截器类,它的主要作用是在执行数据库操作之前对SQL语句进行拦截和处理。具体来说,它会解析SQL语句中的分页参数(如页码、每页行数等),并根据这些参数重新生成新的SQL语句。

      当我们在MyBatis中调用查询方法时,PageInterceptor就会拦截这个查询操作,并将其转化为一个新的查询操作。在转化过程中,PageInterceptor会根据传入的分页参数生成相应的SQL语句,并设置合适的查询范围和排序方式。最终,PageInterceptor会将新的SQL语句返回给MyBatis框架,让其执行真正的查询操作。

      通过使用PageInterceptor,我们可以在不修改原有SQL语句的情况下,实现分页查询功能。同时,PageInterceptor还支持多种数据库类型和分页模式,可以满足不同场景下的需求。

第三步:编写Java代码

Mapper接口中的方法:

List<Car> selectAll();

SQL映射文件:

<select id="selectAll" resultType="Car">
  select * from t_car
</select>

在写SQL映射文件的时候,写完一条SQL语句一般后面不要加“;”,如果上述这条SQL语句加了“;”那么使用PageHelper的时候将会抛出异常:

在“;”后面又拼接了Limit语句

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-54FfYUO6-1685102541210)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230526182324092.png)]

测试程序:

@Test
public void testPageHelper() throws Exception{
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
    SqlSession sqlSession = sqlSessionFactory.openSession();
    CarMapper mapper = sqlSession.getMapper(CarMapper.class);

    // 开启分页,拦截SQL语句,生成新的sql语句相对于原来的sql语句,后面增加了“LIMIT 2, 3”【简单理解,第一个参数时页码,第二个参数时每页显示多少条记录,在这里就是查询出来第二页的三条记录】
    PageHelper.startPage(2, 3);

    // 执行查询语句,一定要再执行查询语句之前开启分页功能,开启分页功能之后这里查询到的就不是所有的Car了,而是第二页的3条记录
    List<Car> cars = mapper.selectAll();

    // 获取分页信息对象
    PageInfo<Car> pageInfo = new PageInfo<>(cars, 5);

    System.out.println(pageInfo);
}
  1. 关键点:
    1. 在查询语句之前开启分页功能。因为再MyBatis的核心配置文件中配置了分页拦截器,然后再执行查询语句的时候,拦截器会拦截MyBatis发送给DBMS的SQL语句,对SQL语句进行修改,生成一条新的SQL语句发送给DBMS。
    2. 在查询语句之后封装PageInfo对象。【如果时web应用可以将pageInfo对象存储到request域当中。在页面上展示。】
  2. PageInfo类构造方法的参数:
    1. 第一个:开启分页之后执行的查询语句,返回的结果。
    2. 第二个:页签,下面显示几个页签。

执行结果:

PageInfo{pageNum=2, pageSize=2, size=2, startRow=3, endRow=4, total=6, pages=3, list=Page{count=true, pageNum=2, pageSize=2, startRow=2, endRow=4, total=6, pages=3, reasonable=false, pageSizeZero=false}[Car{id=86, carNum=‘1234’, brand=‘丰田霸道’, guidePrice=50.5, produceTime=‘2020-10-11’, carType=‘燃油车’}, Car{id=87, carNum=‘1234’, brand=‘丰田霸道’, guidePrice=50.5, produceTime=‘2020-10-11’, carType=‘燃油车’}], prePage=1, nextPage=3, isFirstPage=false, isLastPage=false, hasPreviousPage=true, hasNextPage=true, navigatePages=5, navigateFirstPage=1, navigateLastPage=3, navigatepageNums=[1, 2, 3]}

对执行结果格式化:

PageInfo{
  pageNum=2, pageSize=2, size=2, startRow=3, endRow=4, total=6, pages=3, 
  list=Page{count=true, pageNum=2, pageSize=2, startRow=2, endRow=4, total=6, pages=3, reasonable=false, pageSizeZero=false}
  [Car{id=86, carNum='1234', brand='丰田霸道', guidePrice=50.5, produceTime='2020-10-11', carType='燃油车'}, 
  Car{id=87, carNum='1234', brand='丰田霸道', guidePrice=50.5, produceTime='2020-10-11', carType='燃油车'}], 
  prePage=1, nextPage=3, isFirstPage=false, isLastPage=false, hasPreviousPage=true, hasNextPage=true, 
  navigatePages=5, navigateFirstPage=1, navigateLastPage=3, navigatepageNums=[1, 2, 3]
}

MyBatis的注解式开发

  1. MyBatis中也提供了注解式开发,采用注解可以减少sql映射文件的配置。

  2. 使用注解的话sql语句可以写再Java程序中,这种方式也会给sql语句维护成本降低。

  3. 使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让你本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。

    使用注解编写复杂的SQL是这样的:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IeP0OLBL-1685102541211)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230526194109371.png)]

    虽然也能编写但是写出来的sql让然看起来复杂的不能看。

  4. 原则:简答sql可以注解,复杂sql使用xml。

@Insert

//接口中的方法
public interface CarMapper {

@Insert(value="insert into t_car values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})")
int insert(Car car);
}

//测试程序
@Test
public void testInsert() throws Exception{
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
    SqlSession sqlSession = sqlSessionFactory.openSession();
    CarMapper mapper = sqlSession.getMapper(CarMapper.class);
    Car car = new Car(null, "1112", "卡罗拉", 30.0, "2000-10-10", "燃油车");
    int count = mapper.insert(car);
    System.out.println("插入了几条记录:" + count);
    sqlSession.commit();
    sqlSession.close();
}

@Delete

@Delete("delete from t_car where id = #{id}")
int deleteById(Long id);
@Test
public void testDelete() throws Exception{
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
    SqlSession sqlSession = sqlSessionFactory.openSession();
    CarMapper mapper = sqlSession.getMapper(CarMapper.class);
    mapper.deleteById(89L);
    sqlSession.commit();
    sqlSession.close();
}

@Update

@Update("update t_car set car_num=#{carNum},brand=#{brand},guide_price=#{guidePrice},produce_time=#{produceTime},car_type=#{carType} where id=#{id}")
int update(Car car);
@Test
public void testUpdate() throws Exception{
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
    SqlSession sqlSession = sqlSessionFactory.openSession();
    CarMapper mapper = sqlSession.getMapper(CarMapper.class);
    Car car = new Car(88L,"1001", "凯美瑞", 30.0,"2000-11-11", "新能源");
    mapper.update(car);
    sqlSession.commit();
    sqlSession.close();
}

@Select

@Select("select * from t_car where id = #{id}")
//使用@Results注解配置结果映射
@Results({
    @Result(column = "id", property = "id", id = true), //id = true代表id是主键
    @Result(column = "car_num", property = "carNum"),
    @Result(column = "brand", property = "brand"),
    @Result(column = "guide_price", property = "guidePrice"),
    @Result(column = "produce_time", property = "produceTime"),
    @Result(column = "car_type", property = "carType")
})
Car selectById(Long id);
@Test
public void testSelectById() throws Exception{
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
    SqlSession sqlSession = sqlSessionFactory.openSession();
    CarMapper carMapper = sqlSession.getMapper(CarMapper.class);
    Car car = carMapper.selectById(88L);
    System.out.println(car);
}

执行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8rYmwyMJ-1685102541211)(C:\Users\XueYingHao\AppData\Roaming\Typora\typora-user-images\image-20230526194448260.png)]
L语句发送给DBMS。
2. 在查询语句之后封装PageInfo对象。【如果时web应用可以将pageInfo对象存储到request域当中。在页面上展示。】
2. PageInfo类构造方法的参数:

  1. 第一个:开启分页之后执行的查询语句,返回的结果。
  2. 第二个:页签,下面显示几个页签。

执行结果:

PageInfo{pageNum=2, pageSize=2, size=2, startRow=3, endRow=4, total=6, pages=3, list=Page{count=true, pageNum=2, pageSize=2, startRow=2, endRow=4, total=6, pages=3, reasonable=false, pageSizeZero=false}[Car{id=86, carNum=‘1234’, brand=‘丰田霸道’, guidePrice=50.5, produceTime=‘2020-10-11’, carType=‘燃油车’}, Car{id=87, carNum=‘1234’, brand=‘丰田霸道’, guidePrice=50.5, produceTime=‘2020-10-11’, carType=‘燃油车’}], prePage=1, nextPage=3, isFirstPage=false, isLastPage=false, hasPreviousPage=true, hasNextPage=true, navigatePages=5, navigateFirstPage=1, navigateLastPage=3, navigatepageNums=[1, 2, 3]}

对执行结果格式化:

PageInfo{
  pageNum=2, pageSize=2, size=2, startRow=3, endRow=4, total=6, pages=3, 
  list=Page{count=true, pageNum=2, pageSize=2, startRow=2, endRow=4, total=6, pages=3, reasonable=false, pageSizeZero=false}
  [Car{id=86, carNum='1234', brand='丰田霸道', guidePrice=50.5, produceTime='2020-10-11', carType='燃油车'}, 
  Car{id=87, carNum='1234', brand='丰田霸道', guidePrice=50.5, produceTime='2020-10-11', carType='燃油车'}], 
  prePage=1, nextPage=3, isFirstPage=false, isLastPage=false, hasPreviousPage=true, hasNextPage=true, 
  navigatePages=5, navigateFirstPage=1, navigateLastPage=3, navigatepageNums=[1, 2, 3]
}

MyBatis的注解式开发

  1. MyBatis中也提供了注解式开发,采用注解可以减少sql映射文件的配置。

  2. 使用注解的话sql语句可以写再Java程序中,这种方式也会给sql语句维护成本降低。

  3. 使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让你本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。

    使用注解编写复杂的SQL是这样的:

    [外链图片转存中…(img-IeP0OLBL-1685102541211)]

    虽然也能编写但是写出来的sql让然看起来复杂的不能看。

  4. 原则:简答sql可以注解,复杂sql使用xml。

@Insert

//接口中的方法
public interface CarMapper {

@Insert(value="insert into t_car values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})")
int insert(Car car);
}

//测试程序
@Test
public void testInsert() throws Exception{
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
    SqlSession sqlSession = sqlSessionFactory.openSession();
    CarMapper mapper = sqlSession.getMapper(CarMapper.class);
    Car car = new Car(null, "1112", "卡罗拉", 30.0, "2000-10-10", "燃油车");
    int count = mapper.insert(car);
    System.out.println("插入了几条记录:" + count);
    sqlSession.commit();
    sqlSession.close();
}

@Delete

@Delete("delete from t_car where id = #{id}")
int deleteById(Long id);
@Test
public void testDelete() throws Exception{
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
    SqlSession sqlSession = sqlSessionFactory.openSession();
    CarMapper mapper = sqlSession.getMapper(CarMapper.class);
    mapper.deleteById(89L);
    sqlSession.commit();
    sqlSession.close();
}

@Update

@Update("update t_car set car_num=#{carNum},brand=#{brand},guide_price=#{guidePrice},produce_time=#{produceTime},car_type=#{carType} where id=#{id}")
int update(Car car);
@Test
public void testUpdate() throws Exception{
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
    SqlSession sqlSession = sqlSessionFactory.openSession();
    CarMapper mapper = sqlSession.getMapper(CarMapper.class);
    Car car = new Car(88L,"1001", "凯美瑞", 30.0,"2000-11-11", "新能源");
    mapper.update(car);
    sqlSession.commit();
    sqlSession.close();
}

@Select

@Select("select * from t_car where id = #{id}")
//使用@Results注解配置结果映射
@Results({
    @Result(column = "id", property = "id", id = true), //id = true代表id是主键
    @Result(column = "car_num", property = "carNum"),
    @Result(column = "brand", property = "brand"),
    @Result(column = "guide_price", property = "guidePrice"),
    @Result(column = "produce_time", property = "produceTime"),
    @Result(column = "car_type", property = "carType")
})
Car selectById(Long id);
@Test
public void testSelectById() throws Exception{
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
    SqlSession sqlSession = sqlSessionFactory.openSession();
    CarMapper carMapper = sqlSession.getMapper(CarMapper.class);
    Car car = carMapper.selectById(88L);
    System.out.println(car);
}

执行结果:

[外链图片转存中…(img-8rYmwyMJ-1685102541211)]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薛英豪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值