MyBatis

文章目录

  • 1. 简介
  • 2. 第一个程序
  • 3. 全局配置文件
    • 3.1 引入DTD约束
    • 3.2 properties标签
    • 3.3 settings标签 ☢
    • 3.4 typeAliases标签 ☢
    • 3.5 typeHandlers标签
    • 3.6 plugins标签 ☢
    • 3.7 environments标签
    • 3.8 databaseIdProvider标签
    • 3.9 mappers标签 ☢
  • 4. 映射文件
    • 4.1 增删改查
    • 4.2 insert获取自增主键值
    • 4.3 参数处理 ☢
    • 4.4 参数值的获取
    • 4.5 select返回集合 ☢
    • 4.6 自定义映射规则 ☢
      • 关联查询⭐
      • 分步查询⭐
      • 延迟加载⭐
      • collection标签
      • discriminator标签
  • 5. 动态SQL
    • 5.1 if/where
    • 5.2 trim
    • 5.3 choose
    • 5.4 set
    • 5.5 foreach
    • 5.6 两个内置参数
    • 5.7 bind
    • 5.8 sql
  • 6. 缓存机制
    • 6.1 一级缓存
    • 6.2 二级缓存
    • 6.3 第三方缓存
  • 7. MyBatis-Spring整合
    • 7.1 编写数据源配置
    • 7.2 SqlSessionFactoryBean
    • 7.3 SqlSessionTemplate
    • 7.4 加实现类
  • 8. 逆向工程(MBG)
    • 8.1 配置 ☢
    • 8.2 运行
    • 8.3 测试
  • 9. MyBatis工作原理
    • 9.1 分层架构
    • 9.2 运行流程
    • 9.3 总结 ☢
  • 10. 插件开发
    • 10.1 插件原理
    • 10.2 插件实现
    • 10.3 多个插件 ☢
    • 10.4 简单使用
    • 10.5 插件应用
      • PageHelper ☢
      • 批量处理
      • 存储过程
      • typeHandler处理类型

1. 简介

ROM框架发展:JDBC -> DbUtils(QueryRunner) -> JdbcTemplate -> Hibernate -> MyBatis

前三个只能称为简单的工具,sql语句都写在java源代码中,硬编码高耦合。

Hibernate : 全自动全映射ORM(Object Relation Mapping)框架,全部都封装好了,亦在消除sql

缺点:sql语句不能定制优化,由框架自动编写(学习HQL可以自己编写,但学习成本高)。查询一个字段把所有字段都映射了,性能不好。

MyBatis:半自动轻量级框架,把sql编写,放到配置文件中,其他操作框架封装好,sql与java编码分离。sql语句交给程序员编写,不失去灵活性。


2. 第一个程序

  1. 创建数据库对应的Bean实体类 Emp.java

    @Data
    public class Emp implements Serializable {
        private Integer id;
        private String lastName;   // 注意这个和数据库字段名不一样
        private String gender;     // 也可用char
        private String email;
    }
    
  2. 创建全局MyBatis配置文件,数据源环境信息,事务管理器信息…(类路径下) mybatis-config.xml

    
    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="com.mysql.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
                    <property name="username" value="root"/>
                    <property name="password" value="9527"/>
                dataSource>
            environment>
        environments>
    
        
        <mappers>
            <mapper resource="empMapper.xml"/>  
        mappers>
    configuration>
    
  3. 创建sql映射xml文件 ,配置每一个sql和结果的封装规则(类路径下)empMapper.xml

    <mapper namespace="com.sutong.dao.EmpMapping">
        
        <select id="selectEmp" resultType="com.sutong.bean.Emp">
        	select `id`, `last_name` as lastName, `gender`, `email` from t_emp where id = #{id}
      	select>
    mapper>
    
  4. 获取执行sql的对象,去执行sql语句( SqlSession实例 - 这一个对象就代表和数据库的一次会话,用完关闭,和Connection一样都是线程不安全的,每次使用都应该去获取新的对象)

    public class HelloMyBatis01 {
        @Test
        public void test01() throws IOException {
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            SqlSession sqlSession = sqlSessionFactory.openSession();
    
            try {
                // 第一个参数是sql语句的唯一标识符!!!
                // (就是select标签里面的id,但多个文件id可能冲突,所以需要加上命名空间),第二个的执行sql需要的参数
                Emp emp = sqlSession.selectOne("com.sutong.dao.EmpMapping.selectEmp", 1);
                System.out.println(emp); 
            } finally {
                sqlSession.close(); // 始终都要关闭
            }
        }
    }
    
  5. 接口式编程,我们Dao层写的接口可以直接和MyBatis映射文件绑定!MyBatis自动为接口创建代理对象,去执行增删改查

    • namespace必须指定为接口的全类名
    • select标签的id属性是要绑定接口方法的名
    public interface EmpMapping {
        Emp getEmpById(Integer id);
    }
    
    <mapper namespace="com.sutong.dao.EmpMapping">
        
        <select id="getEmpById" resultType="com.sutong.bean.Emp">
        	select `id`, `last_name` as lastName, `gender`, `email` from t_emp where id = #{id}
      	select>
    mapper>
    

    测试:

    //先获取SqlSession实例
    try {
        EmpMapping mapper = sqlSession.getMapper(EmpMapping.class); // 获取接口实现类,
        Emp emp = mapper.getEmpById(1);      // 实际调用接口代理的方法,就不用写上面一大堆sql标识了,还有类型检查!
        System.out.println(emp);
    } finally {
        sqlSession.close();
    }
    

3. 全局配置文件

  • configuration(配置)

    • properties(属性)

    • settings(设置)

    • typeAliases(类型别名)

    • typeHandlers(类型处理器)

    • objectFactory(对象工厂)

    • plugins(插件)

    • environments(环境配置)

      • environment(环境变量)
        • transactionManager(事务管理器)
        • dataSource(数据源)
    • databaseIdProvider(数据库厂商标识)

    • mappers(映射器)

注意:配置的时候要按照上面这个顺序配置!!


3.1 引入DTD约束

MyBatis的xml配置文件约束(语法),(IDEA自带)


DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">


DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

3.2 properties标签

类路径下创建 dbconfig.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis
jdbc.username=root
jdbc.password=9527

使用:


<properties resource="dbconfig.properties"/> 

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

3.3 settings标签 ☢

这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。里面很多设置。!!

  1. mapUnderscoreToCamelCase :是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。这样就可以不用查询的时候在sql语句中用as取别名了。默认值是false
  2. jdbcTypeForNull 当没有为参数指定特定的 JDBC 类型时,空值的默认 JDBC 类型。默认是OTHER
  3. lazyLoadingEnabled 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。默认是fasle
  4. aggressiveLazyLoading开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载。默认false(3.4.1前默认是true)
  5. cacheEnabled全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。默认true
  6. localCacheScope MyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。 默认值为 SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。
  7. …等多设置看MyBatis官网 ,官网:配置

原则:即使的默认的,我们确认开启的设置都要显示的配置出来!

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

3.4 typeAliases标签 ☢

类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写


<typeAliases>
    
    <typeAlias type="com.sutong.bean.Emp"/>
    
    
    <package name="com.bean"/>
typeAliases>

使用:

<select id="getEmpById" resultType="emp">
    select * from t_emp where id = #{id}
select>

批量区别名问题:当多包下有相同的类时,都取默认别名,则MyBatis就会报错,这时可以在重名类上面使用@Alias注解,给个别类单独起个别名 @Alias("emp01"),就不冲突了。

MyBatis为我们起好了一些别名:

  • 别名:_int <–> 映射的类型:int (基本类型就是前面加下划线)
  • integer <–> Integer (基本类型的包装类都是首字母小写)
  • string <–> String
    MyBatis_第1张图片

3.5 typeHandlers标签

类型处理器:在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时,都用类型处理器将获取到的值以合适的方式转换成 Java 类型。

Mybatis 3.4.5后添加Java8的新日期API支持

