Mybatis 学习记录

Mybatis相关

目录

前言

计划花费一些时间学习一下当下非常流行的持久层框架Mybatis.

视频参考:尚硅谷雷丰阳Mybatis教程

其它资料参考:Mybatis3官方中文文档

另外, 文章凡引用大佬文章的地方, 均贴出了链接, 感谢!

斜体文字均摘自MyBatis3官方文档

笔记仅供参考, 如有差错, 欢迎指出!

常见的持久化层技术

Mybatis教程-P1常见持久化技术的对比

  • JDBC

    原生持久化层技术, 更是一种规范.

    使用JDBC写过项目的都知道, DAO层需要写一系列接口和实现类.并且每个方法除了需要关注核心的sql语句外,

    还需要频繁地创建Connection,PreparedStatement,ResultSet等非核心操作. 重复性工作很多

    • 针对JDBC的第三方工具包: DBUtil(Apache) , JDBCTemplate(Spring)

      没使用过, 个人不做评价.

      Mybatis 学习记录

  • Hibernate (没使用过, 不做评价)

    Mybatis 学习记录

    • 全自动: 我们只需要告诉Hibernate, JavaBean与数据库中记录的对应关系, 相应的sql语句由框架自动生成.

      这也就意味着很难进行sql优化(可以使用Hibernate提供的HQL进行相关优化, 增加了学习成本)

    • 全映射: 意味着每次查询出来的都是表的全部字段, 效率较低. 而进行部分映射又需要借助HQL

      ORM:Object Relational Mapping 对象关系映射
      将数据库表和实体类、实体类的属性对应起来,让我们可以实现操作实体类即操作数据表

  • Mybatis(半自动框架)

    Mybatis 学习记录

    Mybatis编写SQL, 这一核心工作交给程序员来完成, 并且采用xml形式解耦, 而非像JDBC一样,将sql硬编码在Java代码之中

Mybatis 起步

第一个Mybatis Demo

