• 【JavaWeb的从0到1的构建知识体系(四)】认识Mybatis(上)


    认识Mybatis

    在前面JDBC的学习中,虽然我们能够通过JDBC来连接和操作数据库,但是哪怕只是完成一个SQL语句的执行,都需要编写大量的代码,更不用说如果我还需要进行实体类映射,将数据转换为我们可以直接操作的实体类型,JDBC很方便,但是还不够方便,我们需要一种更加简洁高效的方式来和数据库进行交互。

    再次强调:学习厉害的框架或是厉害的技术,并不是为了一定要去使用它,而是它们能够使得我们在不同的开发场景下,合理地使用这些技术,以灵活地应对需要解决的问题。
    img
    MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Ordinary Java Object,普通的 Java对象)映射成数据库中的记录。

    我们依然使用传统的jar依赖方式,从最原始开始讲起,不使用Maven,有关Maven内容我们会在后面统一讲解!全程围绕官方文档讲解!

    这一块内容很多很杂,再次强调要多实践!

    XML语言概述

    在开始介绍Mybatis之前,XML语言发明最初是用于数据的存储和传输,它可以长这样:

    
    <outer>
      <name>Kplusonename>
      <desc>写bugdesc>
    	<inner type="1">
        <age>10age>
        <sex>sex>
      inner>
    outer>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    如果你学习过前端知识,你会发现它和HTML几乎长得一模一样!但是请注意,虽然它们长得差不多,但是他们的意义却不同,HTML主要用于通过编排来展示数据,而XML主要是存放数据,它更像是一个配置文件!当然,浏览器也是可以直接打开XML文件的。

    一个XML文件存在以下的格式规范:

    • 必须存在一个根节点,将所有的子标签全部包含。
    • 可以但不必须包含一个头部声明(主要是可以设定编码格式
    • 所有的标签必须成对出现,可以嵌套但不能交叉嵌套
    • 区分大小写
    • 标签中可以存在属性,比如上面的type="1"就是inner标签的一个属性,属性的值由单引号或双引号包括

    XML文件也可以使用注释:

    
    
    
    • 1
    • 2

    通过IDEA我们可以使用Ctrl+/来快速添加注释文本(不仅仅适用于XML,还支持很多种类型的文件)

    那如果我们的内容中出现了<或是>字符,那该怎么办呢?我们就可以使用XML的转义字符来代替:
    img
    如果嫌一个一个改太麻烦,也可以使用CD来快速创建不解析区域:

    <test>
        <name><><>是一点都不懂哦>>>]]>name>
    test>
    
    • 1
    • 2
    • 3

    那么,我们现在了解了XML文件的定义,现在该如何去解析一个XML文件呢?比如我们希望将定义好的XML文件读取到Java程序中,这时该怎么做呢?

    JDK为我们内置了一个叫做org.w3c的XML解析库,我们来看看如何使用它来进行XML文件内容解析:

    // 创建DocumentBuilderFactory对象
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    // 创建DocumentBuilder对象
    try {
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document d = builder.parse("file:mappers/test.xml");
        // 每一个标签都作为一个节点
        NodeList nodeList = d.getElementsByTagName("test");  // 可能有很多个名字为test的标签
        Node rootNode = nodeList.item(0); // 获取首个
    
        NodeList childNodes = rootNode.getChildNodes(); // 一个节点下可能会有很多个节点,比如根节点下就囊括了所有的节点
        //节点可以是一个带有内容的标签(它内部就还有子节点),也可以是一段文本内容
    
        for (int i = 0; i < childNodes.getLength(); i++) {
            Node child = childNodes.item(i);
            if(child.getNodeType() == Node.ELEMENT_NODE)  //过滤换行符之类的内容,因为它们都被认为是一个文本节点
            System.out.println(child.getNodeName() + ":" +child.getFirstChild().getNodeValue());
            // 输出节点名称,也就是标签名称,以及标签内部的文本(内部的内容都是子节点,所以要获取内部的节点)
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    当然,学习和使用XML只是为了更好地去认识Mybatis的工作原理,以及如何使用XML来作为Mybatis的配置文件,这是在开始之前必须要掌握的内容(使用Java读取XML内容不要求掌握,但是需要知道Mybatis就是通过这种方式来读取配置文件的

    不仅仅是Mybatis,包括后面的Spring等众多框架都会用到XML来作为框架的配置文件!

    初次使用Mybatis

    那么我们首先来感受一下Mybatis给我们带来的便捷,就从搭建环境开始,中文文档网站:
    Mybatis

    我们需要导入Mybatis的依赖,同样地放入到项目的根目录下,右键作为依赖即可!(依赖变多之后,我们可以将其放到一个单独的文件夹,不然会很繁杂)

    依赖导入完成后,我们就可以编写Mybatis的配置文件了(现在不是在Java代码中配置了,而是通过一个XML文件去配置,这样就使得硬编码的部分大大减少,项目后期打包成Jar运行不方便修复,但是通过配置文件,我们随时都可以去修改,就变得很方便了,同时代码量也大幅度减少,配置文件填写完成后,我们只需要关心项目的业务逻辑而不是如何去读取配置文件)我们按照官方文档给定的提示,在项目根目录下新建名为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>
      <environments default="development">
        <environment id="development">
          <transactionManager type="JDBC"/>
          <dataSource type="POOLED">
            <property name="driver" value="${驱动类(含包名)}"/>
            <property name="url" value="${数据库连接URL}"/>
            <property name="username" value="${用户名}"/>
            <property name="password" value="${密码}"/>
          </dataSource>
        </environment>
      </environments>
    </configuration>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    我们发现,在最上方还引入了一个叫做DTD(文档类型定义)的东西,它提前帮助我们规定了一些标签,我们就需要使用Mybatis提前帮助我们规定好的标签来进行配置(因为只有这样Mybatis才能正确识别我们配置的内容)

    通过进行配置,我们就告诉了Mybatis我们链接数据库的一些信息,包括URL、用户名、密码等,这样Mybatis就知道该链接哪个数据库、使用哪个账号进行登陆了(也可以不使用配置文件,其实,只要是能够用配置文件实现的配置,用代码也一定可以,因为配置文件也是要被读取到Java代码里的)

    配置文件完成后,我们需要在Java程序启动时,让Mybatis对配置文件进行读取并得到一个SqlSessionFactory对象:

    public static void main(String[] args) throws FileNotFoundException {
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream("mybatis-config.xml"));
        try (SqlSession sqlSession = sqlSessionFactory.openSession(true)){
    			//暂时还没有业务
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    直接运行即可,虽然没有干什么事情,但是不会出现错误,如果之前的配置文件编写错误,直接运行会产生报错!那么现在我们来看看,SqlSessionFactory对象是什么东西:

    img
    每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的,我们可以通过SqlSessionFactory来创建多个新的会话,SqlSession对象,每个会话就相当于我不同的地方登陆一个账号去访问数据库,你也可以认为这就是之前JDBC中的Statement对象,会话之间相互隔离,没有任何关联。

    而通过SqlSession就可以完成几乎所有的数据库操作,我们发现这个接口中定义了大量数据库操作的方法,因此,现在我们只需要通过一个对象就能完成数据库交互了,极大简化了之前的流程。

    我们来尝试一下直接读取实体类,读取实体类肯定需要一个映射规则,比如类中的哪个字段对应数据库中的哪个字段,在查询语句返回结果后,Mybatis就会自动将对应的结果填入到对象的对应字段上。首先编写实体类,,直接使用Lombok是不是就很方便了:

    import lombok.Data;
    
    @Data
    public class Student {
        int sid;   //名称最好和数据库字段名称保持一致,不然可能会映射失败导致查询结果丢失
        String name;
        String sex;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    根目录下重新创建一个mapper文件夹,新建名为TestMapper.xml的文件作为我们的映射器,并填写以下内容:

    
    DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="TestMapper">
        <select id="selectStudent" resultType="com.test.entity.Student">
            select * from student
        select>
    mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    其中namespace就是命名空间,每个Mapper都是唯一的,因此需要用一个命名空间来区分,它还可以用来绑定一个接口。我们在里面写入了一个select标签,表示添加一个select操作,同时id作为操作的名称,resultType指定为我们刚刚定义的实体类,表示将数据库结果映射为Student类,然后就在标签中写入我们的查询语句即可。

    编写好后,我们在配置文件mybatis-config中添加这个Mapper映射器:

    <mappers>
        <mapper url="file:mappers/TestMapper.xml"/>
        <!--    这里用的是url,也可以使用其他类型,我们会在后面讲解    -->
    </mappers>
    
    • 1
    • 2
    • 3
    • 4

    最后在程序中使用我们定义好的Mapper即可

    public static void main(String[] args) throws FileNotFoundException {
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream("mybatis-config.xml"));
        try (SqlSession sqlSession = sqlSessionFactory.openSession(true)){
            List<Student> student = sqlSession.selectList("selectStudent");
            student.forEach(System.out::println);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们会发现,Mybatis非常智能我们只需要告诉一个映射关系,就能够直接将查询结果转化为一个实体类!

    配置Mybatis

    在了解了Mybatis为我们带来的便捷之后,现在我们就可以正式地去学习使用Mybatis了

    由于SqlSessionFactory一般只需要创建一次,因此我们可以创建一个工具类来集中创建SqlSession,这样会更加方便一些:

    public class MybatisUtil {
    
        //在类加载时就进行创建
        private static SqlSessionFactory sqlSessionFactory;
        static {
            try {
                sqlSessionFactory = new SqlSessionFactoryBuilder().build(new FileInputStream("mybatis-config.xml"));
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 获取一个新的会话
         * @param autoCommit 是否开启自动提交(跟JDBC是一样的,如果不自动提交,则会变成事务操作)
         * @return SqlSession对象
         */
        public static SqlSession getSession(boolean autoCommit){
            return sqlSessionFactory.openSession(autoCommit);//SqlSession就相当于JDBC的statement用来执行SQL语句的。
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    现在我们只需要在main方法中这样写即可查询结果了:

    public static void main(String[] args) {
        try (SqlSession sqlSession = MybatisUtil.getSession(true)){
            List<Student> student = sqlSession.selectList("selectStudent");
            student.forEach(System.out::println);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    之前我们演示了,如何创建一个映射器来将结果快速转换为实体类,但是这样可能还是不够方便,我们每次都需要去找映射器对应操作的名称(select标签的id),而且还要知道对应的返回类型,再通过SqlSession来执行对应的方法(selectList),能不能再方便一点呢?

    现在,我们可以通过namespace来把mapper.xml文件绑定到一个接口上,利用接口的特性,我们可以直接指明方法的行为,而实际实现则是由Mybatis来完成

    //mapper.xml文件就相当是它的实现类
    public interface TestMapper {
        List<Student> selectStudent();
    }
    
    • 1
    • 2
    • 3
    • 4

    将Mapper文件的命名空间修改为我们的接口全类名),建议同时将其放到同名包中,作为内部资源

    <mapper namespace="com.test.mapper.TestMapper">
        <select id="selectStudent" resultType="com.test.entity.Student">
            select * from student
        select>
    mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    作为内部资源后,我们需要修改一下配置文件mybatis-config中的mapper定义,不使用url而是resource表示是Jar内部的文件:

    <mappers>
        <mapper resource="com/test/mapper/TestMapper.xml"/>
    </mappers>
    
    • 1
    • 2
    • 3

    现在我们就可以直接通过SqlSession获取对应的实现类,通过接口中定义的行为来直接获取结果:

    public static void main(String[] args) {
        try (SqlSession sqlSession = MybatisUtil.getSession(true)){
            TestMapper testMapper = sqlSession.getMapper(TestMapper.class);//获取到接口的实现类
            List<Student> student = testMapper.selectStudent();
            student.forEach(System.out::println);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    那么肯定有人好奇,TestMapper明明是一个我们自己定义接口啊,Mybatis也不可能提前帮我们写了实现类啊,那这接口怎么就出现了一个实现类呢?我们可以通过调用getClass()方法来看看实现类是个什么

    TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
    System.out.println(testMapper.getClass());
    
    • 1
    • 2

    我们发现,实现类名称很奇怪,名称为com.sun.proxy.$Proxy4,它是通过动态代理生成的,相当于动态生成了一个实现类,而不是预先定义好的,有关Mybatis这一部分的原理,我们放在最后一节进行讲解。

    接下来,我们再来看配置文件,之前我们并没有对配置文件进行一个详细的介绍:

    <configuration>
        <environments default="development">//设置默认环境,可以按照需求切换
            <environment id="development">//表示这是开发环境,如果要上线就是生产环境
                <transactionManager type="JDBC"/>//使用JDBC规范(java.sql)
                <dataSource type="POOLED">//数据库连接池
                    <property name="driver" value="com.mysql.cj.jdbc.Driver"/>//mysql驱动包里的针对JDBC规范做的实现类,项目启动要去加载这个类
                    <property name="url" value="jdbc:mysql://localhost:3306/study"/>//数据库软件运行的地址
                    <property name="username" value="test"/>//用户
                    <property name="password" value="123456"/>//密码
                </dataSource>
            </environment>
        </environments>
        <mappers>
            <mapper resource="com/test/mapper/TestMapper.xml"/>//必须将mapper文件写到配置文件里
        </mappers>
    </configuration>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    首先就从environments标签说起,一般情况下,我们在开发中,都需要指定一个数据库的配置信息,包含连接URL、用户、密码等信息,而environment就是用于进行这些配置的!实际情况下可能会不止有一个数据库连接信息,比如开发过程中我们一般会使用本地的数据库,而如果需要将项目上传到服务器或是防止其他人的电脑上运行时,我们可能就需要配置另一个数据库的信息,因此,我们可以提前定义好所有的数据库信息,该什么时候用什么即可!

    environments标签上有一个default属性,来指定默认的环境,当然如果我们希望使用其他环境,可以修改这个默认环境,也可以在创建工厂时选择环境:

    sqlSessionFactory = new SqlSessionFactoryBuilder()
            .build(new FileInputStream("mybatis-config.xml"), "环境ID");//虽然可以在代码里换数据库,但是推荐在配置文件里
    
    • 1
    • 2

    我们还可以给类型起一个别名,以简化Mapper的编写:

    <!-- 需要在environments的上方 -->//规范大于配置
    <typeAliases>
        <typeAlias type="com.test.entity.Student" alias="Student"/>
    </typeAliases>
    
    • 1
    • 2
    • 3
    • 4

    现在Mapper就可以直接使用别名了:

    <mapper namespace="com.test.mapper.TestMapper">
        <select id="selectStudent" resultType="Student">
            select * from student
        select>
    mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果这样还是很麻烦,我们也可以直接让Mybatis去扫描一个包,并将包下的所有类自动起别名(别名为首字母小写的类名

    <typeAliases>
        <package name="com.test.entity"/>
    </typeAliases>
    
    • 1
    • 2
    • 3

    也可以为指定实体类添加一个注解,来指定别名(凡是可以写配置文件的,都可以换成代码的方法,要不就设置配置类的属性,要不就是注解的方式):

    @Data
    @Alias("lbwnb")
    public class Student {
        private int sid;
        private String name;
        private String sex;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    当然,Mybatis也包含许多的基础配置,通过使用:

    <settings>
        <setting name="" value=""/>
    settings>
    
    • 1
    • 2
    • 3

    所有的配置项可以在中文文档处查询(阅读官方文档是学习技术的最好的方式),本文不会进行详细介绍,在后面我们会提出一些比较重要的配置项。

    有关配置文件的介绍就暂时到这里为止,我们讨论的重心应该是Mybatis的应用,而不是配置文件,所以省略了一部分内容的讲解。

    增删改查

    在了解了Mybatis的一些基本配置之后,我们就可以正式来使用Mybatis来进行数据库操作了!

    在前面我们演示了如何快速进行查询,我们只需要编写一个对应的映射器既可以了:

    <mapper namespace="com.test.mapper.TestMapper">
        <select id="studentList" resultType="Student">
            select * from student
        select>
    mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    当然,如果你不喜欢使用实体类,那么这些属性还可以被映射到一个Map上:

    <select id="selectStudent" resultType="Map">
        select * from student
    select>
    
    • 1
    • 2
    • 3
    public interface TestMapper {
        List<Map> selectStudent();
    }
    
    • 1
    • 2
    • 3

    Map中就会以键值对的形式来存放这些结果了。

    list[0]=map,map={sid=0,sex=男,name=bob......(键值对)}//大概格式就是这样
    
    • 1

    通过设定一个resultType属性,让Mybatis知道查询结果需要映射为哪个实体类,要求字段名称保持一致。那么如果我们不希望按照这样的规则来映射呢?我们可以自定义resultMap来设定映射规则
    写在mapper文件里。

    <resultMap id="Test" type="Student">
        <result column="sid" property="sid"/>
        <result column="sex" property="name"/>
        <result column="name" property="sex"/>
    </resultMap>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    通过指定映射规则,我们现在名称和性别一栏就发生了交换,因为我们将其映射字段进行了交换。

    如果一个类中存在多个构造方法,那么很有可能会出现这样的错误:

    ### Exception in thread "main" org.apache.ibatis.exceptions.PersistenceException: 
    ### Error querying database.  Cause: org.apache.ibatis.executor.ExecutorException: No constructor found in com.test.entity.Student matching [java.lang.Integer, java.lang.String, java.lang.String]
    ### The error may exist in com/test/mapper/TestMapper.xml
    ### The error may involve com.test.mapper.TestMapper.getStudentBySid
    ### The error occurred while handling results
    ### SQL: select * from student where sid = ?
    ### Cause: org.apache.ibatis.executor.ExecutorException: No constructor found in com.test.entity.Student matching [java.lang.Integer, java.lang.String, java.lang.String]
    	at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
    	...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这时就需要使用constructor标签指定构造方法

    <resultMap id="test" type="Student">
        <constructor>
            <arg column="sid" javaType="Integer"/>
            <arg column="name" javaType="String"/>
        constructor>
    resultMap>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    值得注意的是,指定构造方法后,若此字段被填入了构造方法作为参数,将不会通过反射给字段单独赋值,而构造方法中没有传入的字段,依然会被反射赋值,有关resultMap的内容,后面还会继续讲解(先学会最小必要要知识,不要陷入细节的泥潭,因为作为一个技术,他的体系一定是完备的,但是不代表你要一口气学完,边工作边学习才是正确打开方式)。

    如果数据库中存在一个带下划线的字段,我们可以通过设置让其映射为以驼峰命名的字段,比如my_test映射为myTest因为java类的属性命名规范就是驼峰式命名,还是变相的让数据库字段和属性名字保持一致
    在mybatis-config里配置

    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    settings>
    
    • 1
    • 2
    • 3

    如果不设置,默认为不开启,也就是默认需要名称保持一致

    我们接着来看看条件查询,既然是条件查询,那么肯定需要我们传入查询条件,比如现在我们想通过sid字段来通过学号查找信息:

    //接口中方法的定义
    Student getStudentBySid(int sid);
    
    • 1
    • 2
    <select id="getStudentBySid" parameterType="int" resultType="Student">
        select * from student where sid = #{sid}
    select>
    
    • 1
    • 2
    • 3

    我们通过使用#{xxx}或是${xxx}来填入我们给定的属性,实际上Mybatis本质也是通过PreparedStatement首先进行一次预编译,有效地防止SQL注入问题,但是如果使用${xxx}就不再是通过预编译,而是直接传值,因此我们一般都使用#{xxx}来进行操作。【挖个坑:#和$的区别是什么

    使用parameterType属性来指定参数类型(非必须,可以不用,推荐不用)

    接着我们来看插入、更新和删除操作,其实与查询操作差不多,不过需要使用对应的标签,比如插入操作

    <insert id="addStudent" parameterType="Student">
        insert into student(name, sex) values(#{name}, #{sex})
    insert>
    
    • 1
    • 2
    • 3
    int addStudent(Student student);
    
    • 1

    我们这里使用的是一个实体类,我们可以直接使用实体类里面对应属性替换到SQL语句中,只需要填写属性名称即可,和条件查询是一样的。

    复杂查询

    一个老师可以教授多个学生,那么能否一次性将老师的学生全部映射给此老师的对象(1:N)呢,比如:

    @Data
    public class Teacher {//1
        int tid;
        String name;
        List<Student> studentList;//N	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    映射为Teacher对象时,同时将其教授的所有学生一并映射为List列表,显然这是一种一对多的查询,那么这时就需要进行复杂查询了。而我们之前编写的都非常简单,直接就能完成映射,因此我们现在需要使用resultMap来自定义映射规则:
    三张表
    student

    sidsexname
    1张三
    2李四
    3aaa
    4bbb
    5ccc

    teacher

    tidname
    1
    2
    3

    teach

    idtidsid
    111
    212
    323
    424
    535

    下面的sql联表结果就是:

    sidsexnametidteacher.name
    1张三1
    2李四1
    3aaa2
    4bbb2
    5ccc3
    <select id="getTeacherByTid" resultMap="asTeacher">//注意这里写的是返回resultMap,然后通过resultMap标签去定义
            select *, teacher.name as tname 
            from student inner join teach on student.sid = teach.sid  #将教学表和学生表内联
            inner join teacher on teach.tid = teacher.tid where teach.tid = #{tid}
    select>
    //注意这里的匹配关系就完全是和连表查询后的字段一一对应,查到的字段有学生的所有字段、tid、teacher.name(tname),然后和teacher的属性对应起来。
    <resultMap id="asTeacher" type="Teacher">
        <id column="tid" property="tid"/>//注意id字段使用的是id标签,主键就是特殊哈
        <result column="tname" property="name"/>
        <collection property="studentList" ofType="Student">
            <id column="sid" property="sid"/>
            <result column="name" property="name"/>
            <result column="sex" property="sex"/>
        collection>
    resultMap>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    可以看到,我们的查询结果是一个多表联查的结果,而联查的数据就是我们需要映射的数据(比如这里是一个老师有N个学生,联查的结果也是这一个老师对应N个学生的N条记录),其中id标签用于在多条记录中辨别是否为同一个对象的数据比如上面的查询语句得到的结果中,tid这一行始终为1,因此所有的记录都应该是tid=1的教师的数据,而不应该变为多个教师的数据,如果不加id进行约束,那么会被识别成多个教师的数据!

    通过使用collection来表示将得到的所有结果合并为一个集合,比如上面的数据中每个学生都有单独的一条记录,因此tid相同的全部学生的记录就可以最后合并为一个List,得到最终的映射结果,当然,为了区分,最好也设置一个id,只不过这个例子中可以当做普通的result使用。

    了解了一对多,那么多对一又该如何查询呢,比如每个学生都有一个对应的老师,现在Student新增了一个Teacher对象,那么现在又该如何去处理呢?
    一对多和多对一其实是相反的过程:上面例子是老师是1,学生是多。多对一也是学生是多,老师是1,只不过这次是查学生。(多个学生对应一个老师)
    现在我们希望的是,每次查询到一个Student对象时都带上它的老师,同样的,我们也可以使用resultMap来实现(先修改一下老师的类定义,不然会很麻烦,因为多对一和一对多其实是相同的表,但是类的设计不一样罢了,换句话说,多对一和一对多只要学会一个就行):

    @Data
    @Accessors(chain = true)
    public class Student {
        private int sid;
        private String name;
        private String sex;
        private Teacher teacher;
    }
    
    @Data
    public class Teacher {
        int tid;
        String name;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    <resultMap id="test2" type="Student">
        <id column="sid" property="sid"/>
        <result column="name" property="name"/>
        <result column="sex" property="sex"/>
        <association property="teacher" javaType="Teacher">
            <id column="tid" property="tid"/>
            <result column="tname" property="name"/>
        </association>
    </resultMap>
    <select id="selectStudent" resultMap="test2">
        select *, teacher.name as tname from student left join teach on student.sid = teach.sid
                                                     left join teacher on teach.tid = teacher.tid
    </select>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    多对一的SQL语句和之前讲的一对多完全一样(只不过我随便设计的表看不出内联和左联的区别,如果稍微改一下教学表:不是所有学生多对应有老师,那么就能体现这两种联表的差别。),所以参考上面的表结构。

    通过使用association进行关联,形成多对一的关系,实际上和一对多是同理的,都是对查询结果的一种处理方式罢了。

    事务操作

    我们可以在获取SqlSession关闭自动提交来开启事务模式,和JDBC其实都差不多:

    public static void main(String[] args) {
        try (SqlSession sqlSession = MybatisUtil.getSession(false)){
            TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
    
            testMapper.addStudent(new Student().setSex("男").setName("小王"));
    
            testMapper.selectStudent().forEach(System.out::println);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    我们发现,在关闭自动提交后,我们的内容是没有进入到数据库的,现在我们来试一下在最后提交事务

    sqlSession.commit();
    
    • 1

    在事务提交后,我们的内容才会被写入到数据库中。现在我们来试试看回滚操作:

    try (SqlSession sqlSession = MybatisUtil.getSession(false)){
        TestMapper testMapper = sqlSession.getMapper(TestMapper.class);
    
        testMapper.addStudent(new Student().setSex("男").setName("小王"));
    
        testMapper.selectStudent().forEach(System.out::println);
        sqlSession.rollback();
        sqlSession.commit();
        testMapper.selectStudent().forEach(System.out::println);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    回滚操作也印证成功。

    动态sql

    动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

    在实际开发中的SQL语句没有之前的这么简单,很多时候需要根据传入的参数情况动态的生成SQL语句。Mybatis提供了动态SQL相关的标签让我们使用。

    if

    可以使用if标签进行条件判断,条件成立才会把if标签中的内容拼接进sql语句中。

    例如:

        <select id="findByCondition" resultType="com.sangeng.pojo.User">
             select * from user
             where  id = #{id}
            <if test="username!=null">
               and username = #{username}
            if>
        select>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果参数username为null则执行的sql为:select * from user where id = ?

    如果参数username不为null则执行的sql为:select * from user where id = ? and username = ?

    注意:在test属性中表示参数的时候不需要写#{},写了会出问题。

    trim

    可以使用该标签动态的添加前缀或后缀,也可以使用该标签动态的消除前缀或后缀。

    prefixOverrides属性

    ​ 用来设置需要被清除的前缀,多个值可以用|分隔,注意|前后不要有空格。例如: and|or

    例如:

        <select id="findByCondition" resultType="com.sangeng.pojo.User">
            select * from user
            <trim prefixOverrides="and|or" >
                and
            trim>
        select>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    最终执行的sql为: select * from user

    suffixOverrides属性

    ​ 用来设置需要被清除的后缀,多个值可以用|分隔,注意|前后不要有空格。例如: and|or

    例如:

        <select id="findByCondition" resultType="com.sangeng.pojo.User">
            select * from user
            <trim suffixOverrides="like|and" >
                where 1=1 like
            trim>
        select>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    最终执行的sql为: select * from user 去掉了后缀like

    prefix属性

    ​ 用来设置动态添加的前缀如果标签中有内容就会添加上设置的前缀

    例如:

        <select id="findByCondition" resultType="com.sangeng.pojo.User">
            select * from user
            <trim prefix="where" >
               1=1
            trim>
        select>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    最终执行的sql为:select * from user where 1=1 动态增加了前缀where

    suffix属性

    ​ 用来设置动态添加的后缀如果标签中有内容就会添加上设置的后缀

        <select id="findByCondition" resultType="com.sangeng.pojo.User">
            select * from user
            <trim suffix="1=1" >
               where
            trim>
        select>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    最终执行的sql为:select * from user where 1=1 动态增加了后缀1=1

    动态添加前缀where 并且消除前缀and或者or
    User findByCondition( Integer id, String username);
    
    • 1
        <select id="findByCondition" resultType="com.sangeng.pojo.User">
            select * from user
            <trim prefix="where" prefixOverrides="and|or" >
                <if test="id!=null">
                    id = #{id}
                if>
                <if test="username!=null">
                    and username = #{username}
                if>
            trim>
        select>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    调用方法时如果传入的id和username为null则执行的SQL为:select * from user ,这里trim标签没有起作用,因为trim标签里面没有东西

    调用方法时如果传入的id为null,username不为null,则执行的SQL为:select * from user where username = ?,这时候标签里面有第二个if里的,就会加上前缀where,然后去掉and。

    where

    where标签等价于:

    <trim prefix="where" prefixOverrides="and|or" >trim>
    
    • 1

    可以使用where标签动态的拼接where并且去除前缀的and或者or。其实就是上面的效果,不过这个时候直接用where标签

    例如:

        <select id="findByCondition" resultType="com.sangeng.pojo.User">
            select * from user
            <where>
                <if test="id!=null">
                    id = #{id}
                if>
                <if test="username!=null">
                    and username = #{username}
                if>
            where>
        select>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    如果id和username都为null,则执行的sql为:select * from user

    如果id为null,username不为null,则执行的sql为:select * from user where username = ?

    set

    set标签等价于

    <trim prefix="set" suffixOverrides="," >trim>
    
    • 1

    可以使用set标签动态的拼接set并且去除后缀的逗号。

    例如:

        <update id="updateUser">
            UPDATE USER
            <set>
                <if test="username!=null">
                    username = #{username},
                if>
                <if test="age!=null">
                    age = #{age},
                if>
                <if test="address!=null">
                    address = #{address},
                if>
            set>
            where id = #{id}
        update>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    如果调用方法时传入的User对象的id为2,username不为null,其他属性都为null则最终执行的sql为:UPDATE USER SET username = ? where id = ?

    foreach

    可以使用foreach标签遍历集合或者数组类型的参数,获取其中的元素拿来动态的拼接SQL语句。

    例如:

    方法定义如下

    List<User> findByIds(@Param("ids") Integer[] ids);//首先这个方法定义在接口里,不是接受前端请求那个,别搞混了。@Param("ids")表示如果形参名和SQL语句的不一致,可以使用注解统一。
    
    • 1

    如果期望动态的根据实际传入的数组的长度拼接SQL语句。例如传入长度为4个数组最终执行的SQL为:

    select * from User WHERE id in( ? , ? , ? , ? ) ;
    
    • 1

    则在xml映射文件中可以使用以下写法

    	<select id="findByIds" resultType="com.sangeng.pojo.User">
            select * from User
            <where>
                <foreach collection="ids" open="id in(" close=")" item="id" separator=",">
                    #{id}
                foreach>
            where>
        select>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    collection:表示要遍历的参数。

    open:表示遍历开始时拼接的语句

    item:表示给当前遍历到的元素的取的名字,与填入占位符的名字相同。(这里是id)

    separator:表示每遍历完一次拼接的分隔符

    close:表示最后一次遍历完拼接的语句

    注意:如果方法参数是数组类型,默认的参数名是array,如果方法参数是list集合默认的参数名是list。建议遇到数组或者集合类型的参数统一使用@Param注解进行命名,就是为了统一罢了

    choose、when、otherwise

    ​ 当我们不想使用所有的条件,而只是想从多个条件中选择一个使用时。可以使用choose系列标签。类似于java中的switch。

    例如:

    接口中方法定义如下

    List<User> selectChose(User user);
    
    • 1

    期望:

    ​ 如果user对象的id不为空时就通过id查询。

    ​ 如果id为null,username不为null就通过username查询。

    ​ 如果id和username都会null就查询id为3的用户。

    xml映射文件如下

     	<select id="selectChose" resultType="com.sangeng.pojo.User">
            select * from user
            <where>
                <choose>
                    <when test="id!=null">
                        id = #{id}
                    when>
                    <when test="username!=null">
                        username = #{username}
                    when>
                    <otherwise>
                        id = 3
                    otherwise>
                choose>
            where>
        select>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • choose类似于java中的switch

    • when类似于java中的case

    • otherwise类似于java中的dufault

    一个choose标签中最多只会有一个when中的判断成立。从上到下去进行判断。如果成立了就把标签体的内容拼接到sql中,并且不会进行其它when的判断和拼接。如果所有的when都不成立则拼接otherwise中的语句。

    SQL片段抽取

    ​ 我们在xml映射文件中编写SQL语句的时候可能会遇到重复的SQL片段。这种SQL片段我们可以使用sql标签来进行抽取。然后在需要使用的时候使用include标签进行使用。
    实现了语句的复用。
    例如:

        <sql id="baseSelect" >id,username,age,addresssql>
        <select id="findAll" resultType="com.sangeng.pojo.User">
            select <include refid="baseSelect"/>  from user
        select>
    
    • 1
    • 2
    • 3
    • 4

    最终执行的sql为: select id,username,age,address from user

  • 相关阅读:
    R语言使用dt函数生成t分布密度函数数据、使用plot函数可视化t分布密度函数数据(t Distribution)
    HTML知识小结
    C++类和对象(三)
    Spring中IOC容器的基本配置使用
    让ChatGPT等模型学会自主思考!开创性技术“自主认知”框架
    vscode 通过ssh 连接虚拟机vmware(ubuntu)
    pgsql、MySQL 自定义排序对比
    直击永悦科技半年报:双轮驱动下的“增长曲线”
    点胶缺陷视觉检测都是怎么检测的?
    浅谈集合幂级数
  • 原文地址:https://blog.csdn.net/weixin_44063572/article/details/127689450