MyBatis_第2张图片
​ …


3.6 plugins标签 ☢

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

后面讲!!


3.7 environments标签

将 SQL 映射应用于多种数据库之中。记住:尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。


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

    <environment id="test">
        <transactionManager type="..."/>
        <dataSource type="...">dataSource>
    environment>
environments>

3.8 databaseIdProvider标签

可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性。


<databaseIdProvider type="DB_VENDOR">
    
    <property name="MySQL" value="mysql"/>
    <property name="Oracle" value="oracle" />
    <property name="SQL Server" value="sqlserver"/>
    <property name="DB2" value="db2"/>
databaseIdProvider>

在sql映射文件select标签上加上databaseId属性,值为上面的别名


<select id="getEmpById" resultType="emp" databaseId="mysql">
	select `id`, `last_name`, `gender`, `email` from t_emp where id = #{id}
select>


<select id="getEmpById" resultType="emp" databaseId="oracle">
	select `id`, `last_name`, `gender`, `email` from t_emp where id = #{id}
select>

3.9 mappers标签 ☢

1.引用配置文件

将sql映射注册到全局配置中。

<mappers>
    
    <mapper resource="empMapper.xml"/> 
mappers>

2.引用(注册)接口

mapper 还有一个属性class :引用(注册)接口

<mappers>
    <mapper class="com.sutong.dao.EmpMapping"/>
mappers>
  • 如果有sql映射文件,映射文件名必须和接口同名,并且放在一个目录下

    IDEA中不会把java包下的.java以外的文件放到classes里面。可以在resources目录下,创建一个和java包下与dao层同名的包resources.com.sutong.dao.EmpMapper.xml,编译后会自动合并的

    // 接口main.java.com.sutong.dao.EmpMapping.java
    public interface EmpMapping {
        Emp getEmpById(Integer id);
    }
    
    
    <mapper namespace="com.sutong.dao.EmpMapping">
        <select id="getEmpById" resultType="emp" databaseId="mysql">
            select `id`, `last_name`, `gender`, `email` from t_emp where id = #{id}
        select>
    mapper>
    
  • 如果没有映射文件,所有的sql语句都是利用注解写在接口上(这种还不如xml,如果改还要动源码,而且sql长了就麻烦了)

    public interface EmpMapping {
        // Update,Delete等注解都有...
        @Select("select `id`, `last_name`, `gender`, `email` from t_emp where id = #{id}")
        Emp getEmpById(Integer id);
    }
    

3.批量注册

<mappers>
     
    <package name="com.sutong.dao"/>
mappers>

总结:比较重要的复杂的Dao接口来写sql映射文件,不重要的简单的Dao接口为了开发快速可以使用注解!


4. 映射文件

  • cache – 该命名空间的缓存配置。
  • cache-ref – 引用其它命名空间的缓存配置。
  • resultMap – 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
  • sql – 可被其它语句引用的可重用语句块。
  • insert – 映射插入语句。
  • update – 映射更新语句。
  • delete – 映射删除语句。
  • select – 映射查询语句。

4.1 增删改查

接口:

public interface EmpMapper {
    Emp getEmpById(Integer id);

    void addEmp(Emp emp);

    void updateEmp(Emp emp);

    void deleteEmp(Integer id);
}

映射文件:

<mapper namespace="com.sutong.dao.EmpMapper">
    <select id="getEmpById" resultType="com.sutong.bean.Emp" databaseId="mysql">
        select `id`, `last_name`, `gender`, `email` from t_emp where id = #{id}
    select>
    
    
    <insert id="addEmp" parameterType="com.sutong.bean.Emp">
        insert t_emp(`last_name`, `gender`, `email`) values(#{lastName}, #{gender}, #{email})
    insert>

    
    <update id="updateEmp">
        update t_emp
        set `last_name` = #{lastName}, `gender` = #{gender}, `email` = #{email}
        where `id` = #{id}
    update>

    <delete id="deleteEmp">
        delete from t_emp where `id` = #{id}
    delete>
mapper>

测试:

// 先获取sqlSessionFactory...

// openSession()方法可以传个boolean值,代表是否自动提交,默认不自动提交!!
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
    EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); // 获取接口实现类
    Emp emp = mapper.getEmpById(1); // 查
    mapper.addEmp(new Emp(null, "sutong", "1", "[email protected]")); // 增
    mapper.updateEmp(new Emp(1, "Tom", "1", "[email protected]")); // 改
    mapper.deleteEmp(4); // 删

    sqlSession.commit(); // 手动提交!!
} finally {
    sqlSession.close();
}

Mybati允许增删改直接定义 Integer,Long,Boolean,void 类型的返回值,会帮我们自动封装好!!

例如:Long addEmp(Emp emp); 映射文件中不用写关于返回值的配置!


4.2 insert获取自增主键值