下面就参照Mybatis官方文档-起步一节, 以Mybatis的方式, 进行一次查询并封装相应结果, 开始我们对Mybatis的学习.

  • 首先引入Mybatis的依赖

    `<dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.7</version>
    </dependency>
    
  • 接下来配置Mybatis的全局配置文件

    全局配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(ransactionManager) 详细内容后面会在探讨

    <?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="dbconfig.properties"/> <!-- 以properties 文件的形式, 给出数据库的相关配置信息 -->
        <environments default="development">
            <environment id="development">
                <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),这些映射器的 XML 映射文件包含了 SQL 代码和映射定义信息 -->
            <mapper resource="studentMapper.xml"/> 
        </mappers>
    </configuration>
    
    #血的教训!!! Driver一定要大写, 不然会一直出ClassNotFoundException的
    jdbc.driver=com.mysql.cj.jdbc.Driver  
    jdbc.url=jdbc:mysql://localhost:3306/qdu?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8
    jdbc.username=root
    jdbc.password=root
    
  • 下面给出映射文件studentMapper.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.shy.mybatis.studentMapper"> <!-- namespace:调用已映射的sql语句用 命名空间.id -->
        <!-- id: 该ql语句在此namespace下的唯一标识 -->
        <!-- resultType: 此sql语句期望封装的返回值类型 -->
        <select id="selectStudentById" resultType="com.shy.entity.Student">
            SELECT * FROM student WHERE id = #{id} <!-- #{id} 为动态输入的参数 -->
        </select>
    </mapper>
    

    student表:

    Mybatis 学习记录

    Mybatis 学习记录

  • 配置文件准备就绪, 接下来开始创建 SqlSessionFactory,

    public class MybatisUtil {
    
        private static SqlSessionFactory sqlSessionFactory;
        
        public static synchronized SqlSessionFactory getSqlSessionFactory() throws IOException {
            // 获取mybatis全局配置文件的输入流
            // Mybatis提供了 Resources 工具类, 方便获取配置文件的输入流
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            // 采用单例模式(官方文档推荐这样做)创建SqlSessionFactory
            if(null == sqlSessionFactory){
                // SqlSessionFactoryBuilder唯一的用处就是创建SqlSessionFactory
                sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            }
            return sqlSessionFactory;
        }
    }
    

    Mybatis 学习记录

  • SqlSessionFactory顾名思义就是来创建SqlSession的工厂, 所以接下来创建SqlSession, 并进行查询封装输出测试

    @Test
    public void test01() throws IOException {
        SqlSessionFactory sqlSessionFactory = MybatisUtil.getSqlSessionFactory();
        // 利用SqlSessionFactory获取SqlSession, SqlSession相当与数据库的一次会话
        SqlSession sqlSession = sqlSessionFactory.openSession();
        try {
            // 调用mapper中的SQL语句, 并传入SQL语句所需参数
            Student student = sqlSession.selectOne("com.shy.mybatis.studentMapper.selectStudentById",1);
            System.out.println(student.toString());
        }finally {
            // 使用完sqlSession后一定要关闭
            sqlSession.close();
        }
    }
    //Student{id=1, name='Lando', gender='male', age=20}
    

    Mybatis 学习记录

    Q: 为什么使用完sqlSession后一定要手动关闭?

    A: 参考Mybatis 学习记录

使用Mapper接口

Student student = sqlSession.selectOne("com.shy.mybatis.studentMapper.selectStudentById",1);

在第一个demo中, 我们使用了 "com.shy.mybatis.studentMapper.selectStudentById来调用sql语句:

<mapper namespace="com.shy.mybatis.studentMapper"> 
    <select id="selectStudentById" resultType="com.shy.entity.Student">
        SELECT * FROM student WHERE id = #{id}
    </select>
</mapper>

不过, 现在更简洁,更流行的方式是使用与指定sql语句的参数和返回值相匹配的接口

演示:

  • 首先编写相应的接口StudentMapper.java

    public interface StudentMapper {
        // 返回值与resultType相同
        // 方法名与id相同
        // 参数列表与Sql语句的参数保持一致
        public Student selectStudentById(int id);
    }
    
  • 将接口与mapper的配置文件进行绑定

    即将studentMapper.xmlmappernamespace改为接口的全限定名

    <mapper namespace="com.shy.dao.StudentMapper">  <!-- 动态绑定 -->
        <select id="selectStudentById" resultType="com.shy.entity.Student">
            SELECT * FROM student WHERE id = #{id}
        </select>
    </mapper>
    
  • 准备就绪后, 创建SqlSession进行测试

    @Test
    public void test03() throws IOException {
        SqlSessionFactory sqlSessionFactory = MybatisUtil.getSqlSessionFactory();
        SqlSession sqlSession = sqlSessionFactory.openSession();
        try{
            // 调用getMapper(), Mybatis底层会通过AOP的方式, 创建出StudentMapper接口相应的实现类(代理设计模式)
            StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
            Student student = studentMapper.selectStudentById(1);
            System.out.println(student.toString());
        }finally {
            sqlSession.close();
        }
    }
    // Student{id=1, name='Lando', gender='male', age=20}
    

    Mybatis 学习记录

使用注解配置Sql语句

public interface StudentMapper {
    @Select("SELECT * FROM Student WHERE id = #{id}")
    public Student selectStudentById(int id);
}

使用注解配置sql语句, 就不需要studentMapper.xml映射文件了.

注册mapper时, 使用接口的全限定名. 因为没有xml映射文件了

<mappers>    
    <mapper class="com.shy.dao.StudentMapper"/>
</mappers>

测试:

@Testpublic void test03() throws IOException {    SqlSessionFactory sqlSessionFactory = MybatisUtil.getSqlSessionFactory();    SqlSession sqlSession = sqlSessionFactory.openSession();    try{        // 调用getMapper(), Mybatis底层会通过AOP的方式, 创建出StudentMapper接口相应的实现类(代理设计模式)        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);        Student student = studentMapper.selectStudentById(1);        System.out.println(student.toString());    }finally {        sqlSession.close();    }}// Student{id=1, name='Lando', gender='male', age=20}

Mybatis 学习记录

Mybatis全局配置文件

参考: Mybatis官方文档-XML配置

该配置文件决定了Mybatis的工作方式

结构

Mybatis全局配置文件主要包括以下几种属性

Mybatis 学习记录

properties(属性)

<properties>主要用来配置一些属性

最常见的方式是引入外部的.properties文件(解耦合), 如dbconfig.properties即数据库的相关信息

<properties resource="dbconfig.properties"/>
<!-- resource 类路径   url 网络路径/磁盘路径 -->
jdbc.driver=com.mysql.cj.jdbc.Driver  
jdbc.url=jdbc:mysql://localhost:3306/mybatis
jdbc.username=root
jdbc.password=root

此外还可以在<properties>标签内编写属性

<properties>
    <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
    ...
</properties>

或者作为方法参数传入

public SqlSessionFactory build(InputStream inputStream, Properties properties) {
    return build(inputStream, null, properties);
}

如果不只一个地方进行了配置,Mybatis 将按照下面的顺序来加载:

通过方法参数传递的属性具有最高优先级,resource/url 属性中指定的配置文件次之,最低优先级的则是 properties 元素中指定的属性(高优先级覆盖低优先级)

settings(设置)

<settings>用来配置一些可以影响Mybatis工作方式的属性.

  • mapUnderscoreToCamelCase "驼峰式"策略映射

    Mybatis 学习记录

    Mybatis默认将表的字段值映射到Java Bean与字段名相同的属性上.

    student表 id字段 → \rightarrow →Student实体的 id属性上

    而在实战当中, 数据库字段多写成student_id的形式, Java Bean的属性多遵循驼峰式写成studentId的形式.

    在这种情况下:

    student表 student_id字段 ↛ \nrightarrow ↛Student实体的 studentId属性上

    此时可以设置mapUnderscoreToCamelCase,开启驼峰命名的自动映射

    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    

    更多属性详情请参照官方文档

  • autoMappingBehavior 自动映射

    Mybatis 学习记录

  • lazyLoadingEnabled 延迟加载

    Mybatis 学习记录

  • aggressiveLazyLoading

    Mybatis 学习记录

typeAliases(类型别名)

类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写(但最好还是写全限定名,语义明确)

<typeAliases>
    <!-- 别名默认使用 Bean 的首字母小写的非限定类名 student
	<!--alias指定一个别名-->
    <typeAlias type="com.shy.entity.Student" alias="student"/>
    <package name="com.shy"/> <!-- 为此包下的每个类都起一个别名 -->
</typeAliases>

还可以使用@Alias注解

@Alias("student")
public class Student{}

此外, Mybatis还未Java常见的内置的类型提供了默认别名, 具体参看文档

typeHandlers(类型处理器)

typeHandlers用来将JDBC类型(如varchar)和Java类型(如String)相映射, 完成类型的转换,从而将字段值封装到JavaBean

MyBatis默认为我们提供了一系列内置的typeHandlers

详细内容见[Mybatis 类型处理器](#Mybatis 类型处理器)

*objectFactory(对象工厂)

每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作

默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法。

如果想覆盖对象工厂的默认行为,可以通过继承DefaultObjectFactory创建自己的对象工厂来实现

plugins(插件)

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用 (AOP)

Mybatis插件开发

environments(环境)

environment

MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中, 现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema 的多个生产数据库中使用相同的 SQL 映射。还有许多类似的使用场景

<environments default="development"> <!-- 指定默认的environment -->
    <environment id="development"> <!-- 环境的唯一标识 -->
        <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>
    <environment id="test">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            ...
        </dataSource>
    </environment>
</environments>

在创建SqlSessionFactory时可以指定environment

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"), "test");

transactionManager

对事务的理解不是太深, 直接搬上官方文档了

在Spring整合Mybatis时, 会使用Spring的相关功能管理事务

Mybatis 学习记录

datasource

用来配置JDBC连接对象的资源.

三大内置数据源类型(具体内容参考文档)

UNPOOLED– 这个数据源的实现会每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的简单应用程序来说,是一个很好的选择。 性能表现则依赖于使用的数据库,对某些数据库来说,使用连接池并不重要,这个配置就很适合这种情形

POOLED– 这种数据源的实现利用池的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 这种处理方式很流行,能使并发 Web 应用快速响应请求

JNDI – 这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。

另外, 继承UnpooledDataSourceFactory可以自定义数据源

databaseIdProvider

MyBatis 可以根据不同的数据库厂商执行不同的语句.

Mybatis 学习记录

一定要设置属性别名, 不然识别不出来

<databaseIdProvider type="DB_VENDOR">
    <property name="MySQL" value="mysql"/> <!-- name 属性不能随意指定 -->
    <property name="SQL Server" value="sqlserver"/>
    <property name="DB2" value="db2"/>
    <property name="Oracle" value="oracle" />
</databaseIdProvider>
sqlSession.getConnection().getMetaData().getDatabaseProductName(); // 可以获取当前数据库的name属性
<!-- databaseId="mysql" 该查询只会在MySQL环境下被执行 -->
<select id="selectStudentById" resultType="com.shy.entity.Student" databaseId="mysql">
    SELECT * FROM mybatis.student WHERE id = #{id}
</select>

如果配置了databaseIdProvider, 则内置参数_databaseProvider代表当前数据库的名称

mappers

mapper用来告诉Mybatis,Sql映射文件或对应的接口的位置.

<mappers>
    <!-- resource 适用于 StudentMapper.xml 与 StudentMapper接口不在同一路径下的情况 -->
    <mapper resource="StudentMapper.xml"/>     <!-- 根据我三四天的经验来看, 这个在IDEA下更常用一些 -->
    <mapper class="com.shy.dao.StudentMapper"/> 
    <package name="com.shy.dao"/>
</mappers>

以上三种配置方式, 适用于不同情况, 具体参考大佬博客

Mybatis 学习记录

Mybatis映射文件

MyBatis 的真正强大在于它的语句映射,这是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 致力于减少使用成本,让用户能更专注于 SQL 代码。

结构

Mybatis 学习记录

Mybatis映射文件是其最为核心的部分, 这一部分的学习主要参考尚硅谷雷丰阳Mybatis教程, 顺序与上图结构略有不同

insert/update/delete

这三个元素分别用来映射插入/更新/删除语句

<!--    public int insertStudent(Student student);-->
<!-- 一般 parameterType 省略即可, Mybatis可以自动识别传入参数的类型 -
<insert id="insertStudent" parameterType="com.shy.entity.student">
    INSERT INTO student(name, gender, age)
    VALUES (#{name}, #{gender}, #{age})
</insert>

<!--    public int updateStudentById(Student student);-->
<update id="updateStudentById">
    UPDATE student
    SET name   = #{name},
    gender = #{gender},
    age    = #{age}
    WHERE id = #{id}
</update>

<!--    public int deleteStudentById(int id);-->
<delete id="deleteStudentById">
    DELETE
    FROM student
    WHERE id = #{id}
</delete>

属性总览

Mybatis 学习记录

注意事项

  • 通过传入Java Bean传递多个参数

    在上述插入和更新方法中, 我们直接传入Student对象(封装多个参数), 这样在Sql语句中就可以使用#{bean的属性}来传递值.

  • 增删改要及时提交commit

    如果不调用SQLSession.commit(), Sql语句虽然会执行成功, 但数据库中不会有相应修改

    这是因为如果不commit, 在sqlSession.close(), 会自动回滚rollback

    // 相关源码
    @Override
    public void close(boolean forceRollback) {
     try {
       if (forceRollback) { // 不commit, forceRollback = true
         tcm.rollback();
       } else {
         tcm.commit();
       }
     } finally {
       delegate.close(forceRollback);
     }
    }
    
    try {
        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
        Student student = new Student("Norris","male",25);
        int rows = studentMapper.insertStudent(student);
        System.out.println(rows); 
        sqlSession.commit(); // 一定要commit
    }finally {
        sqlSession.close();
    }
    

    当然也可在创建SqLSession时设置autoCommit

    SqlSession sqlSession = sqlSessionFactory.openSession(true);
    
  • 增删改的返回值可以是void/Integer(int)/Long/Boolean

    只需在接口对应的方法上指定返回值类型即可

    public int/Boolean updateStudentById(Student student);
    
    // 相关源码
    private Object rowCountResult(int rowCount) {
        final Object result;
        if (method.returnsVoid()) {
            result = null;
        } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) {
            result = rowCount;
        } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) {
            result = (long) rowCount;
        } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) {
            result = rowCount > 0;
        } else {
            throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType());
        }
        return result;
    }
    
  • 获取自增主键(将自增主键赋值给Java Bean的属性)

    <!-- keyProperty指定自增主键赋值给Java Bean的哪个属性 -->
    <insert id="insertStudent" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO student(name, gender, age)
        VALUES (#{name}, #{gender}, #{age})
    </insert>
    

    此方式只适用于支持主键自增的数据库(如MySql, SqlServer)

    对于不支持主键自增的数据库,使用selectKey

    Mybatis 学习记录

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kBNOdtA0-1631105867439)(D:/Typora%20Img/image-20210820215214427.png)]

Sql语句中的参数传递

参考: Mybatis的几种传参方式,你了解吗? - 知乎

源码分析: Mybatis参数传递封装成Map的过程

在编写XXXMapper.xml文件中的Sql语句时, 往往会涉及到#{xx}的参数传递的问题,下面就简要探讨一下

  • 单个参数

    #{key}key可以是任意形式, 但一般与入参名称保持一致

    <!--    public Student selectStudentById(int id);-->
    <select id="selectStudentById" resultType="com.shy.entity.Student">
        SELECT *FROM student WHERE id = #{id}
    </select>
    
  • 多个参数

    • 默认情况下

      #{param1/args0}对应第一个参数,#{param2/args1}对应第二个参数…

      <!--    public Student selectStudentByNameAndGender(String Name, String Gender);-->
      <select id="selectStudentByNameAndGender" resultType="com.shy.entity.Student">
          #     SELECT * FROM student WHERE name = #{arg0} AND gender = #{arg1}
          #     SELECT * FROM student WHERE name = #{param1} AND gender = #{param2}
      </select>
      

      由于这种参数不够语义化, 所以不推荐使用

    • 接口参数使用@Param注解

      Mybatis 学习记录

      #{key}中的值可以是@Param("")中的值

      <!--    public Student selectStudentByNameAndGender(@Param("name") String Name, @Param("gender") String Gender);-->
      <select id="selectStudentByNameAndGender" resultType="com.shy.entity.Student">
          SELECT * FROM student WHERE name = #{name} AND gender = #{gender}
      </select>
      

另外接口的参数可能有多种类型

  • 接口参数为POJO

    POJO: plain ordinary java object 简单Java Bean

    #{key}中的值对应POJO的某一项属性

    public class Student {
        private int id;
        private String name;
        private String gender;
        private int age;
        ...
    }
    
    <!--    public int updateStudentById(Student student);-->
    <update id="updateStudentById">
        UPDATE student SET name = #{name},gender = #{gender},age = #{age}WHERE id = #{id}
    </update>
    

    对于用@Param("student")标注的参数, 可以用#{student.name}来进行映射

  • 接口参数封装成Map

    假如我们要传递的参数某一entity中的数据, 且不经常使用, 我们可以将其封装成Map进行传递.

    #{key}中的key对应在Map中封装的key

    <!--    public Student selectStudentByMap(Map<String,Object> map);-->
    <select id="selectStudentByMap" resultType="com.shy.entity.Student">
        SELECT * FROM student WHERE name = #{name} AND age = #{age}
    </select>
    

    对于用@Param("map")标注的参数, 可以用#{map.name}来进行映射

    // 测试
    Map<String, Object> map = new HashMap<>();
    map.put("name","Fati");
    map.put("age",18);
    Student student = studentMapper.selectStudentByMap(map);
    System.out.println(student.toString());
    // Student{id=2, name='Fati', gender='male', age=18}
    

    假如经常使用,我们可以将其封装成一个TO(数据传输对象)来进行传递(如分页类Page)

  • 接口参数类型为Collection(List/Set) / Array

    参数类型为List, 则用#{list[0]}/#{collection[0]}指代其中的第一个元素

    参数类型为set,则用#{collection[0]}指代其中的第一个元素

    参数类型为array则用#{array[0]}指代其中的第一个元素

    当然如果用@Param()标注后, 就可以无视以上规则

    // #{actorIdList[0]}
    public List<Actor> selectActorListByActorIdList(@Param("actorIdList") List<Integer> actorIdList);
    
    // 相关源码
    public static Object wrapToMapIfCollection(Object object, String actualParamName) {
        if (object instanceof Collection) {
            ParamMap<Object> map = new ParamMap<>();
            map.put("collection", object);
            if (object instanceof List) {
                map.put("list", object);
            }
            Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
            return map;
        } else if (object != null && object.getClass().isArray()) {
            ParamMap<Object> map = new ParamMap<>();
            map.put("array", object);
            Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
            return map;
        }
        return object;
    }
    

#{}与${}

参考:MyBatis中#{}和${}的区别_siwuxie095’s blog

#{}类似于PreparedStatement中的?占位符, 能够防止sql注入, 但是表名,字段名和关键字(如排序方式)无法使用#{}

当sql语句中需要动态替换这些字段时, 就可以使用${}.

${}实质上是字符串拼接.

<!--    public List<Actor> selectActorListByColumnAndOrder(String column, String order);-->
<select id="selectActorListByColumnAndOrder" resultType="com.shy.entity.Actor">
    SELECT * FROM actor ORDER BY ${column} ${order}
</select>

#{}还支持设置一些其他的属性

Mybatis 学习记录

处理null值

Mybatis默认将Java中的null值映射为JDBC中的Other类型, 而对于一些数据库(如Oracle)无法识别.

这时可以设置将null值映射为JDBC中的NULL类型

#{name, jdbcType=NULL}

或者修改全局默认配置

<settings>
    <setting name="jdbcTypeForNull" value="NULL"/>
</settings>

Mybatis 学习记录

select返回List/Map

Mybatis 学习记录

<!--    public List<Actor> selectActorList();-->
<select id="selectActorList" resultType="com.shy.entity.Actor"> <!-- resultType为List包含的类型 -->
    SELECT * FROM actor;
</select>

此外, Mybatis还支持将返回值List封装成Map,只需指定MapKey

@MapKey("ActorId")
public Map<Integer,Actor> selectActorMap();
<select id="selectActorMap" resultType="com.shy.entity.Actor">
    SELECT * FROM actor
</select>

Mybatis 学习记录

resultMap

resultMapMybatis中最重要最强大的元素, 它可以让你自定义数据库字段与JavaBean属性的映射规则

当自动映射的规则不能满足需求时, 就可以使用resultMap自定义映射规则

id/result

id 用来指定主键列

result 用来指定普通字段

<!-- 自定义映射规则.   type: 返回值类型 -->
<resultMap id="actorResultMap" type="com.shy.entity.Actor">
<!-- property: JavaBean的某一属性 column:表的某一字段 -->
<id property="ActorId" column="aid"/> <!-- id 用来指定主键列 -->
<result property="ActorChineseName" column="aChineseName"/> <!-- result 用来指定普通字段 -->
<!-- 其他名称相同字段与属性, 便会按照规则自动映射. 当然我们也可以指定 -->
<result property="ActorOriginName" column="ActorOriginName" />
<result property="ActorInfo" column="ActorInfo" />
</resultMap>
<select id="selectActorList" resultMap="actorResultMap">
    SELECT * FROM actor
</select>

1:1关联查询

假设有Student实体

public class Student {
    private int id;
    private String name;
    private String gender;
    private int age;
    private School school; // 一名学生只在一所学校学习
}

public class School {
    private int schoolId;
    private String schoolName;
}

数据库中有对应表student,school

Mybatis 学习记录

Mybatis 学习记录

现在想要查出id=1的学生信息, 则需要进行多表查询

SELECT * FROM student, school WHERE student.schoolId = school.schoolId AND id = 1;

并且将查出的信息借助Mybatis封装到Student对象中

按之前所学,我们可能会这样写:

<select id="selectStudentById" resultType="com.shy.entity.Student">
    SELECT * FROM student, school WHERE student.schoolId = school.schoolId AND id = #{id}
</select>

但测试之后发现school并不能正确封装

Student{id=1, name='Pedri', gender='male', age=18, school=null}

这时, 我们便需要使用resultType自定义映射规则

  • 级联属性封装查询结果

    <resultMap id="studentResultMap" type="com.shy.entity.Student">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="gender" column="gender"/>
        <result property="age" column="age"/>
        <!-- school实体的属性 -->
        <result property="school.schoolId" column="schoolId"/> 
        <result property="school.schoolName" column="schoolName"/>
    </resultMap>
    
  • 使用association封装查询结果

    Mybatis 学习记录

    Mybatis 学习记录

    • 嵌套结果映射

      <resultMap id="studentResultMap" type="com.shy.entity.Student">
          <id property="id" column="id"/>
          <result property="name" column="name"/>
          <result property="gender" column="gender"/>
          <result property="age" column="age"/>
          <association property="school" javaType="com.shy.entity.School">
              <id property="schoolId" column="schoolId"/>
              <result property="schoolName" column="schoolName"/>
          </association>
      </resultMap>
      
      <!-- 也可以定义在外部, 以便复用 -->
      <resultMap id="schoolResultMap" type="com.shy.entity.School">
          <id property="schoolId" column="schoolId"/>
          <result property="schoolName" column="schoolName"/>
      </resultMap>
      
      <resultMap id="studentResultMap" type="com.shy.entity.Student">
          <id property="id" column="id"/>
          <result property="name" column="name"/>
          <result property="gender" column="gender"/>
          <result property="age" column="age"/>
          <association property="school" resultMap="schoolResultMap"/> <!-- 引用 schoolResultMap -->
      </resultMap>
      
    • 嵌套Select查询

      简单来说, 就是把多表查询分解成了多个简单的查询

      # 原有的多表查询
      SELECT * FROM student, school WHERE student.schoolId = school.schoolId AND id = #{id}; 
      # 分解后的查询
       1.SELECT * FROM student WHERE id = #{id}
       2.SELECT * FROM school WHERE schoolId = #{schoolId}
      
      <!-- studentMapper.xml-->
      <select id="selectStudentById" resultMap="studentResultMap">
          SELECT * FROM student WHERE id = #{id}
      </select>
      
      <resultMap id="studentResultMap" type="com.shy.entity.Student">
          <id property="id" column="id"/>
          <result property="name" column="name"/>
          <result property="gender" column="gender"/>
          <result property="age" column="age"/>
          
      <!--将selectStudentById查出的schoolId传递给com.shy.dao.SchoolMapper.selectSchoolBySchoolId进行查询-->
          <association property="school"
                       select="com.shy.dao.SchoolMapper.selectSchoolBySchoolId" column="schoolId"/>
      </resultMap>
      
      <!-- schoolMapper.xml-->
      <select id="selectSchoolBySchoolId" resultType="com.shy.entity.School">
          SELECT * FROM school WHERE schoolId = #{schoolId}
      </select>
      

      Mybatis 学习记录

      # 延迟加载简单来讲, 就是只有在用到school的相关属性时, 才会执行第2条sql. 从而提高效率
      1.SELECT * FROM student WHERE id = #{id}
      2.SELECT * FROM school WHERE schoolId = #{schoolId}
      
      <association ... fetchType="lazy"/> <!-- 开启延迟加载 -->
      <setting name="lazyLoadingEnabled" value="TRUE"/> <!-- 或通过全局配置开启延迟加载 --->
      

      另外, 有时嵌套的子查询可能需要多个参数, 这是我们可以借助column={属性1=字段1,属性2-字段2}来传递.

      Mybatis 学习记录

1:n关联查询

假设有FootballClub实体

public class FootballClub {
    private int id;
    private String name;
    private List<Player> players; // 一支球队有多名球员
}

public class Player {
    private int id;
    private String name;
}

数据库中有对应表 football_club ,player

Mybatis 学习记录

Mybatis 学习记录

现在我们想要查询出id=1FootballClub的信息并使用Mybatis将结果封装到对象中

SELECT * FROM football_club, player WHERE football_club.fc_id = player.fc_id AND football_club.fc_id = 1

这时, 我们需要借助collection将相关信息封装到List集合中, 同样可以采用嵌套结果映射嵌套SELECT映射两种方式

  • 嵌套结果映射

    <select id="selectFootballClubByFootBallClubId" resultMap="FootballClubResultMap">
        SELECT * FROM football_club, player WHERE football_club.fc_id = player.fc_id AND football_club.fc_id = #{id}
    </select>
    
    <resultMap id="FootballClubResultMap" type="com.shy.entity.FootballClub">
        <id property="id" column="fc_id"/>
        <result property="name" column="fc_name"/>
        <!-- ofType 指定集合内封装的类型(必须指定!)-->
        <collection property="players" ofType="com.shy.entity.Player">
            <id property="id" column="player_id"/>
            <result property="name" column="player_name"/>
        </collection>
    </resultMap>
    
  • 嵌套SELECT映射

    <!-- FootballClub.xml -->
    <resultMap id="FootballClubResultMap" type="com.shy.entity.FootballClub">
        <id property="id" column="fc_id"/>
        <result property="name" column="fc_name"/>
        <!-- 将 fc_id 传递给selectPlayerListByFootballClubId 查出列表封装给players -->
        <collection property="players"  select="com.shy.dao.PlayerMapper.selectPlayerListByFootballClubId" column="fc_id" 
                    ofType="com.shy.entity.Player"/>
    </resultMap>
    <select id="selectFootballClubByFootBallClubId" resultMap="FootballClubResultMap">
        SELECT * FROM football_club WHERE fc_id = #{fc_id}
    </select>
    
    <!-- PlayerMapper.xml-->
    <resultMap id="PlayerResultMap" type="com.shy.entity.Player">
        <id property="id" column="player_id"/>
        <result property="name" column="player_name"/>
    </resultMap>
    <select id="selectPlayerListByFootballClubId" resultMap="PlayerResultMap">
        SELECT * FROM player WHERE fc_id = #{fc_id}
    </select>
    

discriminator(鉴别器)

discriminator可以根据情况关联不同的resultMap

Mybatis 学习记录

sql 可重用代码段

<sql>标签中可以用来定义可重用的 SQL 代码片段,以便在其它语句中使用

<include>标签用来引用已经应以的<sql>标签, 并且在其中可以定义不同的参数值

<sql id="studentColumns"> <!-- 将查询字段抽取出来 -->
    ${table}.id,${table}.name,${table}.gender,${table}.age,${table}.schoolId
</sql>
<select id="selectStudentById" resultMap="studentResultMap">
    SELECT
    <include refid="studentColumns"> <!-- 引用 <sql>片段 -->
        <property name="table" value="student"/> <!-- 设置参数 -->
    </include>
    FROM student WHERE id = #{id}
</select>

动态Sql

Mybatis提供了动态Sql技术, 实现在不同的情况拼接Sql语句, 从而执行不同的Sql语句的功能.

if

if最常见的应用场景就是根据条件包含where子句的一部分

public List<Student> selectStudentListByIfConditions
    (@Param("name")String name, @Param("gender")String gender, @Param("age") Integer age);
<select id="selectStudentListByIfConditions" resultType="com.shy.entity.Student">
    SELECT * FROM student WHERE
    <if test="name != null">name = #{name}</if>
    <if test="gender != null">AND gender = #{gender}</if>
    <if test="age != null">AND age = #{age}</if>
</select>
// 测试
List<Student> studentList = studentMapper.selectStudentListByIfConditions("messi", "male", null);
// SELECT * FROM student WHERE name = "messi" AND gender = "male"
// age 为空没有作为筛选条件

test中的遵循OGNL的语法

又或者update动态更新一部分数据

public int updateStudentInfoByIfConditions
    (@Param("name") String name, @Param("gender")String gender, @Param("age") Integer age, @Param("id") Integer id);
<update id="updateStudentInfoByIfConditions">
    UPDATE student SET
    <if test="name != null">name = #{name},</if>
    <if test="gender != null">gender = #{gender},</if>
    <if test="age != null">age = #{age}</if>
    WHERE id = #{id}
</update>
// 测试
int rows = studentMapper.updateStudentInfoByIfConditions("Leo Messi",null,18,7);
// UPDATE student SET name = "Leo Messi", age = 18 WHERE id = 7;
// gender 为空没有更新

where/set/trim

  • where

    where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除 (只会去除开头多余的AND OR)

    <select id="selectStudentListByIfConditions" resultType="com.shy.entity.Student">
        SELECT * FROM student WHERE
        <if test="name != null">name = #{name}</if>
        <if test="gender != null">AND gender = #{gender}</if>
        <if test="age != null">AND age = #{age}</if>
    </select>
    考虑这个SELECT, 假如测试条件中只有一个gender="male", 那么将要被执行的sql语句为:
    SELECT * FROM student WHERE AND gender = "male"
    显然AND是多余的. 这是我们就要借助 <where> 标签, 其会自动去掉开头的 AND.
    <select id="selectStudentListByIfConditions" resultType="com.shy.entity.Student">
        SELECT * FROM student
        <where>
            <if test="name != null">name = #{name}</if>
            <if test="gender != null">AND gender = #{gender}</if>
            <if test="age != null">AND age = #{age}</if>
        </where>
    </select>
    此时执行的sql语句为:
    SELECT * FROM student WHERE gender = "male"
    
  • set

    set元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号 (只会去掉句末的逗号)

    <update id="updateStudentInfoByIfConditions">
        UPDATE student SET
        <if test="name != null">name = #{name},</if>
        <if test="gender != null">gender = #{gender},</if>
        <if test="age != null">age = #{age}</if>
        WHERE id = #{id}
    </update>
    考虑这个UPDATE, 假如测试条件中只有一个name="梅西", 那么将要被执行的sql语句为:
    UPDATE student SET name = "梅西", WHERE id = 7;
    显然 , 是多余的, 这时我们就要借助<set>标签, 其会自动去掉末尾多余的 ,
    <update id="updateStudentInfoByIfConditions">
        UPDATE student
        <set>
            <if test="name != null">name = #{name},</if>
            <if test="gender != null">gender = #{gender},</if>
            <if test="age != null">age = #{age}</if>
        </set>
        WHERE id = #{id} <!-- 一定要放在set标签外, 不然 ,无法成为末尾 -->
    </update>
    此时执行的sql语句为:
    UPDATE student SET name = "梅西" WHERE id = 7;
    
  • trim

    有时 <where><set> 可能无法满足我们的需求.

    我们便可以使用 <trim> 来自定义 前后缀以及开头末尾要去掉的字符

    Mybatis 学习记录

<!-- 等效于 <update> -->
<trim prefix="SET" suffixOverrides=",">
<!-- 等效于 <where> -->
<trim prefix="WHERE" prefixOverrides="AND">
<trim prefix="WHERE" prefixOverrides="OR">

choose

从多个条件中选择一个使用, 类似于switch

<select id="selectStudentListByChooseCondition" resultType="com.shy.entity.Student">
    SELECT * FROM student
    <trim prefix="WHERE" prefixOverrides="AND">
        <choose>
            <when test="name != null">name = #{name}</when>
            <when test="gender != null">gender = #{gender}</when>
            <otherwise>age = #{age}</otherwise>
        </choose>
    </trim>
</select>

foreach

Mybatis 学习记录

Mybatis 学习记录

  • 使用in进行查询

    <!--    public int selectStudentByIdList(@Param("idList") List<Integer> IdList);-->
    <select id="selectStudentByIdList" resultType="com.shy.entity.Student">
        SELECT * FROM student WHERE id IN
        <foreach collection="idList" item="id" open="(" close=")" separator=",">
            #{id}
        </foreach>
    </select>
    SQL: SELECT * FROM SELECT * FROM student WHERE id IN (1, 2, 3)
    
  • 批量插入

    <!--    public int insertStudentList(@Param("studentList")List<Student> studentList);-->
    <insert id="insertStudentList">
        INSERT INTO student(name, gender, age) VALUES
        <foreach collection="studentList" item="student"  separator=",">
            (#{student.name},#{student.gender},#{student.age})
        </foreach>
    </insert>
    SQL:  INSERT INTO student(name, gender, age) VALUES ("Messi","male",34),("Xavi","male",40);
    
    
    <!--    public int insertStudentList2(@Param("studentList")List<Student> studentList);-->
    <insert id="insertStudentList2">
        <foreach collection="studentList" item="student" separator=";">
      INSERT INTO student(name, gender, age) VALUES (#{student.name}, #{student.gender}, #{student.age})
        </foreach>
    </insert>
    SQL: INSERT INTO student(name, gender, age) VALUES ("Messi","male",34);
         INSERT INTO student(name, gender, age) VALUES ("Xavi","male",40),
    <!-- 此方式需要设置MySQL的全局属性 allowMultiQueries=true(设置此属性,需将其附加在jdbc.url后) -->
    

bind

bind允许你使用OGNL表达式创建一个变量, 以便在需要时引用这个变量

<!-- 例子直接搬官网了 -->
<select id="selectBlogsLike" resultType="Blog">
  <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
  SELECT * FROM BLOG
  WHERE title LIKE #{pattern}
</select>

_parameterMybatis内置的参数, 在没有方法参数使用@Param注解前提下:

若只有一个参数, 则_parameter代表这个参数

若有多个参数, 则_parameter代表这些个参数封装的map.

Mybatis缓存机制

考虑下面一个应用场景:

有时我们在做查询时, 可能执行两次相同的查询拿到同样的数据, 如果第二次查询可以直接引用之前的结果, 那么查询效率就会大大增加.

MyBatis内置了一个强大的事务性查询缓存机制, 可以有效提高查询效率

MyBatis缓存机制包括一级缓存和二级缓存, 同时支持实现Cache接口自定义二级缓存

一级缓存

  • 一级缓存(Local Cache), 默认作用域是SqlSession

    • 同一会话期间查询到的数据会放到一级缓存中, 之后如果需要获取相同的数据, 会直接从一级缓存中得到

      studentMapper.selectStudentById(1);
      studentMapper.selectStudentById(1);
      执行上述两次相同的查询, Mybatis只向数据库发送了一次查询请求
      

      Mybatis 学习记录

    • 一级缓存可以通过全局配置的localCacheScope改变作用域

      Mybatis 学习记录

      如果设置 localCacheScope = STATEMENT,
      即使两次相同的查询, 也需要向数据库请求两次, 一级缓存失效.
      

      Mybatis 学习记录

  • 一级缓存不可以被关闭, 但是可以手动清除

    • sqlSession.clearCache() 手动清除一级缓存

    • CRUD标签的flushCache属性手动清除一级/二级缓存

      CRUD即 增加(Create)、检索(Retrieve)、更新(Update)和删除(Delete)

      <!-- SELECT 默认不刷新Cache -->
      <select id="selectStudentByName" resultType="com.shy.entity.Student" flushCache="false">
          SELECT *
          FROM student
          WHERE name = #{name}
      </select>
      
      <!-- INSERT/UPDATE/DELETE 默认刷新Cache -->
      <insert id="insertStudent" useGeneratedKeys="true" keyProperty="id" flushCache="true">
          INSERT INTO student(name, gender, age)
          VALUES (#{student.name, jdbcType=NULL}, #{student.gender}, #{student.age})
      </insert>
      
  • 一级缓存失效的四种情况

    Mybatis 学习记录

二级缓存

  • 二级缓存的作用域是基于namespace的, 即一个Mapper接口对应一个二级缓存

  • 二级缓存需要手动配置

    • 在全局配置文件中, 通过cacheEnabled = true开启二级缓存

      虽然默认值为true, 不过还是推荐显式地配置该属性, 即使与默认值相同

      Mybatis 学习记录

    • 在需要使用二级缓存的mapper.xml文件中添加<cache/>标签配置二级缓存

      • <cache>标签的属性

        Mybatis 学习记录

    • 对应的POJO要实现Serializable接口

  • 二级缓存的工作流程

    • 二级缓存中的数据全部来自一级缓存: 当SqlSession关闭后,该会话对应的一级缓存中的数据才会被保存到对应的二级缓存中
    • Mybatis执行CRUD操作时, 会依次到二级缓存/一级缓存中查找缓存, 若没有相应缓存, 则执行SQL语句
    // 在开启二级缓存后, 每执行一条CRUD操作. 控制台中都会打印出缓存命中率(缓存命中/查找次数)
    [main] [com.shy.dao.StudentMapper]-[DEBUG] Cache Hit Ratio [com.shy.dao.StudentMapper]: 0.5
    
  • 二级缓存支持手动请除

    • CRUD标签的flushCache属性手动清除一级/二级缓存
    • SELECT标签的useCache属性可以配置该查询是否使用二级缓存
    • 当进行了insert/update/delete操作后, 二级缓存将被刷新

自定义缓存

MyBatis支持通过实现Cache接口来自定义缓存

public class MyCache implements Cache {
    @Override
    public String getId() {
        return null;
    }

    @Override
    public void putObject(Object key, Object value) {}
    
    @Override
    public Object getObject(Object key) {
        return null;
    }

    @Override
    public Object removeObject(Object key) {
        return null;
    }

    @Override
    public void clear() {}

    @Override
    public int getSize() {
        return 0;
    }
}

不过在实战中, 我们往往选用更优秀的第三方缓存整合到Mybatis中

Mybatis的github仓库中可以找到许多整合方案

缓存机制流程

Mybatis 学习记录

SSM整合

由于Spring MVC还没有学习, 此章节稍后补充

Mybatis Generator

Mybatis 学习记录

更多详细信息参考 Mybatis Generator 官方文档 (只有英文)

  • 引入依赖

    <dependency>
      <groupId>org.mybatis.generator</groupId>
      <artifactId>mybatis-generator-core</artifactId>
      <version>1.4.0</version>
    </dependency>
    
  • 编写Mybatis Generator配置文件

    Mybatis 学习记录

    <?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>
        <!-- 从类路径导入相关JAR包 不过我们使用maven管理JAR包-->
        <!-- <classPathEntry location="/Program Files/IBM/SQLLIB/java/db2java.zip" />-->
    
        <!-- 读取外部.properties文件-->
        <properties resource="dbconfig.properties"/>
    
        <!-- context中用于配置MBG的一些属性   targetRuntime 指定MBG生成代码的风格(具体参考文档)-->
        <context id="MysqlTables" targetRuntime="MyBatis3">
    
            <!--指定如何连接到目标数据库-->
            <jdbcConnection driverClass="${jdbc.driver}"
                            connectionURL="${jdbc.url}"
                            userId="${jdbc.username}"
                            password="${jdbc.password}">
            </jdbcConnection>
    
            <!-- 指定MBG如何 JDBC类型和JAVA类型相互转换时的策略 这里我们使用默认配置,详细信息参考文档-->
            <javaTypeResolver >
                <property name="forceBigDecimals" value="false" />
            </javaTypeResolver>
    
            <!-- 指定JavaBean生成的包和工程的位置, 以及JavaBean时的策略(默认配置, 详细信息参考文档)-->
            <!-- 路径一定要回, 不然什么东西都不会生成
            <javaModelGenerator targetPackage="com.shy.model" targetProject="src/main/java">
                <property name="enableSubPackages" value="true" />
                <property name="trimStrings" value="true" />
            </javaModelGenerator>
    
            <!-- 指定mapper映射文件生成的包和工程的位置, 以及生成时的策略(默认配置, 详细信息参考文档)-->
            <sqlMapGenerator targetPackage="mapper"  targetProject="src/main/resources">
                <property name="enableSubPackages" value="true" />
            </sqlMapGenerator>
    
            <!-- 指定Mapper接口生成的类型, 包和工程的位置, 以及生成时的策略(默认配置, 详细信息参考文档)-->
            <javaClientGenerator type="XMLMAPPER" targetPackage="com.shy.mapper"  targetProject="src/main/java">
                <property name="enableSubPackages" value="true" />
            </javaClientGenerator>
    
            <!-- table用于指定根据那些表创建JavaBean以及Mapper-->
            <!-- schema 数据库中的哪个模式  tableName 哪张表 domainObjectName 生成的Bean的名称 -->
            <table schema="mybatis" tableName="student" domainObjectName="Student" >
                <!-- 一些生成策略 详细信息参考文档-->
                <!--            <property name="useActualColumnNames" value="true"/>-->
                <!--            <generatedKey column="ID" sqlStatement="DB2" identity="true" />-->
                <!--            <columnOverride column="DATE_FIELD" property="startDate" />-->
                <!--            <ignoreColumn column="FRED" />-->
                <!--            <columnOverride column="LONG_VARCHAR_FIELD" jdbcType="VARCHAR" />-->
            </table>
            <table tableName="actor" domainObjectName="Actor"/>
    
        </context>
    </generatorConfiguration>
    
  • 运行Mybatis Generator

    运行MBG有多种方式, 这里我们选择Running MBG from Java with an XML Configuration File

    其他方式参考MyBatis Generator Core – Running MyBatis Generator

    // 生成代码照搬官网
    @Test
    public void test() throws Exception {
        List<String> warnings = new ArrayList<String>();
        boolean overwrite = true;
        File configFile = new File("src/main/resources/MBGConfig.xml"); // 加载MBG配置文件
        ConfigurationParser cp = new ConfigurationParser(warnings);
        Configuration config = cp.parseConfiguration(configFile);
        DefaultShellCallback callback = new DefaultShellCallback(overwrite);
        MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
        myBatisGenerator.generate(null);
    }
    
  • 生成的entity,mapper接口,mapper映射文件

    Mybatis 学习记录

    其中我们可以通过生成的XXXexample类构造任何查询条件

    参考: mybatis中关于example类详解 - 知乎 (zhihu.com)

    Mybatis自动生成的Example类的使用与解析_莫离瑜的博客-CSDN博客

    Mybatis 学习记录

​ 配合生成的mapper接口进行CRUD操作

Mybatis 学习记录

mapper.xml可以看出, MBG通过动态sql构造条件

<sql id="Example_Where_Clause">
    <!--
      WARNING - @mbg.generated
      This element is automatically generated by MyBatis Generator, do not modify.
      This element was generated on Sat Aug 28 08:43:33 CST 2021.
    -->
    <where>
        <foreach collection="oredCriteria" item="criteria" separator="or">
            <if test="criteria.valid">
                <trim prefix="(" prefixOverrides="and" suffix=")">
                    <foreach collection="criteria.criteria" item="criterion">
                        <choose>
                            <when test="criterion.noValue">
                                and ${criterion.condition}
                            </when>
                            <when test="criterion.singleValue">
                                and ${criterion.condition} #{criterion.value}
                            </when>
                            <when test="criterion.betweenValue">
                                and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
                            </when>
                            <when test="criterion.listValue">
                                and ${criterion.condition}
                                <foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">
                                    #{listItem}
                                </foreach>
                            </when>
                        </choose>
                    </foreach>
                </trim>
            </if>
        </foreach>
    </where>
</sql>
  • 测试
@Test
public void test16() throws IOException {
    SqlSessionFactory sqlSessionFactory = MybatisUtil.getSqlSessionFactory();
    SqlSession sqlSession = sqlSessionFactory.openSession();
    try {
        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
        StudentExample studentExample = new StudentExample();

        // Criteria 用来封装查询条件, 内部用and连接
        StudentExample.Criteria studentExampleCriteria = studentExample.createCriteria();
        studentExampleCriteria.andGenderEqualTo("male");
        studentExampleCriteria.andAgeEqualTo(18);

        StudentExample.Criteria studentExampleCriteria1 = studentExample.createCriteria();
        studentExampleCriteria1.andNameLike("%e%");
        // 不同的Criteria之间可以用or连接
        studentExample.or(studentExampleCriteria1);

        studentMapper.selectByExample(studentExample);
    }finally {
        sqlSession.close();
    }
}
// Mybatis执行的sql:
//[com.shy.mapper.StudentMapper.selectByExample]-[DEBUG] ==>  Preparing: select id, name, gender, age, schoolId from student WHERE ( gender = ? and age = ? ) or( name like ? )
//[main] [com.shy.mapper.StudentMapper.selectByExample]-[DEBUG] ==> Parameters: male(String), 18(Integer), %e%(String)

Mybatis运行原理

此部分建议参考 Mybatis源码分析视频

/**
     * DEBUG模式简单学习Mybatis运行原理
     */
@Test
public void test17() throws IOException {
    SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
    // 1.获取sqlSessionFactory对象
    SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
    // 2.获取sqlSession对象
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 3.获取Mapper接口的代理对象
    ActorMapper actorMapper = sqlSession.getMapper(ActorMapper.class);
    // 4.执行CRUD操作
    List<Actor> actorList = actorMapper.selectActorList();
}

在使用Mybatis进行CRUD操作时, 往往需要进行以上4步. 下面结合上述视频中的时序图, 在debug模式下简单学习一下Mybatis运行原理

  • 获取sqlSessionFactory对象

    Mybatis 学习记录

    Mybatis通过相关解析器解析全局配置文件中的信息, 以及各个mapper.xml中的信息

    (包括封装了sql语句的MappedStatement对象, 以及封装了创建各个mapper接口代理对象的工厂的MappedRegisty),

    Mybatis 学习记录

    Mybatis 学习记录

    这些信息封装保存到Configuration对象中. 并通过build()方法, 创建了包含该Configuration对象的DefaultSqlSessionFactory对象

    Mybatis 学习记录

  • 获取sqlSession对象

    Mybatis 学习记录

    DefaultSqlSessionFactory创建了DefaultSqlSession对象, 其包含Configuration对象和Executor对象

    Mybatis 学习记录

    其中, Executor对象是用来执行CRUD操作的

    Mybatis 学习记录

  • 获取Mapper接口的代理对象

    Mybatis 学习记录

    DefaultSqlSession对象通过getMapper()方法创建出对应mapper接口的动态代理对象 mapperProxy,

    其包含了SqlSession(其中有executor, 从而能进行增删改查)对象

    Mybatis 学习记录

  • 执行CRUD操作

    Mybatis 学习记录

    Mybatis 学习记录

Mybatis插件开发

Mybatis 学习记录

开发第一个插件

实现Interceptor接口 → \rightarrow → 编写@Intercepts注解指定拦截哪些对象的哪些方法 → \rightarrow → 注册该Interceptor

package com.shy.plugin;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.javassist.tools.reflect.Metaobject;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;

import java.sql.Statement;
import java.util.Properties;
// 指定拦截哪些对象(Executor/ParameterHandler/ResultSetHandler/StatementHandler)的哪些方法
@Intercepts({
    @Signature(type = StatementHandler.class, method = "parameterize", args = {Statement.class})
})
public class MyPlugin implements Interceptor {

    /**
* 只会拦截目标对象的目标方法
* @param invocation 包含目标对象,方法的一些信息
*/
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //        System.out.println("在原方法执行之前执行代码");
        System.out.println("invocation.getArgs() = " + invocation.getArgs());
        System.out.println("invocation.getMethod() = " + invocation.getMethod());
        System.out.println("invocation.getTarget() = " + invocation.getTarget());
        System.out.println("invocation.getClass() = " + invocation.getClass());
        // SystemMetaObject.forObject获取的metaObject封装了一个originalObject,本例中即StatementHandler的实现类
        MetaObject metaObject = SystemMetaObject.forObject(invocation.getTarget());

        // metaObject.getValue获取属性
        Object boundSql = metaObject.getValue("boundSql");
        System.out.println("boundSql.sql = " + boundSql);
        //        metaObject.setValue("key","value"); 可以设置属性, 前提是有set方法
        Object result = invocation.proceed(); // 执行原对象的原方法
        //        System.out.println("在原方法执行之后执行代码");

        return result;
    }

    /**
* 为target创建代理对象
* @param target 目标对象
* @return 如果target是要拦截的对象, 则返回target的代理对象. 否则直接返回target
*/
    @Override
    public Object plugin(Object target) {
        Object wrap = Plugin.wrap(target, this); // wrap就是来为target创建动态代理对象
        return wrap;
    }

/**
* 将<plugin>标签中的属性设置进来
*/
    @Override
    public void setProperties(Properties properties) {
        Interceptor.super.setProperties(properties);
    }
}
<configuration>
    <plugins>
        <plugin interceptor="com.shy.plugin.MyPlugin">
            <property name="username" value="root"/>
            <property name="password" value="root"/>
        </plugin>
    </plugins>
</configuration>

编写插件之前一定要大致了解Mybatis的运行原理, 否则可能会破坏其运行流程

尤其是要熟悉四大对象Executor/ParameterHandler/ResultSetHandler/StatementHandler的创建时间及一些方法的大致作用

编写的插件会在SqlSessionFactory创建时, 通过configuration.addInterceptor(interceptorInstance);封装到Configuration对象中.

另外, Executor/ParameterHandler/ResultSetHandler/StatementHandler创建之后都会调用interceptorChain.pluginAll()方法, 根据已经注册的Interceptor决定为哪些对象创建代理

假如有多个插件拦截了同一个对象, 则会在创建代理的代理对象, 所以在执行方法时, 先执行SecondPlugin,再执行FirstPlugin插入的代码

Mybatis 学习记录

PageHelper 第三方分页插件

简单学习一下PageHelper的使用, 详细信息请参考: MyBatis 分页插件 PageHelper 官方文档

  • 引入依赖

    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>5.2.1</version>
    </dependency>
    
  • 注册插件

    PageHelper本质上是一个Interceptor, 利用动态代理的方式添加分页逻辑

    <configuration>
        <plugins>
            <plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
        </plugins>
    </configuration>
    
  • 简单使用

    /**
         * 测试PageHelper
         */
    @Test
    public void test() throws IOException {
        SqlSessionFactory sqlSessionFactory = MybatisUtil.getSqlSessionFactory();
        SqlSession sqlSession = sqlSessionFactory.openSession();
        try {
            ActorMapper actorMapper = sqlSession.getMapper(ActorMapper.class);
            // 查询第一页, 一页5条数据
            PageHelper.startPage(1,5); 
            List<Actor> actorList = actorMapper.selectActorList();
            for (Actor actor : actorList) {
                System.out.println(actor.toString());
            }
            // PageInfo类封装了有关分页的信息
            PageInfo pageInfo = new PageInfo(actorList,5);
            // 导航栏应该展示的页码数(如 3 4 5 6 7 )
            int[] navigatepageNums = pageInfo.getNavigatepageNums(); 
        }finally {
            sqlSession.close();
        }
    }
    /**
    Actor{ActorId=16, ActorChineseName='李雪健', ActorOriginName='Xuejian Li', ActorGender='男'}
    Actor{ActorId=17, ActorChineseName='陈道明', ActorOriginName='Daoming Chen', ActorGender='男'}
    Actor{ActorId=18, ActorChineseName='杰森.弗莱明', ActorOriginName='Jason Flemyng', ActorGender='男'}
    Actor{ActorId=19, ActorChineseName='马修.麦康纳', ActorOriginName='Matthew McConaughey', ActorGender='男'}
    Actor{ActorId=20, ActorChineseName='查理.汉纳姆', ActorOriginName='Charlie Hunnam', ActorGender='男'}
    **/
    

Mybatis批量处理Sql

在学习动态sql:foreach时, 曾介绍过利用拼接Sql的方式批量处理Sql语句,但是数据库有时并不支持过长的语句.

其实, 我们可以在创建SQLSession时, 指定ExecutorType=Batch来批量处理Sql

Mybatis 学习记录

Mybatis 学习记录

  • 批处理方式

    @Test
    public void test20() throws IOException {
        SqlSessionFactory sqlSessionFactory = MybatisUtil.getSqlSessionFactory();
        SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
        long start = System.currentTimeMillis();
        try {
            StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
            for (int i = 0; i < 100; i++) {
                studentMapper.insertStudent(new Student("测试","male",18));
            }
            sqlSession.commit();
            long end = System.currentTimeMillis();
            System.out.println("执行时间: " + (end - start) + "ms");
        }finally {
            sqlSession.close();
        }
    }
    
    此方式在commit之后才执行sql, 之前一直在准备参数
    [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] ==>  Preparing: INSERT INTO student(name, gender, age) VALUES (?, ?, ?)
    [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] ==> Parameters: 测试(String), male(String), 18(Integer)
    [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] ==> Parameters: 测试(String), male(String), 18(Integer)
    [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] ==> Parameters: 测试(String), male(String), 18(Integer)
    [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] ==> Parameters: 测试(String), male(String), 18(Integer)
    执行时间: 3642ms
    
  • 原始方式

    [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] ==>  Preparing: INSERT INTO student(name, gender, age) VALUES (?, ?, ?)
    [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] ==> Parameters: 测试(String), male(String), 18(Integer)
    [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] <==    Updates: 1
    [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] ==>  Preparing: INSERT INTO student(name, gender, age) VALUES (?, ?, ?)
    [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] ==> Parameters: 测试(String), male(String), 18(Integer)
    [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] <==    Updates: 1
    执行时间: 7153ms
    

Mybatis 类型处理器

类型处理器, 即TypeHandler,用来将JDBC类型(如varchar)和Java类型(如String)相映射, 完成类型的转换,从而将字段值封装到JavaBean中.

即可以自定义数据库字段与Java属性映射的方式

枚举类型的TypeHandler

Enum默认使用的TypeHandler是EnumTypeHandler

// EnumTypeHandler源码
public class EnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {
    //...
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
        // 在数据库中保存枚举的名称
        if (jdbcType == null) {
            ps.setString(i, parameter.name());
        } else {
            ps.setObject(i, parameter.name(), jdbcType.TYPE_CODE);
        }
        //...
    }

此外, 我们也可以将Enum的TypeHandler更改为EnumOrdinalTypeHandler, 将在数据库中保存枚举的序号(ordinal)

<typeHandlers>
    <!-- javaType 告知Handler要处理的java类型 com.shy.entity.Status为自定义的枚举类-->
    <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="com.shy.entity.Status"/>
</typeHandlers>
// com.shy.entity.Status
public enum Status {
    LOGIN, LOGOUT
}
// EnumOrdinalTypeHandler源码
public class EnumOrdinalTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {
    //...
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
        // 在数据库中保存枚举的序号
        ps.setInt(i, parameter.ordinal());
    }
    //...
}

自定义TypeHandler

// 假如我们更改Status枚举如下所示, 现在想要在数据库中保存状态码code, 而不是名称/序号.
// 并且希望通过数据库中的code,返回对应的枚举对象
// 我们就需要借助自定义TypeHandler 
public enum Status {
    LOGOUT(404,"登出"), LOGIN(200,"登录");
    private Integer code;
    private String msg;

    Status(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    // 根据状态码返回枚举对象
    public static Status getStatusByCode(Integer code){
        if(code == 404) {
            return LOGOUT;
        }else{
            return LOGIN;
        }
    }
}

自定义TypeHandler, 可以实现TypeHandler接口, 或者继承BaseTypeHandler

public class StatusEnumTypeHandler implements TypeHandler {
    /**
* 定义Java类型如何保存进数据库
*/
    @Override
    public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
        Status status = (Status) parameter;
        ps.setString(i,status.getCode().toString()); // 保存状态码, i为index
    }

    /**
* 定义数据库字段如何映射成Java类型
*/
    @Override
    public Object getResult(ResultSet rs, String columnName) throws SQLException {
        int code = Integer.parseInt(rs.getString(columnName));
        // 根据状态码返回Status对象
        Status status = Status.getStatusByCode(code);
        return status;
    }

    @Override
    public Object getResult(ResultSet rs, int columnIndex) throws SQLException {
        int code = Integer.parseInt(rs.getString(columnIndex));
        Status status = Status.getStatusByCode(code);
        return status;
    }

    @Override
    public Object getResult(CallableStatement cs, int columnIndex) throws SQLException {
        int code = Integer.parseInt(cs.getString(columnIndex));
        Status status = Status.getStatusByCode(code);
        return status;
    }
}

注册该TypeHandler

<typeHandlers>
    <typeHandler handler="com.shy.typeHandler.StatusEnumTypeHandler" javaType="com.shy.entity.Status" />
</typeHandlers>

测试插入

@Test
public void test22() throws IOException {
    SqlSessionFactory sqlSessionFactory = MybatisUtil.getSqlSessionFactory();
    try(SqlSession sqlSession = sqlSessionFactory.openSession()){
        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
        Student student = new Student("Russell","male",22,Status.LOGIN);
        studentMapper.insertStudent(student);
        sqlSession.commit();
    }
}

Mybatis 学习记录

测试查询

@Test
public void test22() throws IOException {
    SqlSessionFactory sqlSessionFactory = MybatisUtil.getSqlSessionFactory();
    try(SqlSession sqlSession = sqlSessionFactory.openSession()){
        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
        Student Russell = studentMapper.selectStudentById(20);
        System.out.println(Russell.toString());
    }
}
// Student{id=20, name='Russell', gender='male', age=22, status=LOGIN} 

Mybatis后记

此部分是在系统性学习Mybatis之后, 在实战当中遇到的问题,收获的知识

try-with-resource关闭会话

之前关闭会话时,使用try-finally关闭会话, 但是此方式有可能会在finally中出现异常

SqlSession sqlSession = sqlSessionFactory.openSession();
try {
   // .......
}finally {
    sqlSession.close(); // 有可能会出现异常
}

JDK7之后, 引入了try-with-resource语法糖, Mybatis官方更推荐使用此方式关闭会话

try(SqlSession sqlSession = sqlSessionFactory.openSession()){ // 底层自动调用close关闭
 	// .......
}

详细参考: Java基础之try-with-resource语法糖_江湖人称黑哥的博客-CSDN博客

上一篇:Mybatis传递参数


下一篇:AJAX&JSON