<insert id="addEmp" useGeneratedKeys="true" keyProperty="id">
    insert t_emp(`last_name`, `gender`, `email`) values(#{lastName}, #{gender}, #{email})
insert>

测试:

Emp e = new Emp(null, "haha", "0", "[email protected]");
mapper.addEmp(e); // 增
System.out.println(e.getId()); // id就有值了

Oracle不支持自增,Oracle使用序列来模拟自增,insert标签里面需要使用一个selectKey标签…


4.3 参数处理 ☢

1.单个参数,MyBatis不会做特殊处理

#{参数名} 取出(参数名可以随便写,因为就一个参数!!)

当一个参数是Collection类型(List,Set)或者是数组类型就会特殊处理了

Collection类型的key:collection #{collection[0]} ,使用#{param1}不行

List类型的key:list #{list[0]}

数组类型:array #{array[0]}

单个参数加了@Param注解也会特殊处理

2.多个参数,特殊处理。

// 接口方法
Emp getEmpByIdAndLastName(Integer id, String lastName);

<select id="getEmpByIdAndLastName" resultType="emp">
    select `id`, `last_name`, `gender`, `email` from t_emp where id = #{id} and `last_name` = #{lastName}
select>

// 测试
Emp emp = mapper.getEmpByIdAndLastName(1, "Tom"); // 报错

上面这样会报错!因为多个参数会被封装为一个Map,规则:key就是param1...paramN(或者参数的索引也可以),value就是传入的参数值

<select id="getEmpByIdAndLastName" resultType="emp">
    select `id`, `last_name`, `gender`, `email` 
    from t_emp where id = #{param1} and `last_name` = #{param2}
select>

3.命名参数:明确封装时Map的key,接口方法参数使用注解@Param

Emp getEmpByIdAndLastName(@Param("id") Integer id, @Param("lastName") String lastName);
<select id="getEmpByIdAndLastName" resultType="emp">
    select `id`, `last_name`, `gender`, `email` from t_emp where id = #{id} and `last_name` = #{lastName}
select>

4.如果多个参数正好是业务逻辑的数据模型,直接传入pojo就行了

#{属性名} 就可以取出pojo的属性值

5.如果不是业务路径数据模型,我们可以直接传入Map

接口方法:Emp getEmpByMap(Map map);

#{key}直接取map中的value就行

6.不是业务路径数据模型,而且经常使用,推荐编写一个TO(Transfer Object)数据传输对象,例如Page对象等


4.4 参数值的获取

两者效果一样的,但有一点区别

  1. #{} 是以预编译的形式,将参数设置到sql语句中,PreparedStatement?占位符,可以防止SQL注入
  2. ${} 取出是值直接拼装在sql语句中,Statement,会有安全问题

大多数情况下我们取参数值都应该取使用#{}。原生Jdbc不支持占位符的地方,需要sql拼接是时候可以使用$(),

例如分表拆分 select * from ${year}_salary where xxx,排序select * from t_user oder by ${f_name} ${order}等。

#{} 的丰富用法:

规定参数的一些规则:

javaType,jdbcType,mode(存储过程),numericScale

resultMap,typeHandler,jdbcTypeName,expression(未来准备支持的)

jdbcType(数据库类型)通常在某种情况下需要被设置,我们在数据为null的时候有些数据库可能不能识别mybatis对null的默认处理,例如Oracle(报错),jdbcType OTHER无效的类型。

因为mybatis对所有的null都默认映射为原生的jdbc OTHER 类型,Oracle不能正确。

<insert id="addEmp" parameterType="com.sutong.bean.Emp">
    insert t_emp(`last_name`, `gender`, `email`) 
    values(#{lastName}, #{gender}, #{email, jdbcType=NULL})    
insert>

或者改全局设置 这个值默认是OTHER


4.5 select返回集合 ☢

返回List, Map类型!!

public interface EmpMapper {
    
    List<Emp> getEmpsByLastNameLike(String lastName);
    
    // 返回一条记录的Map,key是列名,value就是该字段对应的值
    // 例如:{gender=1, last_name=Tom, id=1, [email protected]}
    Map<String, Object> getEmpByIdReturnMap(Integer id);
    
    // 多条记录封装为一个Map,key是这条记录的主键(下面注解可指定),value是该记录封装后的JavaBean
    // 例如:{2=Emp{id=2, lastName='Jack', gender='1', email='[email protected]'}, 
    // 			5=Emp{id=5, lastName='haha', gender='0', email='[email protected]'}}
    @MapKey("id")  // 告诉mybatisMap的封装这个map的时候那个属性作为map的key
    Map<Integer, Emp> getEmpsByLastNameReturnMap(String lastName);
}
<mapper namespace="com.sutong.dao.EmpMapper">
    
    <select id="getEmpsByLastNameLike" resultType="com.sutong.bean.Emp">
        select * from t_emp where `last_name` like #{lastNamr}
    select>

    
    <select id="getEmpByIdReturnMap" resultType="map">
        select `id`, `last_name`, `gender`, `email` from t_emp where id = #{id}
    select>
    
    
    <select id="getEmpsByLastNameReturnMap" resultType="com.sutong.bean.Emp" >
        select * from t_emp where `last_name` like #{lastName}
    select>
mapper>    

4.6 自定义映射规则 ☢

resultMap 可以自定义映射规则(1.as起别名 2.设置setting 3.使用resultMap)

public interface EmpMapperPlus {
    Emp getEmpById(Integer id);
}

映射文件:

<mapper namespace="com.sutong.dao.EmpMapperPlus">

    
    <resultMap id="MyEmp" type="com.sutong.bean.Emp">
        
        <id column="id" property="id"/>
        
        <result column="last_name" property="lastName"/>
        <result column="gender" property="gender"/>
        <result column="email" property="email"/>
        
    resultMap>

    
    <select id="getEmpById" resultMap="MyEmp">
        select `id`, `last_name`, `gender`, `email` from t_emp where id = #{id}
    select>
mapper>

关联查询⭐

resultMap 更强大的功能:关联查询

例如:查询每个员工Emp对应的部门Dept信息:

public class Dept {
    private Integer id;
    private String deptName;
}

public class Emp {
    private Integer id;
    private String lastName;  
    private String gender;   
    private String email;
    private Dept dept;
}

// Dao接口方法
public interface EmpMapperPlus {
    Emp getEmpAndDept(Integer id);
}

映射文件:

<mapper namespace="com.sutong.dao.EmpMapperPlus">
    
	
    <resultMap id="MyDifEmp" type="com.sutong.bean.Emp">
        <id column="id" property="id"/>
        <result column="last_name" property="lastName"/>
        <result column="gender" property="gender"/>
        <result column="email" property="email"/>
        <result column="d_id" property="dept.id"/>  
        <result column="dept_name" property="dept.deptName"/>
        
        
        
        <association property="dept" javaType="com.sutong.bean.Dept">
            
            <id column="d_id" property="id"/> 
            <result column="dept_name" property="deptName"/>
        association>
    resultMap>
    
    
    <select id="getEmpAndDept" resultMap="MyDifEmp">
        select 
        	e.id, e.last_name, e.gender, e.email, e.d_id, d.dept_name
        from  
        	t_emp e
        join 
        	t_dept d
        on 
        	e.d_id = d.id
        where 
        	e.id = #{id}
    select>
mapper>

分步查询⭐

association标签还能分步查询:类似子查询!!组合已有的方法完成复杂功能

  1. 先根据员工id查询员工信息
  2. 根据员工信息中的d_id值取部门表查出部门信息
  3. 部门信息设置到员工信息中
<mapper>
	
    <resultMap id="MyEmpStep" type="com.sutong.bean.Emp">
        <id column="id" property="id"/>
        <result column="last_name" property="lastName"/>
        <result column="gender" property="gender"/>
        <result column="email" property="email"/>

        
        <association property="dept" select="com.sutong.dao.DeptMapper.getDeptById" column="d_id"/>
    resultMap>

    <select id="getEmpByIdStep" resultMap="MyEmpStep">
    	select `id`, `last_name`, `gender`, `email`, `d_id` from t_emp where id = #{id}
    select>
mapper>

前提是必须Dept的Dao层得有getDeptById这个方法和对应的映射文件,这个在实际中基本都有的!!

在分步查询过程中,如果需要多列值传入过去,怎么办呢?则需要将多列的值封装到Map中传递,

column="{key1=column1, key2=column2}" (= 换成:也行)


延迟加载⭐

分布查询还能实现 延迟加载/按需加载/懒加载!!

每次查询Emp对象的时候,Dept都跟着一起查出来了

延迟加载:Dept信息在我们使用的时候再去查询!!只需要在分步查询的基础上加上两个配置就能完成

全局配置:

<settings>
    <setting name="lazyLoadingEnabled" value="true"/> 
    <setting name="aggressiveLazyLoading" value="false"/>
settings>

collection标签

collection标签定义关联集合封装规则!!!

例如:查询部门的时候把部门下的所有员工都查出来!!

public class Dept {
    private Integer id;
    private String deptName;
    private List<Emp> emps;
}

// 接口方法:
public interface DeptMapper {
    Dept getDeptByIdPlus(Integer id);
    
    Dept getDeptByIdStep(Integer id);
}

1.关联查询的映射文件(嵌套结果集的方式):

<mapper>
    
    <resultMap id="MyDept" type="com.sutong.bean.Dept">
        <id column="d_id" property="id"/>
        <result column="dept_name" property="deptName"/>
        
        <collection property="emps" ofType="com.sutong.bean.Emp">
            
            <id column="e_id" property="id"/>
            <result column="last_name" property="lastName"/>
            <result column="gender" property="gender"/>
            <result column="email" property="email"/>
        collection>
    resultMap>

    <select id="getDeptByIdPlus" resultMap="MyDept">
        select
        	d.id as d_id, d.dept_name, e.id as e_id, e.last_name, e.gender, e.email
        from
        	t_dept d
        left join
        	t_emp e
        on
        	d.id = e.d_id
        where
        	d.id = #{id}
    select>
mapper>

2.分布查询的映射文件:(只要上面那两个设置没关闭是有延迟查询的)

  • 先在EmpMapperPlus.xml里面定义一个使用d_id查询员工返回List 的方法

    <select id="getEmpsByDeptId" resultType="com.sutong.bean.Emp">
        select `id`, `last_name`, `gender`, `email`, `d_id` from t_emp where d_id = #{deptId}
    select>
    
  • 封装到Dept的List集合中

    <mapper>
        
        <resultMap id="MyDeptStep" type="com.sutong.bean.Dept">
            <id column="id" property="id"/>
            <result column="dept_name" property="deptName"/>
            
            <collection property="emps" 
                        select="com.sutong.dao.EmpMapperPlus.getEmpsByDeptId" 
                        column="id"/>
        resultMap>
    
        <select id="getDeptByIdStep" resultMap="MyDeptStep">
            select id, dept_name from t_dept where id = #{id}
        select>
    mapper>
    

collectionassociation标签里还有个属性fetchType="lazy" 表示延迟加载 ,如果这条查询不需要延迟则可设置为eager


discriminator标签

discriminator:鉴别器。可以根据鉴别器根据某列的值改变封装行为。(了解即可)

例如:查Emp,如果查出的是女生就把部分信息查询出来,否则不查询。

下面利用分步查询的基础上加了个鉴别器!!

<resultMap id="MyEmpDis" type="com.sutong.bean.Emp">
    <id column="id" property="id"/>
    <result column="last_name" property="lastName"/>
    <result column="gender" property="gender"/>
    <result column="email" property="email"/>

    
    <discriminator javaType="string" column="gender">
        
        <case value="0" resultType="com.sutong.bean.Emp">
            <association property="dept" select="com.sutong.dao.DeptMapper.getDeptById" column="d_id"/>
        case>
        
        <case value="1" resultType="com.sutong.bean.Emp"/> 
    discriminator>
resultMap>

5. 动态SQL

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

5.1 if/where

例如:查询员工,要求如果携带了id字段和lastName字段则按照这两个查询,两者携带那一个就按哪一个查询。

if其实就是根据条件拼接SQL语句!

<mapper namespace="com.sutong.dao.EmpMapperDynamicSQL">
    <select id="getEmpsByConditionIf" resultType="com.sutong.bean.Emp">
        select `id`, `last_name`, `gender`, `email` from t_emp
        where 1 = 1 
        
        <if test="id != null">
            and id = #{id}
        if>
        <if test="lastName != null and lastName != ''">
            and last_name = #{lastName}
        if>
    select>
mapper>

id不传可能会导致语SQL法错误,解决方案:

1.where后面写个1=1,后面所有条件都加上and(看上面)

2.使用where标签将所有的查询条件包括(MyBatis推荐),MyBatis会帮我们第一个多出来的and或者or

<select id="getEmpsByConditionIf" resultType="com.sutong.bean.Emp">
    select `id`, `last_name`, `gender`, `email` from t_emp
    <where>
        <if test="id != null">  
            id = #{id}
        if>
        <if test="lastName != null and lastName != ''">
            and last_name = #{lastName}
        if>
    where>
select>

还能这样写:(OGNL表达式甚至还能调用静态方法使用@,可看官方文档)

<if test="email != null and email.trim() != ''">
   	and email = #{email}
if>

/* gender是字符串可以直接和数组比较,会自动转换*/
<if test="gender == 0 or gender == 1">
   	and gender = #{gender}
if>

5.2 trim

可以用trim标签解决后面多出的and或者or!!(了解)

<select id="getEmpsByConditionIf" resultType="com.sutong.bean.Emp">
    select `id`, `last_name`, `gender`, `email` from t_emp
    
    <trim prefix="where" suffixOverrides="and">
        <if test="id != null">
            id = #{id} and
        if>
        <if test="lastName != null and lastName != ''">
            last_name = #{lastName}
        if>
    trim>
select>

5.3 choose

类似Java中的swtich-case,只会选一个!!

<select id="..." resultType="com.sutong.bean.Emp">
    select `id`, `last_name`, `gender`, `email` from t_emp
    <where>
        <choose>
            <when test="id != null">
                id = #{id}
            when>
            <when test="lastName != null">
                last_name = #{lastName}
            when>
            <otherwise>  
                1 = 1
            otherwise>
        choose>
    where>
select>

5.4 set

带那一列的值就更新那一列(当然这个set也能用trim标签替换)

<update id="updateEmp">
    update t_emp
    
    <set>
        <if test="lastName != null">
            last_name = #{lastName},
        if>
        <if test="gender == 0 or gender = 1">
            gender = #{gender},
        if>
        <if test="email != null">
            email = #{email}
        if>
    set>
    where id = #{id}
update>

5.5 foreach

例如:查询指定id的员工,可多个!

public interface EmpMapperDynamicSQL {
    // 批量查询
    List<Emp> getEmpsByConditionForEach(@Param("ids") List<Integer> ids);
    
    // 批量插入
    void addEmps(@Param("emps") List<Emp> emps);
}

批量查询:

<select id="getEmpsByConditionForEach" resultType="com.sutong.bean.Emp">
    select `id`, `last_name`, `gender`, `email` from t_emp where `id` in
    
    <foreach collection="ids" item="item_id" separator="," open="(" close=")">
        #{item_id}
    foreach>
select>

批量保存:

  1. 第一种(推荐)

    
    <insert id="addEmps">
        insert into t_emp(`last_name`, `gender`, `email`, `d_id`) values
        <foreach collection="emps" item="emp" separator=",">
            (#{emp.lastName}, #{emp.gender}, #{emp.email}, #{emp.dept.id})
        foreach>
    insert>
    
  2. 第二种(这种还可以用于其他的批量操作!!删除修改都行)

     jdbc.url=jdbc:mysql://localhost:3306/mybatis?allowMultiQueries=true
    
    
    <insert id="addEmps">
        <foreach collection="emps" item="emp" separator=";">
            insert into t_emp(`last_name`, `gender`, `email`, `d_id`)
            values(#{emp.lastName}, #{emp.gender}, #{emp.email}, #{emp.dept.id});
        foreach>
    insert>
    

Oracle不支持values(),(),()语法。支持这种:

#1.多条insert放到begin end之间
begin
	insert into t_emp(`id`, `last_name`, `gender`, `email`)
	values(t_emp_seq.nextVal, 'test01', '1', '[email protected]');
	insert into t_emp(`id`, `last_name`, `gender`, `email`)
	values(t_emp_seq.nextVal, 'test02', '0', '[email protected]');
end;
#2.利用中间表
insert into t_emp(`id`, `last_name`, `gender`, `email`)
	select t_emp_seq.nextVal, `last_name`, `gender`, `email` from(
        select 'test01' as last_name , '1' as gender, '[email protected]' as email from dual
        union
        select 'test02' as last_name, '0' as gender, '[email protected]' as email  from dual
    )

5.6 两个内置参数

_parameter代表整个参数。

如果单个参数则_parameter就代表这个参数。如果多个参数底层封装为Map,则_parameter就代表这个Map

_databaseId:如果配置了DatabaseIdProvider标签(支持多厂商数据库的标签),那么就代表当前厂商数据库的别名。

<select id="getEmps" resultType="com.sutong.bean.Emp">
    <if test="_databaseId == 'mysql'">
        select * from t_emp
        <if test="_parameter != null">  
            where id = #{id}
        if>
    if>
    
    <if test="_databaseId == 'oracle'">
        select * from emps
    if>
select>

5.7 bind

在增删改查标签里面,还有个bind标签,可以OGNL表达式的值绑定到一个变量中,方便后来引用这个变量。

<select id="getEmp" resultType="com.sutong.bean.Emp">
    <bind name="_lastName" value="'%' + lastName + '%'"/>
    select * from e_emp where last_name like #{_lastName}  
    
select>

但还是推荐在传参是时候加上模糊查询规则:

Emp emp = mapper.getEmp("%a%");


5.8 sql

sql可以抽取可重用的sql片段,方便后面引用。经常用于抽取查询插入时的很多字段。


<sql id="insertColumn">
    `last_name`, `gender`, `email`, `d_id`
sql>

<insert id="addEmp">
    
    insert into t_emp( <include refid="insertColumn">include> ) 
    values (#{emp.lastName}, #{emp.gender}, #{emp.email}, #{emp.dept.id})
insert>

6. 缓存机制

MyBatis包含了一个非常强大的查询缓存特性,它可以非常方便的配置和定制。极大提升了查询效率。默认定义了两级缓存。


6.1 一级缓存

一级缓存(本地缓存):与数据库同一次会话期间查询到的数据会放到本地缓存中。以后如果需要获取相同的数据,直接从缓存中拿,没必要再去查询数据库。

也叫sqlSession级别的一个Map(每个sqlSession都有一个缓存),是一直存在的,我们没办法关闭。

try {
    EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
    Emp emp01 = mapper.getEmpById(1);
    System.out.println(emp01);

    Emp emp02 = mapper.getEmpById(1);
    System.out.println(emp02);// 打印了两次,但只发送了一次sql
    System.out.println(emp01 == emp02); // true 
} finally {
    sqlSession.close();
}

⭐一级缓存失效情况:

  • sqlSession不同
  • sqlSession相同,查询条件不同(原因:当前一级缓存中还没有这个数据)
  • sqlSession相同,两次查询期间执行了增删改操作(增删改可能对当前数据有影响)
  • sqlSession相同,手动清除了一级缓存,sqlSession.clearCache()

6.2 二级缓存

二级缓存(全局缓存):基于namespace级别的缓存,一个namespace对应一个二级缓存。

工作机制:

  1. 一个会话,查询一条数据,这个数据就会放到当前会话的一级缓存中

  2. 如果会话关闭,一级缓存中的数据会被保存到二级缓存中(注意会话关闭才会写入

  3. 开启新的会话,查新信息的时候就会参考二级缓存中的内容

  4. sqlSession --> EmpMapper -> Emp

    ​ --> DeptMapper -> Dept

    不同的namespace查出的数据会放到自己对应的缓存中(Map中)

⭐使用:

1.开启全局二级缓存配置(默认是true)

<setting name="cacheEnabled" value="true"/>

2.取每个mapper.xml中配置使用二级缓存


<cache eviction="LRU" flushInterval="60000" readOnly="false" size="1024"/>
属性可以不写都使用默认的 <cache>cache>

3.因为安全问题,底层使用序列化和反序列化技术,我们的pojo需要实现序列化接口Serializable

注意:查询的数据都会默认放在一级缓存中,只有会话提交或者关闭之后,一级缓存中的数据才会转移到二级缓存中。

和缓存相关的设置/属性:

  • cacheEnabled 如果设置为false,关闭的是二级缓存
  • 每个select标签都有一个useCache="true"属性,默认true。设置为false则关闭的还是二级缓存
  • 每个增删改标签都有一个flushCache="true"属性,默认true,代表增删改执行完之后都要清除缓存(一级二级缓存都会被清空),其实查询标签里面也要这个属性,但默认是false。
  • sqlSession.clearCache() 只会清除当前sqlSession的一级缓存。
  • localCacheScope 本地缓存作用域(影响的是一级缓存),取值SESSION (默认) | STATEMENT (对相同 SqlSession 的不同查询将不会进行缓存。相当于可以禁用一级缓存)

图解(查询缓存的顺序:二级缓存 -> 一级缓存 -> 数据库):
MyBatis_第3张图片


6.3 第三方缓存

MyBatis缓存底层就是个简单是Map,有个Cache接口供第三方拓展(Redis,EHCache)

// EHCache依赖
<dependency>
    <groupId>org.ehcachegroupId>
    <artifactId>ehcacheartifactId>
    <version>3.9.9version>
dependency>

// EhCache和MyBatis整合包(即官网已经根据ehcache实现了CaChe接口,自定义缓存)
<dependency>
    <groupId>org.mybatis.cachesgroupId>
    <artifactId>mybatis-ehcacheartifactId>
    <version>1.2.1version>
dependency>

使用:

  1. 给每个映射文件里加入cache标签

    <cache type="org.mybatis.caches.ehcache.EhcacheCache">cache>
    
    
    <cache-ref namespace="com.sutong.dao.EmpMapper"/>
    
  2. 需要在类路径下放一个ehcache.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">
        
        <diskStore path="D:\Learning\Java_Practice\Java Frame\ehcache"/>
    
        <defaultCache
                maxElementsInMemory="10000"
                maxElementsOnDisk="10000000"
                eternal="false"
                overflowToDisk="true"
                timeToIdleSeconds="120"
                timeToLiveSeconds="120"
                diskExpiryThreadIntervalSeconds="120"
                memoryStoreEvictionPolicy="LRU">
        defaultCache>
    ehcache>
    
  3. 然后开启了二级缓存就可以直接使用了(pojo记得实现序列化接口)

    @Test
    public void test02() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
    
        try {
            EmpMapper mapper1 = sqlSession1.getMapper(EmpMapper.class);
            EmpMapper mapper2 = sqlSession2.getMapper(EmpMapper.class);
    
            Emp e1 = mapper1.getEmpById(1);
            System.out.println(e1);
            sqlSession1.close();  // 关闭才能放到二级缓存中
    
            Emp e2 = mapper2.getEmpById(1);
            System.out.println(e2);
            sqlSession2.close();  // 可以看到上面我们配置的文件夹里面有缓存的文件了
        } finally {
            //sqlSession.close();
        }
    }
    

7. MyBatis-Spring整合

MyBatis-Spring 会帮助你将 MyBatis 代码无缝地整合到 Spring 中。它将允许 MyBatis 参与到 Spring 的事务管理之中,创建映射器 mapper 和 SqlSession 并注入到 bean 中,以及将 Mybatis 的异常转换为 Spring 的 DataAccessException。 最终,可以做到应用代码不依赖于 MyBatis,Spring 或 MyBatis-Spring。

依赖:


<dependency>
    <groupId>org.mybatisgroupId>
    <artifactId>mybatis-springartifactId>
    <version>2.0.6version>
dependency>

7.1 编写数据源配置

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
      destroy-method="close">
    <property name="url" value=".." />
    <property name="username" value=".." />
    <property name="password" value=".." />
    <property name="driverClassName" value=".." />
bean>

7.2 SqlSessionFactoryBean

在基础的 MyBatis 用法中,是通过 SqlSessionFactoryBuilder 来创建 SqlSessionFactory 的。而在 MyBatis-Spring 中,则使用 SqlSessionFactoryBean 来创建。

还有一个常用的属性是 configLocation,它用来指定 MyBatis 的 XML 配置文件路径。

还有个mapperLocations指定sql映射文件的位置(在xml和类不在一个文件夹的时候使用)

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="configLocation" value="classpath:mybatis-config.xml"/>
bean>

7.3 SqlSessionTemplate

SqlSessionTemplate 是 MyBatis-Spring 的核心。作为 SqlSession 的一个实现,这意味着可以使用它无缝代替你代码中已经在使用的 SqlSessionSqlSessionTemplate 是线程安全的,可以被多个 DAO 或映射器所共享使用。

<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
  
  <constructor-arg index="0" ref="sqlSessionFactory" />
bean>

7.4 加实现类

需要给接口加实现类,注入到Spring中。

里面获取sqlSession,获取对应的Mapper代理,调用对应的方法就行了。最后注入Spring,后面的Service层就能用了。

public class UserDaoImpl implements UserDao {

    private SqlSession sqlSession;
    public void setSqlSession(SqlSession sqlSession) { this.sqlSession = sqlSession; }

    public User getUser(String userId) {
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        return mapper.getUser(userId);
    }
}
<bean id="userDao" class="org.mybatis.spring.sample.dao.UserDaoImpl">
  <property name="sqlSession" ref="sqlSession" />
bean>

或者使用 SqlSessionDaoSupport 抽象类:SqlSessionDaoSupport 用来为你提供 SqlSession。调用 getSqlSession() 方法你会得到一个 SqlSessionTemplate,之后可以用于执行 SQL 方法,

public class UserDaoImpl extends SqlSessionDaoSupport implements UserDao {

    // 这样我们的SqlSessionTemplate可以不注入到Spring中,我们可以直接getSqlSession();
    // 但他的父类需要注入一个SqlSessionFactory 
    public User getUser(String userId) {
        SqlSession sqlSession = getSqlSession();
        return sqlSession.getMapper(UserMapper.class).getUser(userId);
    }
}
<bean id="userDao" class="org.mybatis.spring.sample.dao.UserDaoImpl">
  <property name="sqlSessionFactory" ref="sqlSessionFactory" />
bean>

(这个了解,重要是SSM整合)


上面的我们手动写实现类,下面可以让Spring帮我们!!!⭐

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    <property name="basePackage" value="com.sutong.dao"/>
bean>




8. 逆向工程(MBG)

MyBatis Generator(MBG):专门为MyBatis框架使用者定制的代码生成器,可以快速的根据表生成对应的映射文件,以及QBC风格的条件查询。但表连接存储过程等复杂的SQL的定义需要我们手工编写

依赖(当然还需要MySQL驱动和MyBatis依赖):


<dependency>
    <groupId>org.mybatis.generatorgroupId>
    <artifactId>mybatis-generator-coreartifactId>
    <version>1.4.0version>
dependency>

8.1 配置 ☢

详细可以看官网

src/main/resources/generatorConfig.xml


DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    
    <context id="DB2Tables" targetRuntime="MyBatis3Simple">
        
        
        <commentGenerator>
            <property name="suppressDate" value="true"/>  
            <property name="suppressAllComments" value="true" /> 
        commentGenerator>
        
        
        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/mybatis"
                        userId="root"
                        password="9527">
        jdbcConnection>

        
        <javaTypeResolver >
            <property name="forceBigDecimals" value="false" />
        javaTypeResolver>

        
        <javaModelGenerator targetPackage="com.sutong.bean" targetProject=".\src\main\java">
            <property name="enableSubPackages" value="true" />
            <property name="trimStrings" value="true" />
        javaModelGenerator>

        
        <sqlMapGenerator targetPackage="mybatis.mapper"  targetProject=".\src\main\resources">
            <property name="enableSubPackages" value="true" />
        sqlMapGenerator>

        
        <javaClientGenerator type="XMLMAPPER" targetPackage="com.sutong.dao"
                             targetProject=".\src\main\java">
            <property name="enableSubPackages" value="true" />
        javaClientGenerator>

        
        <table tableName="t_emp" domainObjectName="Emp">table>
        <table tableName="t_dept" domainObjectName="Dept">table>

    context>
generatorConfiguration>

pom.xml

<build>
  <plugins>
    <plugin>
      
      <groupId>org.mybatis.generatorgroupId>
      <artifactId>mybatis-generator-maven-pluginartifactId>
      <version>1.4.0version>
      <configuration>
        
        <configurationFile>src/main/resources/generatorConfig.xmlconfigurationFile>
        <verbose>trueverbose>
        <overwrite>trueoverwrite> 
      configuration>
      <executions>
        <execution>
          <id>Generate MyBatis Artifactsid>
          <goals>
            <goal>generategoal>
          goals>
        execution>
      executions>
      <dependencies>
        <dependency>
          <groupId>org.mybatis.generatorgroupId>
          <artifactId>mybatis-generator-coreartifactId>
          <version>1.4.0version>
        dependency>
      dependencies>
    plugin>
  plugins>
build>

还要有全局配置文件mybatis-config.xml别忘了


DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    
    <properties resource="dbconfig.properties"/>

    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="aggressiveLazyLoading" value="false"/>
        <setting name="cacheEnabled" value="true"/>
    settings>

    <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 resource="mybatis/mapper/DeptMapper.xml"/>
        <mapper resource="mybatis/mapper/EmpMapper.xml"/>
    mappers>
configuration>

8.2 运行

public class RunMybatisGenerator {

    @Test
    public void run() throws InterruptedException, SQLException, 
    IOException, XMLParserException, InvalidConfigurationException {

        List<String> warnings = new ArrayList<>();
        boolean overwrite = true;
        File configFile = new File("src/main/resources/generatorConfig.xml");
        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);
    }
}

或者直接右上角Maven的点mybatis-generator命令就可以运行了!!!

它自动把数据库里面的下划线命名法换位Java的驼峰命名法!!

运行后:
MyBatis_第4张图片


8.3 测试

@Test
public void test01() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();

    // MyBatis3Simple
    try {
        EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
        System.out.println(mapper.selectAll());  // 没有自动生成toString()
    } finally {
        sqlSession.close();
    }
    
    
    
    // MyBatis3
    try {
        EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
        // xxExample就是封装查询条件的
        List<Emp> allEmp = mapper.selectByExample(null); // 查询所有

        // (查询员工字母带a的,而且性别的1的) 或者 (Email里面带e的)
        EmpExample empExample = new EmpExample();             // 封装查询条件的Example
        EmpExample.Criteria c1 = empExample.createCriteria(); // criteria就是拼装条件的
        c1.andLastNameLike("%a%").andGenderEqualTo("1");    

        EmpExample.Criteria c2 = empExample.createCriteria();
        c2.andEmailLike("%e%");
        
        empExample.or(c2); // 把或条件拼装上去

        List<Emp> empsByExample = mapper.selectByExample(empExample); // 条件查询
    } finally {
        sqlSession.close();
    }
}

MyBatis3生产的是GBC风格的查询!!


9. MyBatis工作原理

⭐重要,对后面的插件开发有用


9.1 分层架构

MyBatis_第5张图片


9.2 运行流程

1.根据全局配置文件,创建SqlSessionFactory
2.获取SqlSession实例
3.获取接口代理对象 (MapperProxy对象)
4.执行增删改查

①SqlSessionFactory的获取:
MyBatis_第6张图片

其中的每个MapperStatement代表一个增删查改标签的详细信息

Configuration包括 全局配置文件+所有mapper映射文件 的详细信息。

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

最终得到的是一个DefalutSqlSessionFactory 对象(里面包含了Configuration对象)

总结:把所有配置文件信息解析保存到Configuration对象中,然后返回包含了ConfigurationDefalutSqlSessionFactory对象中

②sqlSession的获取:
MyBatis_第7张图片

上图的第七步很重要,是执行所有拦截器的拦截方法的。

SqlSession sqlSession = sqlSessionFactory.openSession();

即调用DefalutSqlSessionFactory方法创建返回一个DefalutSqlSession 对象,里面包含了 ConfigurationExecutor对象!!!(Executor对象会在这一步创建)

总结:返回一个DefalutSqlSession 对象,包含了 ConfigurationExecutor

③获取接口代理对象
MyBatis_第8张图片

EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);

返回接口代理对象,里面包含了DefalutSqlSession对象,所以可以用来执行增删查改。

总结:使用MapperProxyFactory 创建对应代理对象,代理对象包含了DefalutSqlSession对象

④执行增删查改

代理对象 -> DefaultSqlSession -> Executor -> StatementHandler (处理sql语句预编译,设置参数等工作)

-> ParameterHandler (设置预编译参数用的)

-> ResultSetHandler (处理结果集)

(设置参数和处理结果都用到了TypeHandler来做的,处理数据库和Java类型的映射)

查询总结图解:
MyBatis_第9张图片

重要了解四大对象的作用和执行流程就行


9.3 总结 ☢

  1. 根据所有配置文件,初始化出Configuration对象

  2. 创建一个DefaultSqlSession 对象,里面包含ConfigurationExecutor

    (根据配置文件中的defaultExecutorType设置创建对应的Executor)

  3. getMapper() ,拿到Mapper接口对应的MapperProxy,里面包含DefaultSqlSession

  4. 执行增删改查

    • 调用DefaultSqlSession中的Excutor的增删改查
    • 会创建一个StatementHandler对象(同时创建ParameterHandlerRestSetHandler
    • 调用StatementHandler预编译以及利用ParameterHandler设置sql的参数
    • 调用StatementHandler的增删查改方法
    • RestSetHandler封装结果

四大对象创建的时候都会有:interceptorChain.plugAll(xxxx) !!!


10. 插件开发


10.1 插件原理

  1. 四大对象创建不是直接返回的而是先调用interceptorChain.plugAll(xxxx) 方法

  2. plugAll()方法里面是拿到所有的Interceptor(即拦截器,我们需要实现的接口),然后调用每个拦截器的plugin()方法,target = interceptor.plugin(target) ,执行完所有拦截器,后返回target 包装后的对象。

  3. 插件机制,我们我们可以使用插件为目标对象创建一个代理对象:AOP

我们的插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象的每一个执行方法


10.2 插件实现

  1. 编写Interceptor实现类
  2. 使用Intercepts注解
  3. 注册到全局配置文件中

MyFirstPlugin.java

package com.sutong.interceptor;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;

import java.sql.Statement;
import java.util.Properties;

// Intercepts注解完成插件的签名(告诉MyBatis当前插件拦截那个对象的那个方法),里面是Signature数组,
// Signature里面有type属性是要拦截对象(四大对象之一),method属性是要拦截该对象的那个方法,args属性是指定该方法是参数列表
@Intercepts({
        @Signature(type = StatementHandler.class, method = "parameterize", args = Statement.class)
})
public class MyFirstPlugin implements Interceptor {

    /**
     * intercept() - 拦截目标对象目标方法的执行
     * @param invocation 目标方法
     * @return 返回执行后的返回值
     */
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("MyFirstPlugin - intercept -> " + invocation.getMethod());
        Object result = invocation.proceed(); // 执行目标方法,前后可以做一些事
        return result;
    }

    /**
     * plugin() - 包装目标对象 (包装:为目标对象插件代理对象)
     * 四大个对象的创建都会调用这个方法,只有是我们要拦截的对象才会创建代理对象!否则直接返回
     * @param target 目标对象
     * @return 返回当前target插件的动态代理
     */
    public Object plugin(Object target) {
        System.out.println("MyFirstPlugin - plugin -> 将要包装的对象" + target);
        Object wrap = Plugin.wrap(target, this); // Mybatis为了我们使用方便,包装了Proxy生成代理对象
        return wrap;
    }

    /**
     * setProperties() - 将插件注册时的 property 属性设置进来
     * @param properties 插件的配置信息
     */
    public void setProperties(Properties properties) {
        System.out.println("插件的配置信息:" + properties);
    }
}

mybatis-config.xml


<plugins>
    <plugin interceptor="com.sutong.interceptor.MyFirstPlugin">
        <property name="username" value="sutong"/>
        <property name="password" value="666"/>
    plugin>
plugins>

10.3 多个插件 ☢

如果多个插件拦截同一个对象的同一个方法,则:

plugin()则和注册插件的顺序有关!有几个插件包装几次!!多层代理… (正序)

intercept() 由于的多层代理,一进来是先进入最后一个包装的代理,执行其intercept方法,然后进入第二层代理执行方法…(反序)

创建插件动态代理的时候,是按照插件配置顺序创建代理对象,执行目标方法的时候按照逆向顺序执行的。


10.4 简单使用

动态改变一下sql运行的参数!!

@Intercepts({
        @Signature(type = StatementHandler.class, method = "parameterize", args = Statement.class)
})
public class MyFirstPlugin implements Interceptor {

    // 偷梁换柱
    public Object intercept(Invocation invocation) throws Throwable {

        // 动态改变一下sql运行的参数:以前查1号员工,我们实际从数据库查2号员工!!偷梁换柱
        // 拿到StatementHandler -> ParameterHandler -> parameterObject值
        Object target = invocation.getTarget(); // 我们在拦截的对象
        MetaObject metaObject = SystemMetaObject.forObject(target);   // 拿到target的元数据

        //metaObject.getValue("parameterHandler.parameterObject");    // 获取sql语句用的参数
        metaObject.setValue("parameterHandler.parameterObject", 2);   // 修改sql的参数

        // 执行目标方法
        Object result = invocation.proceed();
        return result;
    }

    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    public void setProperties(Properties properties) {}
}

测试:

@Test
public void test01() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    
    try {
        EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
        System.out.println(mapper.getEmpById(1)); // 则这里返回的是2号员工的信息!!!!
    } finally {
        sqlSession.close();
    }
}

10.5 插件应用

  • PageHelper插件进行分页
  • 批量操作
  • 存储过程
  • typeHandler处理枚举

PageHelper ☢

PageHelper分页插件的使用(支持多种数据库)

1.依赖:


<dependency>
    <groupId>com.github.pagehelpergroupId>
    <artifactId>pagehelperartifactId>
    <version>5.3.0version>
dependency>

2.配置拦截器

  • 在MyBatis中配置

    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            
            <property name="param1" value="value1"/>
    	plugin>
    plugins>
    
  • 或 在 Spring 配置文件中配置拦截器插件!!

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
      
      <property name="plugins">
        <array>
          <bean class="com.github.pagehelper.PageInterceptor">
            <property name="properties">
              
              <value>
                params=value1
                helperDialect=mysql
              value>
            property>
          bean>
        array>
      property>
    bean>
    

3.使用

接口:

// 接口
public interface EmpOther {
    List<Emp> getEmps();
}

映射文件:

<mapper namespace="com.sutong.dao.EmpOther">
    <select id="getEmps" resultType="com.sutong.bean.Emp">
        select `id`, `last_name`, `gender`, `email` from t_emp
    select>
mapper>

测试:

@Test
public void test01() throws IOException {
	// ...
    SqlSession sqlSession = sqlSessionFactory.openSession();
    try {
        // 只调用这一个方法进行,第一个是第几页,第二个是每页记录数,返回一个分页的详细信息对象
        Page<Object> pageHandler = PageHelper.startPage(2, 2);
        
        EmpOther mapper = sqlSession.getMapper(EmpOther.class);
        for (Emp emp : mapper.getEmps()) {
            System.out.println(emp);
        }
        System.out.println("当前页码: " + pageHandler.getPageNum());
        System.out.println("总页码: " + pageHandler.getPages());
        System.out.println("总记录数: " + pageHandler.getTotal());
        System.out.println("每页记录数: " + pageHandler.getPageSize()); // 还有...
    } finally {
        sqlSession.close();
    }
}



// 或者使用PageInfo获得信息,更详细!!PageInfo info = new PageInfo<>(list); 对结果进行包装
// 上面的那几个信息都有,还要 info.isIsFirstPage(),isIsLastPage()...等等!!
// 还可以这样new PageInfo<>(list, navigatePages); navigatePages是个int ,代表连续显示几页!!
public void test02() throws IOException {
   	// ...
    SqlSession sqlSession = sqlSessionFactory.openSession();
    try {
        PageHelper.startPage(2, 2);  // 开始分页
        
        EmpOther mapper = sqlSession.getMapper(EmpOther.class);
        List<Emp> list = mapper.getEmps();               // 查询
        PageInfo<Emp> info = new PageInfo<Emp>(list, 5); // 告诉插件我们要连续5页!!!
        for (Emp emp : list) {
            System.out.println(emp);
        }

        System.out.println("是否是第一页: " + info.isIsFirstPage());
        int[] nums = info.getNavigatepageNums(); // 连续显示的页码,就不用我们处理了!!直接返回前端取出
        System.out.println(Arrays.toString(nums));
    } finally {
        sqlSession.close();
    }
}

批量处理

上面的动态SQL使用的foreach严格一样上不算批量处理,那上面只是拼一个很长的SQL一起发送服务器,如果太长了服务器就不能接受了。

有个设置defaultExecutorType 是配置默认的执行器。默认是SIMPLE简单执行器, BATCH 执行器不仅能重用语句还会执行批量更新。

如果在全局配置文件改的话所有是SQL都会变为批量的。我们可以单独设置(在获取sqlSession传入一个参数)。

  1. Mybatis使用

    @Test
    public void testBatch() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    
        // 传入ExecutorType,就可以获得一个批量操作的sqlSession!!!!
        SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
        try {
            EmpOther mapper = sqlSession.getMapper(EmpOther.class);
            List<Emp> list = new ArrayList<Emp>();
            list.add(new Emp(null, UUID.randomUUID().toString(), "1", "[email protected]"));
            list.add(new Emp(null, UUID.randomUUID().toString(), "0", "[email protected]"));
            list.add(new Emp(null, UUID.randomUUID().toString(), "1", "[email protected]"));
            // ....
            
            // 调用多次添加就行了,时间很短的。
            // 非批量的每执行一个增删改查就发送一个SQL到数据库,预编译(n次) -> 设置参数(n次) -> 执行(n次)
            // 批量一起发送执行,预编译(1次) -> 设置参数(n次) -> 执行(1次)
            for (Emp emp : list) {   
                mapper.addEmp(emp);
            }
            sqlSession.commit();
        } finally {
            sqlSession.close();
        }
    }
    
  2. Spring中使用

    配置 spring-dao.xml里面:

    
    <bean id="batchSqlSession" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg name="sqlSessionFactory" value="sqlSessionFactory"/>
        <constructor-arg name="executorType" value="BATCH"/>
    bean>
    

    使用:

    public class EmpServiceImpl {
        private BookMapper bookMapper;
        
        @Autowired
        private SqlSession sqlSession;
        
        public void testBatch() {
            EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); // 然后调用里面的增删改查方法就是批量的了
        }
    }
    
    
  • 非批量:预编译(n次) -> 设置参数(n次) -> 执行(n次)
  • 批量:预编译(1次) -> 设置参数(n次) -> 执行(1次)

存储过程

存储过程:很多sql语句是集合,编译一次,保存在数据库中的。MyBatis支持对存储过程的调用。

先学习一下存储MySQL中的存储过程:

  • 入参存储过程(入参类似java方法中的形参)

    # 参数写法:输入输出类型 参数名称 参数数据类型
    
    # 入参存储过程,例如:根据传来的数字,动态赋值name
    create procedure add_name(in target int)  #in代表形参,out代表返回值
    begin  # begin 和 end 之间写我们的逻辑
    	declare emp_name varchar(11);  #定义一个变量
    	if target = 1 then
    		set emp_name = '一号员工';   # 赋值需要加个set,否则的判断
    		else
    		set emp_name = '其他员工';
    	end if;
    	insert into t_emp(`last_name`, `gender`, `email`) values(emp_name, '1', '[email protected]');
    end
    
    # 调用存储过程:
    call add_name(1);
    
    # 删除存储过程:
    drop procedure add_name;
    
  • 出参存储过程(出参类似java方法中的返回值)

    # 出参存储过程,例如:统计t_emp表总条数
    create procedure count_emp(out count_num int)
    begin
    	# 直接把查询出来的结果赋值给count_num
    	select count(*) into count_num from t_emp; 
    end;
    
    # 定义变量前面需要加上@,这样是不显示东西的,需要去查询这个变量
    call count_emp(@count_num);
    select @count_num;
    
    drop procedure count_emp;
    

MyBatis中存储过程的调用:

接口:

public interface EmpOther {
    void testProcedure(Integer num);
}

映射文件:


<select id="testProcedure" statementType="CALLABLE"> 
    {call add_name( #{num, mode=IN, jdbcType=INTEGER} )} 
select>

测试:

@Test
public void test03() throws IOException {
    // ...
    SqlSession sqlSession = sqlSessionFactory.openSession();
    try {
        EmpOther mapper = sqlSession.getMapper(EmpOther.class);
        mapper.testProcedure(10);  // 调用
    } finally {
        sqlSession.close();
    }
}

typeHandler处理类型

学习写枚举的使用

@Test
public void testEnumUse() {
        EmpStatus login = EmpStatus.LOGIN;
        System.out.println(login.ordinal()); // 枚举索引
        System.out.println(login.name()); // 枚举名字
}

测试默认的枚举处理:

public enum EmpStatus {
    LOGIN, LOGOUT, REMOVE
}

public class Emp {
    private Integer id;
    private String lastName;   
    private String gender;   
    private String email;
    private EmpStatus empStatus; // 员工状态
}
<insert id="addEmp">
    insert into t_emp(`last_name`, `gender`, `email`, `empStatus`) 
    values (#{lastName}, #{gender}, #{email}, #{empStatus})
insert>

测试默认的枚举处理:

try {
    EmpOther mapper = sqlSession.getMapper(EmpOther.class);
    mapper.addEmp(new Emp(null, "enum", "1", "[email protected]", EmpStatus.LOGIN));

    sqlSession.commit();
} finally {
    sqlSession.close();
}

数据库默认保存的是枚举的名字:LOGIN,即使用EnumTypeHandler处理的!!

我们可以改变让数据库存储枚举索引,使用EnumOrdinalTypeHandler,要设置全局配置文件

<typeHandlers>
    
    <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
                 javaType="com.sutong.bean.EmpStatus"/>
typeHandlers>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VzyWTd6I-1662964083316)(MyBatis.assets/image-20220128190140331.png)]


上面这样使用的并不多,自定义处理才是使用最多的:

/**
 * 希望数据库保存的是状态码,而不是默认的索引或者枚举名!
 */
public enum EmpStatus {
    LOGIN(100, "用户登录"), LOGOUT(200, "用户登出"), REMOVE(300, "用户不存在");

    private Integer code;
    private String msg;
    private EmpStatus(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    
    // 按照状态码返回枚举对象
    public static EmpStatus getEmpStatusByCode(Integer code) {
        switch (code) {
            case 100:
                return EmpStatus.LOGIN;
            case 200:
                return EmpStatus.LOGOUT;
            case 300:
                return EmpStatus.REMOVE;
            default:
                return EmpStatus.LOGOUT;
        }
    }
    
    // 下面还有get/set
}

自定义类型处理器要TypeHandler接口或者继承BaseTypeHandler

// 自定义的枚举类型处理器
public class MyEnumEmpStatusHandler implements TypeHandler<EmpStatus> {

    // 定义当前数据如何保存到数据库中
    public void setParameter(PreparedStatement preparedStatement, int i, 
                             EmpStatus empStatus, JdbcType jdbcType) throws SQLException {
        
        preparedStatement.setString(i, empStatus.getCode().toString());  // 保存状态码
    }

    public EmpStatus getResult(ResultSet resultSet, String s) throws SQLException {
        int code = resultSet.getInt(s); // s是数据库字段名
        // 根据从数据库拿到的状态码,返回一个枚举对象
        return EmpStatus.getEmpStatusByCode(code);
    }

    public EmpStatus getResult(ResultSet resultSet, int i) throws SQLException {
        int code = resultSet.getInt(i); // i是数据库字段索引
        return EmpStatus.getEmpStatusByCode(code);
    }

    public EmpStatus getResult(CallableStatement callableStatement, int i) throws SQLException {
        int code = callableStatement.getInt(i); // i是数据库字段索引,是存储过程中拿!!
        return EmpStatus.getEmpStatusByCode(code);
    }
}

全局配置:

<typeHandlers>
    <typeHandler handler="com.sutong.handler.MyEnumEmpStatusHandler"
                 javaType="com.sutong.bean.EmpStatus"/>
typeHandlers>


保存时:#{empStatus,typeHandler=com.sutong.handler.MyEnumEmpStatusHandler}
查询时:就要使用ResultMap了,自定义,<result column="empStatus" property="empStatus" typeHandler=".."/>
我们应该保证在查询和保存使用的的类型处理器是一样的!!!这样才不会出错

测试:

try {
    EmpOther mapper = sqlSession.getMapper(EmpOther.class);
    mapper.addEmp(new Emp(null, "myEnum", "0", "[email protected]", EmpStatus.REMOVE));

    sqlSession.commit();
} finally {
    sqlSession.close();
}


Emp emp = mapper.getEmpById(14);
System.out.println(emp.getEmpStatus()); // 查询出来的时候会封装为EmpStatus对象!!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Ua9jwCM-1662964083316)(MyBatis.assets/image-20220128192439653.png)]

你可能感兴趣的:(SpringFrameWork,笔记,mybatis,java,数据库,java-ee,mysql)