MyBatis学习笔记

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/jacksonary/article/details/80074941

 先做个科普吧,MyBatis是其3.0以后的叫法,之前的版本都叫iBatis,这个名字肯定听过吧,也是大名鼎鼎的Apache旗下的产品,后来易主谷歌,正式改名为MyBatis,代码迁到github上。它是一种持久化框架,和Hibernate一样都是ORM(Object Relation Mapping,对象关系映射)框架,他们需要完成的任务都是将JavaBean映射成数据库中的一条记录。

 不管是什么框架,Java和数据库交互,内部肯定少不了jdbc的支持(所有的框架都是对原生JDBC方式的封装),但是之前接触的都是jdbc的小工具,而不是框架。jdbc本身sql语句都是写在java代码中的,逻辑很混乱,耦合度也很高,所以不好,这里先回顾一下jdbc操作数据的流程:

这里写图片描述

 Hibernate是我学习的第一个ORM框架,觉得挺好用,后来接触到了MyBatis,由于追星的原因,我从来原来的SSH转成了SSM架构开发,网上很多大牛都说Hibernate不如MyBtais好用,不够灵活,我也不懂为什么这么说,但是那么多比我厉害的人都说它好用,那就用呗,反正我是不知道哪里好,今天终于好像有点懂了_。首先看一下Hibernate,它是一个全自动的ORM框架,它的工作流程如下:

这里写图片描述

Hibernate把jdbc的工作都做完了,连编写SQL这活儿都包了,其实挺好的,我就只要面向对象去撸代码就好了,而且Hibernate框架是全映射的ORM(啥意思,就是如果JavaBean有多少属性,它查询时候就直接将对应表中的属性字段全部查询出来,这里我想问一下懒加载这一块有没有反驳的理由),但是这样就带来一个问题,它的SQL的控制权没有留给开发人员带来方便(针对小项目)的同时也带来了麻烦。因为在写大的工程时,不可能不涉及SQL的优化问题,我们需要去自己写SQL,但是Hibernate的本质目的就是为了消除SQL语句,让不懂SQL的人员也可以实现增删改查,那怎么办,当然在Hibernate中也提供了HQL语言,通过HQL我们可以定制SQL语句,但是这个过程还是比较麻烦的,所以说Hibernate不灵活。

 基于上述Hibernate的全封装弊端,我们希望SQL由我们自己写,所以MyBatis出现了,它和Hibernate最大的不同就是将SQL的控制权交给了我们自己(单独放到配置文件中),因为作为一个框架要是要有点逼格的,不能啥都不做吧(所以说MyBatis是一个半自动化框架),它的工作流程如下:

这里写图片描述

1. Hello,MyBatis!

 同理,首先给出MyBatis官网教程,这个是最好的老师,同时,在MyBatis的压缩包中有一个pdf文件也是文档教程。好了,开始搞第一个Demo吧,本文中所有Mybatis的栗子默认都是基于当前最新版本MyBatis-3.4.6。

  • 导入mybatis-3.4.6.jar、mysql-connector-java-5.1.7-bin.jar和log4j-1.2.17.jar,创建一个JavaBean为Employee:
public class Employee {
    private int id;
    private String lastName;
    private String gender;
    private String email;
    //getter and setter as well as toString
}

对应的MySQL的表结构如下
这里写图片描述

  • 创建MyBatis的核心配置文件(全局配置文件):mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!--
        MyBatis的配置文件中子标签必须按照一定的顺序去写
        不按顺序有的配置将不生效或者直接报错;
        正确的子标签顺序应该依次为:
        properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,
        reflectorFactory?,plugins?,environments?,databaseIdProvider?,mappers?
    -->
    <!--配置MyBatis在控制台打印sql语句,需要配置log4j使用-->
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

    <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/db_mybatis"/>
                <property name="username" value="root"/>
                <property name="password" value="921228jack"/>
            </dataSource>
        </environment>
    </environments>

    <!--sql映射文件是在写完mapper文件后再加入这里面的-->
    <mappers>
        <mapper resource="conf/EmployeeMapper.xml"/>
    </mappers>

</configuration>

因为配置了log4j日志,所以这里需要额外写一个log4j的配置文件log4j.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">

<log4j:configuration debug="true">

<appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
        <param name="Encoding" value="UTF-8"/>
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%-5p %d{MM-dd HH:mm:ss,SSS} %m  (%F:%L) \n"/>
        </layout>
    </appender>
    <logger name="java.sql">
        <level value="debug"/>
    </logger>
    <logger name="org.apache.ibatis">
        <level value="info"/>
    </logger>
    <root>
        <level value="debug"/>
        <appender-ref ref="STDOUT"/>
    </root>
</log4j:configuration>
  • 配置sql映射文件EmployeeMapper.xml,里面封装了每一条SQL语句,并将其配置到全局配置文件中:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace:名称空间,写全类名-->
<mapper namespace="com.hhu.entity.Employee">
    <!--id:唯一标识,就是方法名
         resultType:返回值类型,即执行sql后的返回值想要怎么处理,封装成什么对象
         #{id}:从方法传入的参数“id”获取传入sql语句的参数
     -->
    <select id="selectEmp" resultType="com.hhu.entity.Employee">
        <!--这里是查不到lastName属性的,MyBatis默认根据JavaBean中的属性名的方式去查找
              数据库,如果属性和字段名对不上,那么就查不到;
              解决方法:在mapper文件属性名和字段名不一样地方,用JavaBean中的属性名做该字段的别名,
              不用通配符*,即可
        -->
        select id, last_name lastName, gender,email from tbl_employee where id = #{id}
    </select>
</mapper>
  • 测试MyBatis的操作流程:
/**
    * 1. 根据xml配置文件创建一个SqlSessionFactory,再由此创建sqlSession,通过它可以实现MyBatis对数据库的增删改查,每一次用完后一定要将其关闭;
    * 2. sql映射文件(即mapper文件),配置每一条sql语句和其封装规则
    * 3. 将sql映射文件配置到全局配置文件mybatis-config.xml
    */
@Test
public void testSqlSession() throws IOException {
    //读取配配置文件(这是一MyBatis的核心配置文件,也是一个全局配置文件)
    String resource = "conf/mybatis-config.xml";
    //注意这里的Resources是apach包下面的类
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    //获取sqlSession,它可以执行已经映射的SQL语句(即mapper中的文件)
    SqlSession sqlSession = sqlSessionFactory.openSession();

    //注意这里使用try-finally,不管如何,最后一定要将sqlSession关闭
    try{
        //第一个参数是唯一标识,第二个参数是执行SQL传入的参数
        Employee employee = sqlSession.selectOne("selectEmp", 2);
        System.out.println(employee);
    } finally {
        //关闭Session
        sqlSession.close();
    }

}

2. 面向接口编程的MyBatis(推荐)

 在前一小节,使用MyBatis的方式有些古老,正常情况我们都是通过在Dao层的接口直接去关联的mapper的sql映射文件,而不是通过想selectOne类似指定唯一标识符的方式去执行sql,而且这个接口我们不需要有实现类,下面看下MyBatis更为常见的接口使用方式。

  • 创建接口EmployeeDao:
public interface EmployeeDao {
    public Employee getEmployeeById(int id);
}
  • 将接口与sql映射文件动态绑定:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace:名称空间,写绑定接口的全类名-->
<mapper namespace="com.hhu.dao.EmployeeDao">
    <!--id:唯一标识,就不是随意写了,需要写成绑定接口中对应的方法名
         resultType:返回值类型,即执行sql后的返回值想要怎么处理,封装成什么对象
         #{id}:从方法传入的参数“id”获取传入sql语句的参数
     -->
    <select id="getEmployeeById" resultType="com.hhu.entity.Employee">
        <!--这里是查不到lastName属性的,MyBatis默认根据JavaBean中的属性名的方式去查找
              数据库,如果属性和字段名对不上,那么就查不到;
              解决方法:在mapper文件属性名和字段名不一样地方,用JavaBean中的属性名做该字段的别名,
              不用通配符*,即可
        -->
        select id, last_name lastName, gender,email from tbl_employee where id = #{id}
    </select>
</mapper>
  • 调用:
@Test
public void testInterface() throws IOException {
    //从配置文件的输入流读取配置创建SessionFactory
        InputStream is = Resources.getResourceAsStream("conf/mybatis-config.xml");
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(is);

        //打开一个会话
        SqlSession sqlSession = sessionFactory.openSession();

        try {
            //这里虽然是获取的是接口类型的类,但是一旦将接口和sql映射文件动态绑定后
            //MyBatis会自动为接口创建代理对象
            EmployeeDao employeeDao = sqlSession.getMapper(EmployeeDao.class);
            //查看获取对象的类型,可以发现是一个代理对象,可以通过这个代理对象实现增删改查
            System.out.println(employeeDao.getClass());
            Employee e = employeeDao.getEmployeeById(1);
            System.out.println(e);
        } finally {
        sqlSession.close();
        }
    }

经过上述的工作就完成了,在学习过Hibernate和MyBatis之后发现两者都有SqlSession,并且都是通过它来操作数据库,也就是说通过这个对象可以获取Connection对象。

【注意】SqlSession和Connection一样都是非线程安全的,在多线程环境下不能将它们作为成员变量放在外面!比如:

public class TestSqlSessionFactory {

private SqlSession sqlSession = null;

    @Test
    public void test1() {...}

    @Test
    public void test1() {...}

    @Test
    public void test1() {...}
}

上面的写法就是有问题的,如果多线程都获取了这个SqlSession,A线程操作完直接关闭了,那其他线程怎么办,对吧,所以每次都要去获取显得实例。

3. MyBatis的全局配置文件(mybatis-config.xml)

mybatis-config.xml是MyBatis的全局配置文件,也是其核心配置文件,上面是以xml的形式来搞的,当然我们也可以不去搞这个文件,而是通过其他的方式,具体参见官方文档。关于MyBatis的全局配置文件需要写的东西基本都涵盖在了<configuration></configuration>标签中,下面着重对这个标签中的各个子标签做说明。

 这里重要的地方再说一遍,被坑过啊,MyBatis的配置文件中标签必须按照指定的顺序去写,不按顺序写将直接报错!!
正确的标签顺序应该依次为:propertiessettingstypeAliasestypeHandlersobjectFactoryobjectWrapperFactoryreflectorFactorypluginsenvironmentsdatabaseIdProvidermappers,下面看一下关于其中一些常用标签的用法

3.1 properties标签

 这个标签见名知意,很明显是用于配置文件的,在MyBatis这个标签是用来引入外部资源文件的,通常会将数据库的连接配置单独拎出来写一个db.properties文件,下面的是一个小栗子:

<properties resource="db.properties"></properties>

properties除了resource标签外,还有一个url标签,两者作用:

  • resource:用来引入类路径下的资源
  • url:用来引入网络路径或者磁盘路径下的资源

这里在写的时候还要注意,我是将所有的配置文件单独放在了src一个文件下,在IDEA需要将这个文件夹标记成Resources才能这么写,否则前面要加上路径

3.2 settings标签

 这个标签很重要,它可以直接影响MyBatis运行时的行为,里面也有很子标签,具体参见文档,下面是一个关于下划线驼峰命名的设置栗子:

 <settings>
        <!--下划线驼峰命名规则,默认关闭;
        若数据库中的字段为A_COUNT,而JavaBean中的属性名为aCount,即满足下划线后的
        第一个字母大写,其他小写MyBatis即可自动映射成功,如果不满足这个规则将不能做映射,
        像之前的Demo中lastName就是这个原因无法获取,只能通过别名获取,这里配置一下就可以了
        -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

3.3 typeAliases标签

 这个标签是用来给JavaBean起别名(在mapper文件中的别名),在mapper映射文件中,对于返回实体类的SQL语句,之前是写JavaBean的全类名,现在就可以直接写在此标签的中别名,用法如下:

<typeAliases>
    <!--
    type为JavaBean的全类名,alias为该实体类的别名,其中alias属性可以缺省
    (默认为首字母小写的类名)
    -->
    <typeAlias type="com.hhu.entitieses.Employee" alias="employee"/>
</typeAliases>

上述是对单个JavaBean起别名,如果数量比较多的话,这样的方式倒也是很麻烦,对某个包中以及子包中的JavaBean起别名可以使用package标签进行配置,如下:

<typeAliases>
    <!--
    name是包名,它会为该包下的所有类和子包的类起默认别名
    (默认为首字母小写的类名)
    -->
    <package name="com.hhu.entities" />
</typeAliases>

在使用package批量起别名的时候可能遇见这样的情况,父类包和子类包中含有同名的JavaBean,这就比较尴尬了,MyBatis中提供使用注解的方式来解决这个这个问题,只要在有冲突的JavaBean的类上使用@Alias("别名")注解为该JavaBean重新起一个没有冲突的别名即可。

3.4 typeHandlers标签

 它是类型处理器标签,是架起Java类型和数据库数据类型的桥梁,比如将java中String类型如何保存到数据库兼容的varchar或者char,又比如将数据库中查出的int类型转成java的Integer或者int类型。

3.5 objectFactory标签

 这个标签我们一般不动,使用MyBatis默认的就挺好。

3.6 plugins标签

 插件标签也是其中比较重要的标签,MyBatis可以通过插件对SQL语句进行拦截。

3.7 environments标签

 用于配置MyBatis的运行环境(我理解下来就是数据连接的配置),通过default属性指定使用的环境(值为环境的唯一标识符id),总体结构如下:

<environments default="dev_mysql">
    <!--环境1-->
    <environment id="dev_mysql">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <!--引用外部配置文件的属性进行赋值-->
            <property name="driver" value="${jdbc.driver}"/>
            .
            .
            .
        </dataSource>
    </environment>

    <!--环境2、环境3等等-->
</environments>

environments标签中的environment标签可以配置多种MyBatis的运行环境,每个environment标签中必须配置事务管理器transactionManager和数据源dataSource才算完整:

  • transactionManager:事务管理器,通过type属性指定事务管理器(正常为JDBC),在MyBatis中主要有三种:JDBC和MANAGED以及自定义事务管理器
    • JDBC:就是使用JdbcTransactionFactory作为事务管理器
    • MANAGED:就是使用ManagedTransactionFactory作为事务管理器
    • 自定义事务管理器:可以通过实现org.apache.ibatis.transaction.TransactionFactory接口即可,type指定为自定义事务管理器的全类名
  • dataSource:数据源同样,可以通过其中的type指定数据源的类型,MyBatis默认支持UNPOOLED和POOLED以及JNDI,也可以自定义。
    • UNPOOLED:即不使用连接池,每次对数据库的操作都会创建新的连接去操作
    • POOLED:就是使用连接池技术,这个比较好
    • 自定义数据源:实现org.apache.ibatis.datasource.DataSourceFactory接口即可。

3.8 databaseIdProvider标签

 MyBatis可以根据不同的底层数据库的类型执行不同的SQL语句,其实就是MyBatis对于移植性的考虑,Hibernate在这一块做的特别好,因为SQL语句不需要我们自己写,都是由Hibernate自动帮我们发送SQL语句的,但是MyBatis可以根据这个标签指定我们在SQL映射文件使用的SQL语句是基于哪种数据库去写的,从而达到MaBatis根据不同的数据库动态的发送SQL。通过这个标签可以配置不同数据库底层的标识符,MyBatis根据这个标识符执行不同的SQL语句,主要体现MyBatis和数据库底层的解耦,一般的配置如下:

<!--这里面的type属性就是DB_VENDOR,不要写其他的-->
<databaseIdProvider type="DB_VENDOR">
    <!--为不同的数据库厂商起别名,前面name属性是死的,value可以自己指定
        下面的配置就是想让MyBatis同时支持MySQL和MySQL和SQL Server三种数据库,
        在实际开发中主要看的想搞几种数据库类型

        然后在mapper.xml文件中进行SQL语句查询时可以在select标签中用databaseId="别名"的方式
        告诉MyBatis这个SQL语句是发送到哪种数据库的
    -->
    <property name="MySQL" value="mysql"/>
    <property name="Oracle" value="oracle"/>
    <property name="SQL Server" value="sql server"/>
</databaseIdProvider>

由于是不同的数据库,所以很容易就能想到需要配置其他的数据的环境吧,也就是说这个标签需要和前面的environments标签结合使用,另外mapper文件中在写SQL语句的时候需要使用databaseId属性指定使用的是哪种数据库厂商(它的值就是这里配置的各个数据库厂商和别名,比如MySQL我们自定义的别名为mysql),同时SQL映射文件中也要为不同的数据库的查询方式写不同的SQL语句的实现,下面以MySQL和Microsoft SQL Server两种数据库为例,看一下这个标签的用法:

  • 导入额外的MMSQL的jar包:mssql-jdbc-6.4.0.jre8.jar
  • 在MSSQL中创建一张employees表,也有Employee对象的属性,然后在db.properties配置文件中加入MSSQL的连接配置:
# MySQL Configuration--db_mybatis
jdbc.userName=root
jdbc.password=9j
jdbc.url=jdbc:mysql://localhost:3306/db_mybatis
jdbc.driver=com.mysql.jdbc.Driver

# MicroSoft SQL Server--SchoolServer
mssqlserver.userName=sa
mssqlserver.password=9
mssqlserver.url=jdbc:sqlserver://localhost:1433;DatabaseName=SchoolServer
mssqlserver.driver=com.microsoft.sqlserver.jdbc.SQLServerDriver
  • 在mybatis-config.xml配置文件中配置environmentsdatabaseIdProvider标签:
<environments default="dev_mysql">

    <!---配置MySQL的环境-->
    <environment id="dev_mysql">
        <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>

        <!--配置SQLServer的运行环境-->
    <environment id="dev_mssql">
        <transactionManager type="JDBC"></transactionManager>
        <dataSource type="POOLED">
            <property name="driver" value="${mssqlserver.driver}"/>
            <property name="url" value="${mssqlserver.url}"/>
            <property name="username" value="${mssqlserver.userName}"/>
            <property name="password" value="${mssqlserver.password}"/>
        </dataSource>
    </environment>

</environments>


<databaseIdProvider type="DB_VENDOR">
    <property name="MySQL" value="mysql"/>
    <property name="SQL Server" value="sql server"/>
</databaseIdProvider>
  • SQL映射文件增加MSSQL的SQL语句,并用databaseId指定数据库厂商的别名:
    <!--MySQL的查询语句-->
    <select id="getEmployeeById" resultType="employee" databaseId="mysql">
        select * from tbl_employee where id = #{id}
    </select>

    <!--同一SQL在不同数据库查询,这里指定去SQL Server中查询-->
    <select id="getEmployeeById" resultType="employee" databaseId="sql server">
        select * from employees where id = #{id}
    </select>

然后运行面向接口的测试代码,通过控制台发现MyBatis发送的是mysql的SQL语句,然后通过更改environments标签的default属性值为default="dev_mssql",再次运行,控制台打印的是MSSQL的语句。
【注意】这里除了在select标签中使用databaseId属性来指定SQL映射的哪种版本的数据库以外,还可以使用MyBatis内置参数_databaseId和结合if标签完成上面的工作,因为上述的方式必须要写两个select标签,并不符合程序员偷懒的好习惯,改造一下如下:

<!--注意去掉原来标签中的databaseId属性-->
<select id="getEmployeeById" resultType="employee">
    <!--注意在数据库上厂商别名上加单引号,否则MyBatis不能识别-->
    <if test="_databaseId=='mysql'">
        select * from tbl_employee where id = #{id}
    </if>
    <if test="_databaseId=='sql server'">
        select * from employees where id = #{id}
    </if>
</select>

<!--在MyBatis中必须配置数据库厂商的别名-->
<databaseIdProvider type="DB_VENDOR">
    <property name="MySQL" value="mysql"/>
    <property name="SQL Server" value="sql server"/>
</databaseIdProvider>

3.9 mappers标签

 通过该标签可以将SQL映射文件注册到全局配置文件中,用法如下:

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

当然在其子标签mapper是注册各个SQL映射文件的,这个子标签下主要有3个属性:

  • resource:引用类路径下的SQL映射文件;
  • url:引用网络路径或者磁盘路径下的SQL映射文件;
  • class:直接引用接口。

前两种不作太多说明,主要关于第三种class属性做说明,如果直接引用接口,需要将接口和接口绑定的SQL映射文件必须在同一路径下并且两者的同名;但是更常规的做法是利用注解直接将SQL写在接口中的方法身上而不写mapper文件了,然后引用接口的类,比如:

@Select("select * from tbl_employee where id=#{id}")
Employee    getEmployeeById(int id);

但是又不太推荐这么去写,因为MyBatis好不容易将java代码和SQL语句分离开来,现在又给他搞进去,确实不太好,而且不利于复杂SQL语句后期的优化,但是又因为用注解的方式实在太方便了,所以可以将简单的、不太重要的SQL直接用注解去搞,复杂的SQL写在xml中。

 上面是单个SQL映射文件的注入,同时MyBatis也提供了批量映射文件的注册,只用如下的方式:

<mappers>
    <!--会注册该包下所有类-->
    <package name="包名"/>
</mappers>

但是这种适用于注解的方式,如果在xml的形式,就需要将SQL的映射文件和绑定的接口类放到同一包路径下并且同名。

4. SQL映射文件xxmapper.xml

 SQL映射文件是MyBatis的核心文件之一,主要用于SQL语句的存放和接口的绑定。在一开始的Demo中就涉及到了关于mapper文件的一些写法,该文件中的主体是<mapper>标签,里面还有其他的一些常用的标签:

  • cache:配置接口的缓存
  • cache-ref:引用其他接口的缓存
  • resultMap:描述从数据库中加载出来的结果集
  • parameterMap:已废弃
  • sql:一个可重用的SQL语句,可以被其他语句引用;
  • insert:插入操作的SQL映射;
  • update:更新操作的SQL映射;
  • delete:删除操作的SQL映射;
  • select:查询操作的SQL映射;

下面先对其中用的最多的增删改查标签搞一个小栗子:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace:名称空间,写绑定接口的全类名-->
<mapper namespace="com.hhu.dao.EmployeeDao">
    <!--id:唯一标识,就写成绑定接口中对应的方法名
         resultType:返回值类型,即执行sql后的返回值想要怎么处理,封装成什么对象
         #{id}:从方法传入的参数“id”获取传入sql语句的参数
     -->
    <select id="getEmployeeById" resultType="com.hhu.entities.Employee">
        <!--这里是查不到lastName属性的,MyBatis默认根据JavaBean中的属性名的方式去查找
              数据库,如果属性和字段名对不上,那么就查不到;
              解决方法:在mapper文件属性名和字段名不一样地方,用JavaBean中的属性名做该字段的别名,
              不用通配符*,即可
        -->
        select id, last_namemm lastName, gender,email from tbl_employee where id = #{id}
    </select>

    <!--这里更新的时候,使用parameterType指明传入参数类型是Employee对象,
    但是在SQL语句中我们需要的是这个对象中一些属性的时候,可以直接写
    这个对象中的属性名即可,#{id}就是表示employee.id的意思-->
    <update id="upDateById"  parameterType="employee">
        UPDATE tbl_employee SET  last_NameMM=#{lastName}, gender=#{gender},email=#{email}
        WHERE  id = #{id}
    </update>

    <delete id="deleteById">
        DELETE  FROM  tbl_employee WHERE id = #{id}
    </delete>

    <!--
        有时候用户在做数据的写操作的时候,希望得到一个结果的反馈,
        写入成功返回true,写入失败返回false;

        MyBatis在涉及到写操作的删、除、改三种操作是允许返回true或者false的,
        直接在接口中将这些方法的返回值定义为boolean即可,但是在mapper文件
        中标签中不需要指定返回值类型。原理是根据SQL语句影响的行数来
        封装返回值的,如果影响的行数为0则返回false,如果影响的行数大于0则返回true
    -->
    <insert id="saveEmp" parameterType="employee">
        INSERT INTO tbl_employee(last_nameMM,gender,email)VALUES
        (#{lastName}, #{gender}, #{email})
    </insert>

</mapper>

 在看完常用的增删改查的操作后,再来看下sql标签,他是用来抽取可重用的SQL语句元素,有点类似于模板的意思,一般都和include一起出现,就是一处定义,处处可引用,看一下小栗子:

<!--抽取可重用的SQL-->
<sql id="insertSql">
    last_NameMm, gender, email
</sql>

<insert id="insertTest">
    INSERT INTO tbl_employee(
    <!--引用外部定义的SQL-->
    <include refid="insertSql"></include>
    ) VALUES (#{lastName}, #{gender}, #{email})
</insert>

同时在include标签中可以自定属性,反过来在sql中使用,注意使用形式${属性},比如:

<!--抽取可重用的SQL-->
<sql id="insertSql">
    <!--使用include标签中自定义的属性,注意形式-->
    last_NameMm, gender, email, ${testProperty}
</sql>

<insert id="insertTest">
    INSERT INTO tbl_employee(
    <!--引用外部定义的SQL-->
    <include refid="insertSql">
        <!--自定义属性-->
        <property name="testProperty" value="abc" />
    </include>
    ) VALUES (#{lastName}, #{gender}, #{email}, #{testABC})
</insert>

那么最后拼接出来的SQL语句为:

INSERT INTO tbl_employee(last_NameMm, gender, email, abc) 
VALUES (?,?,?,?)

如果希望写入操作的方法返回booleanmapper中正常写,接口中指明返回类型,比如public boolean saveEmp(Employee employee);在返回的时候MyBatis会直接将SQL语句影响的行数转成boolean类型的返回值。在跑测试案例的时需要手动提交sqlSession,上面案例中我们过去Session的方式是sessionFactory.openSession(),这种方式获取的Session是不会自动提交的,关闭Session前,必须先进行session.cmoit()的执行,之前不写是因为只是用来对数据库进行读操作,但凡涉及到数据库的写操作就必须进行提交,否则MySQL只是发送SQL语句,但是这个SQL不会落实到数据库上对其造成影响,但是可以通过sessionFactory.openSession(true)的方式获取可以自动提交的Session

4.1 获取自增主键

 在实际开发中,保存一条记录时,可能想要获取它的id,由于id在数据库中设置的是自增,所以一般不会传进去,比如下面的:

Employee employee = new Employee();
employee.setLastName("testInsert2");
employee.setEmail("insert2@163.com");
employee.setGender(0);
System.out.println(employeeDao.saveEmp(employee));
System.out.println("新增主键:" + employee.getId());

//涉及到写操作的行为必须手动提交,否则不会提交,读操作到没有什么影响
sqlSession.commit();

就这样去做,获取的新增主键为0,必须在mapper文件对应的标签设置使用主键生成器,并且将这个生成的id绑定到JavaBean中的某个属性上,在mapper文件使用useGeneratedKeys="true"开启此功能,使用keyProperty="属性名"将获取的主键绑定到javaBean的属性上即可:

<insert id="saveEmp" parameterType="employee" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO tbl_employee(last_nameMM,gender,email)VALUES
    (#{lastName}, #{gender}, #{email})
</insert>

再次测试上面的案例即可获取到新增id的值;

4.2 获取非自增主键

 上面一小节是通过数据库将主键设置为自增的方式,id属性可以直接不传入,但是对于不支持id自增的数据库(比如Oracle数据库),怎么来做呢,比如id通过序列的方式生成,那么怎么来获取id。传入的保存记录对象照样不用传id属性,我们在SQL语句上做文章,下面以序列id为例看一下MyBatis是如何完成插入操作的:
首先到数据库中查询用户的序列表:select * from user_sequences;,可以查看用户所有的序列表的,然后选择对应表的序列表,这里以表employees主键对应的序列表EMPLOYEES_SEQ为例,然后xml中如下:

<insert id="saveEmp" databaseId="oracle">
        <!--首先去序列表中查询下一个序列值,然后绑定到JavaBean的id属性上
        并且通过order属性指定这个SQL在主体SQL(即查询SQL)前执行即可。
        -->
        <selectKey keyProperty="id" order="BEFORE">
            SELECT EMPLOYEES_SEQ.nextval FROM dual
        </selectKey>
        <!--执行主体SQL时可以直接取上面SQL查询并封装在对象id属性中-->
        INSERT  INTO employees(id,last_name,gender,email) VALUES
        (#{id},#{lastName}, #{gender}, #{email})
</insert>

4.3 MyBatis的传参处理

 MyBatis中对于参数的处理,有多种情况,一一做说明:

  • 对于单个参数,MyBatis不会做处理,约定俗成接口方法中参数名是什么就写成#{参数名}的形式,比如:
public Employee getEmployeeById(int id);

那在mapper中写的映射语句为:

<select id="getEmployeeById" resultType="employee">
        select * from tbl_employee where id=#{id}
</select>

其实这里只有一个参数,不管你接口方法中参数名是什么,在mapper文件中可以任意写,甚至可以瞎写,比如上述的映射语句可以写成:

<select id="getEmployeeById" resultType="employee">
        select * from tbl_employee where id=#{hh}
</select>

只不过写成接口方法中的参数名好理解些。

  • 对于多个参数,MyBatis会自动帮我们处理(封装成一个Map),其中MapKey为依次为param1param2param3……以此类推,接口方法中传入几个参数,value就是调用接口时传入的具体内容,就这样封装,然后在mapper中获取其中的属性时就用#{Key名}的发方式来获取封装好的value,这里首先做一个说明,MyBatis封装的多参数Map的名字叫做_parameter,这也是MyBatis的内置参数,在mapper文件中可以直接使用它来判断诸如传入的多参数对象(这个Map对象可以是MyBatais自动封装的,也可以是一个JavaBean对象)是否为空,比如下面的:
public Employee getByIdAndLastName(int id, String lastName);

然后在mapper文件中获取:

<select id="getByIdAndLastName" resultType="employee">
    <!--这里参数完整的写法应该为#{_parameter.param1},但是通常会省去_parameter-->
    SELECT  * FROM tbl_employee e WHERE e.id=#{param1} AND e.last_namemm=#{param2}
</select>

但是在实际开发中,如果有多个参数,以param1param2……这种方式出现在SQL语句中确实不能起到见名知意的效果,所以这里可以在接口方法参数前面使用@Param("别名")注解,这样就可以指定MyBatis在封装Map时的Key,最后在mapper中取参数的时候就可以用我们自己指定的别名来获取,而不用param1param2这种方式来获取,比如下面:

public Employee getByIdAndLastName(@Param("id") int id, @Param("lastName") String lastName);

mapper中获取

<select id="getByIdAndLastName" resultType="employee">
    SELECT  * FROM tbl_employee e WHERE e.id=#{id} AND e.last_namemm=#{lastName}
</select>

 对于多个参数的处理,除了使用的上面MyBatis自动封装的Map,我们还可以手动自己封装Map,比如上面的getByIdAndLastName方法,其实他没有必要封装,因为传入的参数比较少,使用MyBatis自动封装就挺好,这里纯粹是为了演示手动封装参数的方法而为之:

public Employee getByMap(Map map);

mapper中:

<!--这里的#{}中的别名按照map中传入的key值来取-->
<select id="getByMap" resultType="employee">
    SELECT  * FROM tbl_employee WHERE id=#{id} and last_namemm=#{lastName}
</select>

调用测试:

Map<String, Object> map = new HashMap();
map.put("id", 6);
map.put("lastName","jack");
Employee e = employeeDao.getByMap(map);
System.out.println(e);

【注意】这里的手动封装的Map中的key值就是我们在mapper中使用的别名。当然如果传入的参数刚好完全是JavaBean的几个属性(如果id可以不带id),那么接口传入参数可以直接传入JavaBean对象,在mapper中获取的它的属性时,直接使用#{属性名}的方式来获取即可。

 除了上述的注意的地方还有几个需要注意的点:

/*MyBatis自动封装,mapper获取方式:
* id: #{id}或者#{param1}
* name: #{param2}
*/
public Employee getEmp(@Param("id")int id, String name);

/*MyBatis自动封装,mapper获取方式:
* id: #{param1}
* name: #{param2.name}而不是直接用#{name}
*/
public Employee getEmp(int id, Employee e);

/*MyBatis对Collection(List、Set)和数组有特殊的封装方式,
* 虽然也是封装到Map中,但是Map的Key的名字变了,可以统一
* 使用“collection”,或者:
* List使用“list”,如果取list的首个元素可以使用#{list[0]}
* 数据使用“array”
*/
public Employee getById(List<Integer> ids);

4.4 mapper中取值方式

 在SQL映射文件中,我们通常是以#{}形式来获取传入的参数,但是在MyBatis中也是可以用${}的形式来取值的,两者虽然都能达到我们所需要的结果,但是差异还是很大的。

  • #{}是以预编译的形式将参数设置到sql语句中,PreparedStatement可以防止sql注入;
  • ${}是以拼接sql语句的形式直接将参数搞到SQL语句上,有安全问题。

比如举个栗子:

<select id="getByMap" resultType="employee">
    SELECT  * FROM tbl_employee WHERE id=${id} and last_namemm=#{lastName}
</select>

我们将参数封装好调用接口,从控制台的输出可以看到MyBatis的发送的SQL语句如下:

SELECT * FROM tbl_employee WHERE id=6 and last_namemm=?

和上面所说的一样,${}取值id的参数直接拼接到了SQL语句上,而#{}取值的lastName则是以占位符?的形式在SQL语句中,所以一般情况下我们都是使用#{}

 【注意】那还有一种取值方式在哪种场景下使用呢:原生JDBC不支持占位符的地方我们就必须用${}进行SQL语句的拼接了(比如数据库的表名,根据某个字段排序,里面的“字段”和排序的方式名都是不支持占位符的),他就是不支持占位符的,那此时就必须使用${}的方式了,使用’#{}'会报错,比如公司的员工的薪资表按照“年份_salary”的方式命名的,我们需要动态的查询薪资表的记录(有时查2016_salary有时需要查2018_salary表),此时我们可以这样写:

public Salary getSalary(int year,String user);

在mapper中就可以这么写:

<select id="getSalary" resultType=salary>
    select * from ${param1}_salary where username=#{param2}
</select>

调用接口getSalary(2015,"jack")此时MyBatis发送的SQL语句为:

select * from 2015_salary where username=?

是完美符合我们的要求的。

4.5 关于传入参数为null的情况

 在MyBatis中,如果方法参数传入null,那么它会自动应映射为jdbcType类中的OTHER(Types.OTHER)的形式即OTHER类型,大部分数据库是可以识别的(比如MySQL、Sql Server),但是在有些数据库不能识别MyBatis处理nullOTHER类型(比如Oracle数据库就不能识别),这个时候就需要在传值的时候指定如果参数传入为null,MyBatis应该将它转成JDBCType中什么类型,正常情况传入的null转成NULL都是可以被识别的(Oracle和MySQL以及MSSQL)。有两种用法如下:

  • 在mapper文件中直接对指定传入的参数进行null值转换的处理:
<!--比如在保存Employee时,传入的eamil为null时,
    MyBatis会自动将其转成Oracle数据库可以识别的NULL类型
    而不是转成默认的OTHER类型
-->
<insert id="saveEmp" parameterType="employee" databaseId="oracle">
    INSERT  INTO employees(last_name,gender,email) VALUES
        (#{lastName}, #{gender}, #{email, jdbcType=NULL})
</insert>
  • 当然,还可以直接在MyBatis的全局配置文件中的<settings>中进行设置,这是针对所有的null设置,而不是像第一种方法对某个字段进行null的处理:
<settings>
    <setting name="jdbcTypeForNull" value="NULL"/>
</settings>

可以如果配置的话建议直接用第二种方式,将全局null值在转换的时候转成NULL而不是OTHER

4.6 关于接口方法不同返回值类型的处理

 在接口的查询的方法中会有返回值,也有可能没有返回值,返回值可以是一个基本数据对象,也可以是一个对象(pojo对象直接指定为设定的别名),下面对接口中不同方法的不同返回值的类型的处理做一个探讨;在mapper映射文件中存在返回值的只有<select>标签(除了MyBatis将增、删、改三种操作影响的行数转成boolean类型的返回值),返回值可以用<select>标签中的resultType属性指定(指定为别名或者全类名)或者resultMap自定义封装,如果返回的是集合类型,那么指定为集合中元素的类型,注意resultType属性不能和resultMap属性同时使用。

 比如返回的是一个List对象,据上所述,可以如下:

public List<Employee> getEmps();

在mapper中的返回值类型需要指定为List集合中存放元素的类型,这个方法的映射就是Employee这个JavaBean的实体类型。

<select id="getEmps" resultType="employee">
    select * from tbl_employee
</select>

 如果接口的返回值类型是Map类型,这里需要注意一下,如果返回值就是由一条记录里面所有属性都单独封装起来的多个Map,此时key就是列名,value就是对应的属性值,比如下面的:

public Map<String, Object> getEmpByIdReturnMap(int id);

mapper文件中为:

<!--注意这里的返回值类型写map,MyBatis对常用的数据类型已经起了别名-->
<select id="getEmpByIdReturnMap" resultType="map">
    SELECT  * FROM tbl_employee WHERE id=#{id}
</select>

最后调用接口返回值为Map类型,JavaBean的每个属性在数据库表中对应的列名都被MyBatis封装到Map中,Key为属性名,value就是查询出来的对应属性值,上面是将一个JavaBean对象的各个对应表的字段名封装到多个Map中。但是更好的做法是直接将javaBean作为一个对象封装到一个Map中,而不是将各个字段单独封装成多个Map,就像Map<Integer, Employee>的形式,以Employee的主键id作为Map的Key,如下:

//这要需要指定Map的key是什么,这里是id,当然也可以是其他属性
//需要注意下key的唯一性
@MapKey("id")
public Map<Integer, Employee> getEmps();

mapper文件:

<!--对于集合而言,resultType永远都是集合里面元素的类型-->
<select id="getEmps" resultType="employee">
    SELECT  * FROM tbl_employee
</select>

 返回值除了上述的resultType指定返回值类型外(基本用于MyBatis提供的自动封装),还可以使用一个非常重要的resultMap属性指定自定义结果集。如果JavaBean的属性名和数据库对应的列名不一致,MyBatis是无法自动封装的,出现这种情况,一般有以下几种方法:

  • 在写SQL语句时指定别名(如:select last_name lastName);
  • 开启下划线驼峰命名法(<setting name="mapUnderscoreToCamelCase" value="true"/>),前提是满足条件,如:数据库C_Action==>JavaBean中的cAtion;
  • 返回值使用resultMap来自定义返回集,指定Employee中的属性对应数据库中的哪个字段。

 在开始之前先把resultMap标签中注意点说一下,和<mapper>标签一样,resultMap下的子标签必须按照一定的顺序排列,否则报错,正确顺序如下:constructor?id*result*association*collection*discriminator?

 下面具体看一下resultMap的用法:

//接口中的方法照常写
public Employee getEmployeeById(int id);

mapper文件中使用自定义类型:

<!--自定封装的规则
    id:自定义的封装类型的标识符,方便后面引用
    type:指定自定义封装的JavaBean的类,可以用全类名也可以使用别名
-->
<resultMap id="MyEmp" type="employee">
    <!--指定封装类型的主键,
        column:数据库中的列名
        property:JavaBean中与数据库列名相对应的属性名

        这里自定义封装类型,通常是把所有的JavaBean属性都封装
        但是注意,那些不符合MyBatis自动封装的属性是不能被自动封装的;

        比如:属性名和列名相同的(不分大小写),以及开启下划线驼峰命名规则
        后,刚好可以完成这种转换的才能自动封装
    -->
    <id column="id" property="id"/>
    <!--result用于定义非主键列名和javaBean的属性名的对应关系-->
    <result column="last_nameMm" property="lastName"/>
    <result column="gender" property="gender"/>
    <result column="email" property="email"/>
</resultMap>

<!--这里的返回值类型用resultMap指定为自定义的类型,不能使用resultType指定了-->
<select id="getEmployeeById" resultMap="MyEmp">
    select * from tbl_employee where id = #{id}
</select>

4.7 关于级联属性的查询处理

 在实际开发中会遇到不少级联属性的查询,小栗子(EmployeeDepartmentEmployee中持有Department对象),

 第一部分以多对一的关联关系为例,直接看下两个JavaBean:

public class Employee {
    private int id;
    private String lastName;
    private int gender;
    private String email;
    private Department dep;

    //getter and setter as well as toString(no dep)
}

public class Department {
    private int dId;
    private String dpName;

    //getter and setter as well toString
}

数据库的表结构为:tbl_departmentstbl_employee
这里写图片描述
这里主要是针对在查询Employee对象时怎么将它关联的Department属性查询出来。

 看一下具体的实现,第一种方式,自定义封装返回类型,mapper中如下:

<resultMap id="EmpWithDep" type="employee">
    <id column="id" property="id"/>
    <result column="last_nameMn" property="lastName"/>
    <result column="gender" property="gender"/>
    <result column="email" property="email"/>
    <!--级联属性直接用Employee里面的“dep”去获取关联的Department属性,
        即以“属性.属性”的方式来关联Department对应的表中的列名
    -->
    <result column="d_id" property="dep.dId"/>
    <result column="dp_Name" property="dep.dpName"/>
</resultMap>


<!--
    场景:查询Employee的时候同时把关联的Department查询出来,
    同时查询两张表,多表联查,在查询时SQL写成级联查询的语句
    SQL语句:select * from tbl_employee e, tbl_departments d
                where d.id = e.department and e.id = #{id}
-->
<select id="getEmpAndDpById" resultMap="EmpWithDep">
    SELECT *
    FROM tbl_employee e, tbl_departments d
    WHERE d.d_id = e.department AND e.id = #{id}
</select>

这样以后直接调用getEmpAndDpById(id)方法即可获取Employee以及它关联的Department对象。

第二种方式,使用association标签在封装ResultMap时将dep属性和Department对象关联:

<resultMap id="EmpWithDep2" type="employee">
    <id column="id" property="id"/>
    <result column="last_nameMn" property="lastName"/>
    <result column="gender" property="gender"/>
    <result column="email" property="email"/>
    <!--使用 association 标签指定级联属性,指定Employee对象中的
        dep属性是一个Department对象。
        javaType:指定该属性为何种类型,可以写全类名或者别名

        然后在这个标签里面定义Department的各个字段和属性的封装,而不需要
        通过"属性.属性"的形式去定义,而是以该JavaBean中的属性名来封装
    -->
    <association property="dep" javaType="department">
        <result column="d_id" property="dId"/>
        <result column="dp_Name" property="dpName"/>
    </association>

</resultMap>

使用的时候向上面的用法一样使用即可。

 除了上述的方式,还有一种利用association标签进行分步查询的实现方式,步骤多一些,看一下完整的实现流程:先直接根据员工id获取员工信息,再根据员工信息里面的dep属性(虽然写的是Department dep,实则查询出来是关联的id)去获取Department对象属性,将复杂的级联查询SQL拆分为两个简单的SQL。

 首先提供一个Department根据它的id获取对象的方法,所以这里直接给Department这个对象提供一个接口(实际开发中一般都会用到),mapper文件如下(接口自己脑补下):

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace:名称空间,写关联接口的全类名-->
<mapper namespace="com.hhu.dao.DepDao">
    <select id="getDepById" resultType="department">
        SELECT  * FROM  tbl_departments WHERE d_id=#{id}
    </select>
</mapper>

注意写完了要将mapper文件注册到全局配置文件中mybatis-config.xml,然后在到EmployeeMapper.xml搞一下:

<resultMap id="EmpStep" type="employee">
    <id column="id" property="id"/>
    <result column="last_nameMm" property="lastName"/>
    <result column="gender" property="gender"/>
    <result column="email" property="email"/>
    <!--利用association标签实现级联属性分步查询,
        property:指明Employee对象中的哪个属性是级联属性;

        select:指明该级联对象属性通过哪个接口中的哪个方法查询获取;

        column:将查询员工时候查询到的dep属性的id绑定到即上述接口方法,
                就是上述调用接口中方法所需的参数,虽然查询员工查询到的是
                Department对象dep属性,但是在数据库中是外键的形式,属性列名为department
                

        上述总体流程:根据传过来的id去查询员工,再根据员工中dep.id去查Department
        -->
    <association property="dep"
                    select="com.hhu.dao.DepDao.getDepById" column="department">
    </association>
</resultMap>
<select id="getEmpAndDepByStep" resultMap="EmpStep">
    SELECT  * from tbl_employee WHERE id=#{id}
</select>

这样就将一开始级联查询SQL语句分解成两个简单的SQL语句进行分步查询。

【注意】在做分步查询时,可以在上述的association标签中使用fetchType标签指定级联属性是否进行懒加载,fetchType="lazy"表示进行懒加载,fetchType="eager"表示不进行懒加载,注意fetchType可以覆盖全局配置文件中对全局级联属性懒加载的设置,在哪个级联属性上加了fetchType只改变当前的级联属性,其他地方的级联属性还是跟全局走

 第二部分在第一部分的基础上增加一个一对多的新需求,即在对Department查询时也想将该部门下的员工也一起查询出来,基于上面的方式,我们也依次来看一下实现:

首先在Department类里面添上员工集合吧,不然怎么查询对吧:

//改造原有的Department类添加如下的属性
private List<Employee> employees;
// add getter and setter

对应的Department的mapper文件自定义结果集,注意集合属性用collection标签:

<resultMap id="DepWithEmp" type="department">
    <id column="d_id" property="dId"/>
    <result column="dp_Name" property="dpName"/>

    <!--定义Department中emps集合属性
        集合属性用collection标签进行定义
        property:javaBean中的集合属性名
        ofType:集合中元素的类型,可以写全类名或者别名
    -->
    <collection property="employees" ofType="employee">
        <!--在collection标签下定义集合中元素即Employee的封装规则-->
        <id column="id" property="id"/>
        <result column="last_nameMm" property="lastName"/>
        <result column="gender" property="gender"/>
        <result column="email" property="email"/>
    </collection>
</resultMap>


<!--
    在查询部门的时候将所有属于这个部门的员工也查询出来,采用join进行多表联查
    SELECT * FROM tbl_departments d
    LEFT JOIN tbl_employee e ON d.d_id = e.department
    WHERE d.d_id = #{id}

    注意左外连接的写法,ON关键字后面接的是join的条件,切记SQL语句中都是表的
    列名,除了别名别特么瞎参JavaBean的属性名,再错我打你!!
-->
<select id="getDepPlusById" resultMap="DepWithEmp">
    SELECT * FROM tbl_departments d
    LEFT JOIN tbl_employee e ON d.d_id = e.department
    WHERE d.d_id = #{id}
</select>

上面这样就可以用了,经过上面多对一的情况,这里的一对多也不难想到分步查询的方式,同样是可以的(分步查询时可以使用延迟加载,这里还是比较推荐这种方式的),思路还是和上面的类似:我先查询Department的信息,然后利用查询出来Department信息中的did属性去Employee对应的表中根据did去查询所有员工信息。好,看具体实现:

首先在员工中提供一个根据部门id查询员工的方法:

public List<Employee> getAllEmployeeByDep(int id);

比较简单,在mapper中映射SQL:

<!--用户部门id查询所有员工,返回值是集合,那resultType就写集合中元素的类型
    可以写全类名或者别名
-->
<select id="getAllEmployeeByDep" resultType="employee">
    select id, last_nameMm lastName, gender,email from tbl_employee WHERE department=#{id}
</select>

前提做好了,就去Department的mapper中做具体的查询工作了:

<!--进行分步查询,首先查询Department部门的信息,然后根据Department的id
    去查询员工信息
-->
<resultMap id="DepWithEmpsStep" type="department">
    <id column="d_id" property="dId"/>
    <result column="dp_Name" property="dpName"/>
    <!--将查询的到Department的id对应的列名d_id作为参数传入
        Employee接口中查询所有员工的方法中
    -->
    <collection property="employees" select="com.hhu.dao.EmployeeDao.getAllEmployeeByDep"
                    column="d_id"></collection>
</resultMap>
<select id="getDepPlusByIdStep" resultMap="DepWithEmpsStep">
    SELECT * FROM tbl_departments WHERE d_id=#{id}
</select>

这就号完成了一对多关联关系的分步查询了。

【注意】

  • 上述的collection标签中同样可以使用fetchType标签指定级联属性是否进行懒加载,fetchType="lazy"表示进行懒加载,fetchType="eager"表示不进行懒加载,注意fetchType可以覆盖全局配置文件中对全局级联属性懒加载的设置,在哪个级联属性上加了fetchType只改变当前的级联属性,其他地方的级联属性还是跟全局走
  • 上面不管级联属性单个对象用association标签做分步,还是级联属性是集合用collection标签做分步,在调用第二步接口方法查询级联属性时需要的参数都是一个,使用column标签将第一步查询结果中所需要的参数列名写进入,但是如果这个接口参数有多个的情况下,可以使用map的形式进行封装(用“key=value”的形式表示,key随便取,就是在接口方法SQL映射调用处就用这个名字,value就是传入参数的列名),比如column={dId=d_id, lastName=last_NameMm}

【思考】这里多对一关联关系情况下做分步查询情况下查询“一方”时使用的是association标签,在一对多情况下做分步查询情况查询“多方”时使用的是collection标签。这是很多老师和网友说的情况,实则我在跑测试Demo的时候,发现居然可以混用,这一点让我有些迷糊,标记一下。

4.8 关于懒加载

 在上面刚提过级联属性的查询问题,关于级联属性这一块自然又可以想到懒加载问题,这一块本来是应该放到上一大章节里,但是放在级联属性这一块很和谐。比如在上面的栗子中,我们查询Employee的时候会同时查询到它里面的Department属性,但是懒加载就是想如果我只用Employee里面除dep外的任意属性,我完全没有必要去加载它关联的Department的对象呀。是的基于这种需求,懒加载需要使用association标签实现的分步查询稍加配置就可以实现了(很好理解,分两步,我要什么就发对应的SQL语句去查询获取,如果是整体的级联SQL语句,根本无法实现懒加载啊)。

 基于分步查询的方式配置好了之后,只需要在全局配置文件中mybatis-config.xml配置开启懒加载和禁用侵入懒加载即可:

<settings>

    <!--开启延迟加载,注意是两个配置
        lazyLoadingEnabled:懒加载是否开启
        aggressiveLazyLoading:侵入懒加载,开启将不会进行懒加载,需要禁用
    -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="aggressiveLazyLoading" value="false"/>
    
</settings>

这里我在测试的时候,遇到特别诡异的情况,报出了如下异常信息

org.apache.ibatis.builder.BuilderException: The setting LazyLoadingEnabled is not known.

无赖啊,搞了半天,拼写没错,复制的官方文档上的,没管它,晚上异常就消失了( ̄▽ ̄)"。

 在设置过全局配置文件后,我们在做分步查询的时可以实现懒加载了,可以通过association标签和collection标签中的fetchType属性进行局部级联属性懒加载的更改。

4.9 鉴别器discriminator

 在MyBatis中允许用户对返回值使用resultMap标签进行自定义封装,这里有种需求,就是根据查询的结果进行不同的返回结果集封装,比如上述的栗子中,在查询员工的时候根据性别属性有额外要求:

  • 如果性别为女,就把其所属部门查询出来;
  • 如果性别为男,就把lastName的值赋给email;

好了,对于上述两中不同的封装行为我们做实现:

<!--鉴别器的使用
constructor?,id*,result*,association*,collection*,discriminator?
-->
<resultMap id="EmpGen" type="employee">
    <id column="id" property="id"/>
    <result column="gender" property="gender"/>

    <result column="last_NameMm" property="lastName"/>
    <!--
        column:根据哪一列进行判断就写哪一列的列名,就是条件列名
        javaType:上面列属性的类型
    -->
    <discriminator javaType="int" column="gender">
        <!--
        0为女人:就把其所属部门查询出来
        1为男人:就把lastName的值赋给email
        value:就是上述条件列名的值
        resultType/resultMap:封装结果的类型,可以是全类名或者是别名,这个属性不能丢
        -->
        <case value="0" resultType="com.hhu.entities.Employee">
            <result column="email" property="email"/>
            <association property="dep" select="com.hhu.dao.DepDao.getDepById" column="department"/>
        </case>
        <case value="1" resultType="com.hhu.entities.Employee">
            <result column="last_NameMm" property="email"/>
        </case>

    </discriminator>

</resultMap>

<select id="getEmpGen" resultMap="EmpGen">
    SELECT  * FROM tbl_employee WHERE id=#{id}
</select>

在做上述案例的时候,不知道怎么回事,IDEA一直报别名异常,前面所有SQL映射全部失效,重新搞了个SQL映射文件才搞定。
【注意】在discriminator标签前面的不管是男人还是女人都会被封装,唯一不同的就是鉴别器discriminator中的属性会根据指定列的属性值的不同作不同的封装行为,这里男人、女人有封装行为差异的就是email的属性和是否查询所在部门两种行为。

5. MyBatis中动态SQL

 动态SQL是MyBatis提供的一项重大特性,如果你使用过jdbc或者类型的框架,一定了解拼接SQL语句的痛苦,有时候还要根据传入的参数条件变换不同的SQL语句(比如传入4个参数就需要拼接4个,传入5个就要拼接5个),而且哪些地方少个空格或其他字符就会有SQL语句的异常,MyBatis为此提供了动态SQL的功能,其中动态SQL标签有:

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

5.1 if标签

 首先来看if标签,在如下场景:我传给接口方法有几个合法参数,那么对应映射的SQL语句就拼接几个条件,比如前面章节的Employee对象,为了方便测试,将它所有的属性的类型全部改为String类型,那么正常来说,用原生jdbc来做会非常麻烦,因为不确定哪个属性存在、哪个属性不存在,这里可以利用MyBatis提供的if标签来做这个需求:

接口方法

/*带啥条件就往Employee类型的参数中封装,
* 明白为啥要改造JavaBean的属性为String了吧
*/
public List<Employee> getEmpsByConditionIf(Employee employee);

mapper映射的SQL

<select id="getEmpsByConditionIf" resultType="employee">
    SELECT * FROM tbl_employee WHERE
    <!--传入的参数有什么就将什么拼接上去,没有就不拼接
    test:就是判断的表达式(OGNL表达式,可参看官方文档)
    注意test表达式中的id是从JavaBean属性参数里面取的,只不过
    OGNL表达式不用`#`就能取到
    -->
    <if test="id!=null">id=#{id}</if>
    <!--lastName不为null且不为空串,注意空串的写法' '-->
    <if test="lastName!=null and lastName!=''">AND last_nameMm=#{lastName}</if>
    <if test="gender==0 or gender==1">AND gender=#{gender}</if>
    <!--trim()函数用于去除字符串中的所有空格符-->
    <if test="email!=null and email.trim()!=''">AND email=#{email}</if>
</select>

上述的SQL语句可以实现体条件拼接,注意if的语法,只有符合test属性中表达式条件的时候才会将内部的SQL拼接上去,但是这样拼接如果id为空会有一个问题,SQL语句为

SELECT * FROM tbl_employee WHERE and ……

这明显不太好,哪有WHERE后面直接跟AND的,SQL有语法错误,AND不合法,这里有两种解决方案:

  • 第一种方案,是jdbc中经常这么搞的,在拼接条件之前先搞一个1=1的条件,后续所有的条件追加AND ...即可,这样不管是有没有id属性,都是合法的,就变成了SELECT * FROM tbl_employee WHERE 1=1,然后再追加if标签即可,如下:
<select id="getEmpsByConditionIf" resultType="employee">
    SELECT * FROM tbl_employee WHERE 1=1

    <if test="id!=null">AND id=#{id}</if>
    <!--lastName不为null且不为空串,注意空串的写法' '-->
    <if test="lastName!=null and lastName!=''">AND last_nameMm=#{lastName}</if>
    <if test="gender==0 or gender==1">AND gender=#{gender}</if>
    <!--trim()函数用于去除字符串中的所有空格符-->
    <if test="email!=null and email.trim()!=''">AND email=#{email}</if>
</select>
  • 第二种方案,MyBatis提供了一中独有的解决方案,将整个判断的语句使用<where>标签来包裹即可(此时注意将之前SQL语句中的WHERE关键字去除),MyBatis会自动帮我处理掉像上述情况多出来的AND这类关键字(只会去除每个一条件句首的多余的AND关键字,如果将AND放在每个条件的句末仍然可能会出现语法错误),改造后如下:
<select id="getEmpsByConditionIf" resultType="employee">
    SELECT * FROM tbl_employee 
    <where>
        <if test="id!=null">id=#{id}</if>
        <!--lastName不为null且不为空串,注意空串的写法' '-->
        <if test="lastName!=null and lastName!=''">AND last_nameMm=#{lastName}</if>
        <if test="gender==0 or gender==1">AND gender=#{gender}</if>
        <!--trim()函数用于去除字符串中的所有空格符-->
        <if test="email!=null and email.trim()!=''">AND email=#{email}</if>
    </where>
</select>

5.2 trim标签

 基于上述的案例,我们继续来说,在使用where标签或者加1=1条件可以解决id没有传的情况语法错误,这个都是条件SQL中AND写在前面的,万一我就是犯贱,我就像写在后面:

<select id="getEmpByTrim" resultType="employee">
    SELECT * FROM tbl_employee
    <where>
        <if test="id!=null">id=#{id} AND </if>
        <if test="lastName!=null and lastName!=''">last_NameMm=#{lastName} AND </if>
        <if test="gender==0 or gender==1">gender=#{gender} AND </if>
        <if test="email!=null and email!=''">email=#{email}</if>
    </where>
</select>

此时用where标签无法解决问题,它只能处理每个条件SQL句首的AND标签,如果查询条件不带email属性,SQL语句也是有语法问题的,MyBatis为有这样强迫症的同学提供了trim标签,来看一下它是如何满足这群迫症的:

<select id="getEmpByTrim" resultType="employee">
    SELECT * FROM tbl_employee
    <!--看一下具体各个参数的含义,各个参数不分大小写,
    prefix:为trim标签整体包裹的条件语句添加指定的前缀;
    prefixOverrides:trim标签整体包裹的条件中如果含有指定的前缀就去除它;
    suffix:为trim标签整体包裹的条件语句添加指定的后缀;
    suffixOverrides:trim标签整体包裹的条件中如果含有指定的后缀就去除它;
    -->
    <trim prefix="WHERE" prefixOverrides="" suffix="" suffixOverrides="and">
        <if test="id!=null">id=#{id} AND </if>
        <if test="lastName!=null and lastName!=''">last_NameMm=#{lastName} AND </if>
        <if test="gender==0 or gender==1">gender=#{gender} AND </if>
        <if test="email!=null and email!=''">email=#{email}</if>
    </trim>
</select>

注意各个参数是针对的拼接后的trim标签中的整体,而不是针对的单个条件SQL。

5.3 choose标签

 分支选择,有点类似与switch-case的用法,比如下面有一种查询场景,JavaBean中带了哪一个属性就按照哪个属性条件去查,而不是想上面拼接很多,这里有且仅带一个属性:

<select id="getEmpsByChoose" resultType="employee">
    SELECT * FROM tbl_employee WHERE
    <choose>
        <when test="id!=null">id=#{id}</when>
        <when test="lastName!=null">last_NameMm=#{lastName}</when>
        <when test="gender==0 or gender==1">gender=#{gender}</when>
        <when test="email!=null and email.trim()!=''">email=#{email}</when>
        <!--这个是上述条件都不满足的时候,就指定下面的条件,可以自定义-->
        <otherwise>1=1</otherwise>
    </choose>
</select>

【注意】这里面choose标签只有一个when标签生效!!就是说最后MyBatis拼接出来的SQL语句最后只带一个条件,如果同时有多个条件生效,那么在choose标签中越靠前的优先级别越高!

5.4 set标签

 上面的几个标签都是查询时候的标签,但是这里的set标签是用于修改时候的标签。在前面章节更新Employee对象的时候,是带所有字段进行更新的,但是在动态SQL中显然是探讨的未知字段的更新(即不知道要跟新哪些字段),可能想到前面根据if标签,自然想到这么去搞:

    <!--动态更新Employee对象-->
<select id="updateEmployee">
    UPDATE tbl_employee SET
    <if test="lastName!=null and lastName.trim()!=''">last_nameMm=#{lastName},</if>
    <if test="gender==0 or gender==1">gender=#{gender},</if>
    <if test="email!=null and email.trim()!=''">email=#{email}</if>
    WHERE id=#{id}
</select>

这样以后现然可以动态发送SQL了,比如:

UPDATE tbl_employee SET last_nameMm=?,gender=?,email=? WHERE id=?

这个全字段,和之前引入<where>标签时的情况一样,如果只有前面的字段没有最后的email字段就会出现类似于gender=?,这样尴尬的状态,多了一个,,出现SQL语法错误,联系之前的标签不难想到trim标签,难后对其进行改造,思路正确,如下:

<!--动态更新Employee对象-->
<select id="updateEmployee">
    UPDATE tbl_employee SET
    <trim suffixOverrides="," suffix="" prefixOverrides="" prefix="">
        <if test="lastName!=null and lastName.trim()!=''">last_nameMm=#{lastName},</if>
        <if test="gender==0 or gender==1">gender=#{gender},</if>
        <if test="email!=null and email.trim()!=''">email=#{email}</if>
    </trim>
    WHERE id=#{id}
</select>

如果被tirm包裹的整体条件语句出现后缀,,那么就将它去掉,这是一种解决思路。在MyBatis中,它还提供了一个set标签来帮助我们解决在动态更新字段时候出现多余,的尴尬,和where标签如出一辙,记得拿到之前手写的SET,如下:

<!--动态更新Employee对象-->
<select id="updateEmployee">
    UPDATE tbl_employee SET
    <set>
        <if test="lastName!=null and lastName.trim()!=''">last_nameMm=#{lastName},</if>
        <if test="gender==0 or gender==1">gender=#{gender},</if>
        <if test="email!=null and email.trim()!=''">email=#{email}</if>
    </set>
    WHERE id=#{id}
</select>

5.5 foreach标签

 好了,跋山涉水终于看到了我们最后一个标签foreach标签了,顾名思义,就是用于遍历集合的标签,遍历的都是跟批量操作绑在一起的,批量操作的话又分为好几种场景:批量查询、批量更新、批量插入、批量删除。下面我们就按照上述的几种场景来看一下foreach标签的用法。

 第一种场景:批量查询。这里搞一个场景,我想根据id集合批量查询员工,传入的参数是一个员工id的集合,那么查询的时候SQL语句是想如下这样的写:

SELECT * FROM tbl_employee
WHERE id IN (1,2,3,4)

主要是看(1,2,3,4)这个玩意儿这么写,好,具体来看语法:

接口方法:

/* 批量查询,传入的参数是一个集合,注意集合在MyBatis中封装方式
* 集合类型的参数MyBatis自动将它封装到一个Map中,key就叫collection
* (根据不同的集合类型还有其他的名字,比如List类型的参数还可以叫list),
* 当然我们也可以在接口方法的传入参数上使用@Param("ids")指定该集合的封
* 装为Map时候的key的名字为ids,在mapper文件使用对应的key的名字获取参数
* /
public List<Employee> getBatchEmployee(List<String> ids);

对应的SQL映射文件:

<!--批量查询员工-->
<select id="getBatchEmployee" resultType="employee">
    SELECT * FROM tbl_employee WHERE id IN
    <!--
    foreach几个参数:
        collection:需要遍历的集合的Key的名字;
        item:在遍历集合取出单独每个元素的别名;
        separator:每个元素取出后的之间的分隔符,如果手写“#{id},”的会出现最后一个元素后面多出
                    一个逗号,出现SQL语法错误;
        open:在foreach标签整体前面加上指定前缀,当然这里的“(”和“)”字符也可以放在foreach标签的前后部分,
                    有点丑罢了
        close:在foreach标签整体后面加上指定后缀;

        取出foreach集合中的遍历元素和MyBatis中取出参数的方式一样的,也是
        以#{参数名}的形式取出
    -->
    <foreach collection="collection" item="id" separator="," open="(" close=")">
        #{id}
    </foreach>
</select>

 第二种场景:批量插入。首先看下MySQL批量插入语法:

INSERT INTO tbl_employee (last_nameMM, gender, email) VALUES
("jack","0","jack@163.com"),("air","1","air@163.com")

可以看出来MySQL中批量插入可以通过,的分隔符一次插入多条记录,那么在mapper文件中实现就可简单了:

接口方法:

//批量插入
public void batchInsert(List<Employee> employees);

映射的SQL语句:

<!--批量插入-->
<insert id="batchInsert">
    INSERT INTO tbl_employee (last_nameMM, gender, email) VALUES
    <foreach collection="collection" item="e" separator=",">
        <!--注意这里获取对象的属性方式,使用“#{e.lastName}”的方式-->
        (#{e.lastName}, #{e.gender}, #{e.email})
    </foreach>
</insert>

注意涉及写操作的SQL需要在最后sqlSession.commit()否则不能提交到数据库。

 这是一种方式,有了foreach标签,其实我一开始想到的我直接发送多条SQL语句(因为我开始只知道单个记录的插入语法,并不知道还可以用上面的方式来批量插入),插入几条记录就发几条SQL,这是我作为一个头脑简单的禽兽想到的插入方式,这种方式也是我们所推荐的,比如:

INSERT INTO tbl_employee (last_nameMM, gender, email) VALUES (?,?,?);
INSERT INTO tbl_employee (last_nameMM, gender, email) VALUES (?,?,?);

是吧,我们来实现:

<!--批量插入-->
<insert id="batchInsert">
    <!--注意将分隔符改成分号-->
    <foreach collection="collection" item="e" separator=";">
        INSERT INTO tbl_employee (last_nameMM, gender, email) VALUES
        <!--注意这里获取对象的属性方式,使用“#{e.lastName}”的方式-->
        (#{e.lastName}, #{e.gender}, #{e.email})
    </foreach>
</insert>

但是遗憾的是在MySQL默认是不支持这种一次发送多条SQL的查询方式,我们需要修改这个属性为trueallowMultiQueries=true,我们在db.properties配置连接地址的时候带上这个参数即可:

jdbc.url=jdbc:mysql://localhost:3306/db_mybatis?allowMultiQueries=true

好了,做完这里的更改就可以大胆跑了。

 第三种批量删除和第四种批量更新,结合批量插入的第二种方式我们很容易就可以实现了,这里不做赘述。

6.MyBatis中的缓存机制

 同样的,缓存机制作为一个持久层框架几乎是必不可少的,它可以提高查询速度和系统运行速率,MyBatis也提供了强大的缓存功能,并且它默认定义两级缓存(一级缓存和二级缓存)。

6.1 一级缓存

 首先来看一级缓存,和Hibernate一样,MyBatis默认就是开启一级缓存的(并且我们无法手动关闭),即Session缓存,在MyBatis叫SqlSession,同一个SqlSession查询同一个数据库对象的话,第二次及以后的查询结果都是从MyBatis的一级缓存中获取结果,而不是再从数据库中查询一次,来看测试代码:

EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
Employee e1 = mapper.getEmployeeById(1);
System.out.println(e1);
Employee e2 = mapper.getEmployeeById(1)
System.out.println(e2);
//还记得==号是干嘛的吗?对喽,比较的是两者内存地址
System.out.println(e1==e2);

控制台输出:

DEBUG 03-27 08:48:35,462 ==>  Preparing: SELECT * FROM tbl_employee WHERE id = ?   (BaseJdbcLogger.java:159) 
DEBUG 03-27 08:48:35,524 ==> Parameters: 1(Integer)  (BaseJdbcLogger.java:159) 
DEBUG 03-27 08:48:35,559 <==      Total: 1  (BaseJdbcLogger.java:159) 
Employee{id=1, lastName='jacksonary', gender=0, email='jack@163.com'}
Employee{id=1, lastName='jacksonary', gender=0, email='jack@163.com'}
true

所以瞄一眼就大家就都懂了。很神奇对吧,和Hibernate中一样,我们来看一下什么样的情况会让一级缓存失效呢,主要有如下的四种情况:

  • 重新实例化SqlSession,因为一级缓存是SqlSession级别的,所以在重新实例化一个SqlSession去获取mapper对象操作数据库就会使上个Session缓存失效,怎么可能不失效,都不在一个频道上,就像小明家今天吃肉和小红有啥关系(●ˇ∀ˇ●),所以此时两者是不同的,好来看代码:
EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
Employee e1 = mapper.getEmployeeById(1);
System.out.println(e1);

sqlSession.close();
sqlSession = sqlSessionFactory.openSession();

EmployeeDao mapper2 = sqlSession.getMapper(EmployeeDao.class);
Employee e2 = mapper2.getEmployeeById(1);
System.out.println(e2);
System.out.println(e1==e2);
  • 一级缓存中不存在同一条记录的对象,直白点讲就是查询条件不一样,这说的跟白痴一样◑﹏◐,看代码:
EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
Employee e1 = mapper.getEmployeeById(1);
System.out.println(e1);
Employee e2 = mapper.getEmployeeById(2);
System.out.println(e2);
System.out.println(e1==e2);
  • 两次查询同一条记录中间夹杂了增、删、改、查的操作(这种情况比较多),并不要求一定是针对查询的这条记录的增、删、改、查,也可以是数据库其他记录的写操作,这时候两次查询的记录就不一样了,比如:
EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
Employee e1 = mapper.getEmployeeById(1);
System.out.println(e1);

mapper.deleteById(7);

Employee e2 = mapper.getEmployeeById(2);
System.out.println(e2);
System.out.println(e1==e2);
  • 手动清理了SqlSession的缓存:
EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
Employee e1 = mapper.getEmployeeById(1);
System.out.println(e1);

sqlSession.clearCache();

Employee e2 = mapper.getEmployeeById(2);
System.out.println(e2);
System.out.println(e1==e2);

6.2 二级缓存

 MyBatis中二级缓存又称为全局缓存,基于namespace级别的缓存,一个namespace对应于一个二级缓存。它的工作机制如下:
首先一个会话查询一个对象后,这个对象就会被放入这个SqlSession的一级缓存中,如果这个SqlSession关闭了(注意这里是关闭,不是重新实例化,两者有区别,只要SqlSession不关,里面的缓存对象就不会移到二级缓存中),MyBatis会将这个SqlSession中缓存的对象移到二级缓存中(如果这个对象配置了二级缓存),下次实例化新的SqlSession的时候,getMapper获取的是同一个映射SQL文件的mapper.xml,由于二级缓存是基于namespace的,所以再次获取同一个Mapper的时候,可以从二级缓存中拿到缓存对象。

下面我们来看一下二级缓存的配置方式:

  • 全局配置文件需要配置开启二级缓存(虽然默认开启,但是随着版本的不一样,默认配置可能会改变,所以尽量显示配置):
<settings>
    <!--显式开启二级缓存-->
    <setting name="cacheEnabled" value="true"/>
</settings>
  • 在需要使用二级缓存的mapper文件中配置使用二级缓存标签,在<mapper>标签中加一个<cache>标签即可,如:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace:名称空间,写关联接口的全类名-->
<mapper namespace="com.hhu.dao.EmployeeDao">
    <!--各个属性的含义:
        type:指定自定义的全类名,用默认的就好,当然也可以实现MyBatis的Cache接口自定义
        blocking:这个参数找了一下,居然没有,官方文档上居然也没有说明(*  ̄︿ ̄)
        eviction:缓存回收策略,缓存超了如何处理(默认使用LRU)
                LRU:移除空闲时间最长的缓存对象,即最少使用的对象
                FIFO:先进先出,移除最先进入缓存的对象
                SOFT:软引用,根据垃圾回收器的状态和软引用规则移除缓存对象
                WEAK:弱引用,根据垃圾回收器的状态和弱引用规则移除缓存对象
        flushInterval:缓存清空的时间间隔(单位毫秒),默认是不清空
        readOnly:是否只读,默认为fasle
                true:只读,MyBatis认为所有从缓存中获取数据的操作都是读操作,
                        而不会修改数据,为了加快用户的获取速度,MyBatis会将对象
                        在缓存中的引用直接交给用户,速度快,但不安全;
                false:非只读,MyBatis认为用户从缓存中获取对象后可能会修改这个
                        对象,所以它会用序列化和反序列化的技术克隆一份新的对象
                        给用户玩,随便你怎么搞,反正源数据不会变,速度慢,但是安全。
        size:缓存多少个元素
    -->
    <cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1024"></cache>
    
    <!--其他配置省略-->
</mapper>
  • 如果上述缓存获取的策略是非只读的,由于MyBatis需要序列化和反序列化对象,所以使用二级缓存的对象必须实现序列化接口,如:
public class Employee implements Serializable {
....
}

做完以上的工作即可测试:

EmployeeDao mapper = sqlSession.getMapper(EmployeeDao.class);
Employee e1 = mapper.getEmployeeById(1);
System.out.println(e1);

//关闭SqlSession,原来查询到的e1对象被移至二级缓存中
sqlSession.close();
sqlSession = sqlSessionFactory.openSession();
EmployeeDao mapper1 = sqlSession.getMapper(EmployeeDao.class);

Employee e2 = mapper1.getEmployeeById(1);
System.out.println(e2);

System.out.println(e1==e2);

瞄一眼控制台:

DEBUG 03-27 10:12:39,263 Cache Hit Ratio [com.hhu.dao.EmployeeDao]: 0.0  (LoggingCache.java:62) 
DEBUG 03-27 10:12:39,307 ==>  Preparing: SELECT * FROM tbl_employee WHERE id = ?   (BaseJdbcLogger.java:159) 
DEBUG 03-27 10:12:39,460 ==> Parameters: 1(Integer)  (BaseJdbcLogger.java:159) 
DEBUG 03-27 10:12:39,746 <==      Total: 1  (BaseJdbcLogger.java:159) 
Employee{id=1, lastName='jacksonary', gender=0, email='jack@163.com'}
DEBUG 03-27 10:12:39,881 Cache Hit Ratio [com.hhu.dao.EmployeeDao]: 0.5  (LoggingCache.java:62) 
Employee{id=1, lastName='jacksonary', gender=0, email='jack@163.com'}
false

确实是只发了一条SQL,有些同学可能会疑问,既然从缓存中拿的东西怎么最后输出了fasle,再缕一缕,第一次从数据库中查询到记录放进在SqlSession的一级缓存中(因为之前的缓存中没有这个对象,所以缓存命中率为0%),第二次将SqlSession关闭后,将这个对象移到了二级缓存中,获取的时候是从二级缓存中获取的(所以缓存命中率为50%),所以不一样。

6.3 和缓存相关的属性配置

 在看完上面的一级、二级缓存的配置之后,我们来看下MyBatis中有关缓存的配置:

  • 关于全局配置文件中<set>标签中的cacheEnabled属性,它是用来设置是否启用二级缓存,跟一级缓存是否启用没啥关系;
  • 手动清空缓存的方式SqlSession.clearCache()方法是清空的一级缓存,二级缓存不受其影响;
  • 在mapper映射SQL文件中,每个查询标签<select>中都有一个useCache的属性,默认为true,可以将它设置为false实现局部方法禁用二级缓存;
  • 每个增、删、改、查的标签中,即<insert><delete><update><select>标签中其实都有一个flushCache属性,表示该方法执行后是否清空缓存(这里是清空所有缓存,包含了一级缓存和二级缓存),其中<select>标签中的flushCache的值默认为false,执行完查询的操作后缓存不会被清空,而其他写操作的三个标签中的flushCache的值默认为true,表示执行完该标签的方法后会清空所有缓存,所以这就解释了为什么在两次查询之间夹杂了其他的写操作为什么缓存会失效的问题;
  • 全局配置文件中的<settings>标签有一个localCacheScope属性,可以配置本地缓存作用域,有两种取值SESSIONSTATEMENT,其中SESSION表示当前会话也就我们所说的一级缓存,STATEMENT则设置当前会话没有缓存了,也就没有所谓的缓存对象来共享了,默认是SESSION,这个配置可以禁用一级缓存,别瞎改,影响MyBatis的查询效率。

6.4 MyBatis的缓存机制

 这里结合下面的一幅图来说:
MyBatis的缓存机制
每次MyBatis通过SqlSession去数据库查询一个新对象的时候,会将这个对象缓存到一级缓存中(即图中的Session Cache),然后如果该SqlSession关闭后,MyBatis会将该Session中的对象移至二级缓存(即namespace Cache)中,每次一个新的SqlSession实例化后去查询对象的时候,首先将去namesapce二级缓存中查询,如果没有再到session cache中去查找,如果没有,才到数据库中查询。在MyBatis中,缓存就是一个Map的数据结构。

6.5 MyBatis整合第三方缓存工具

 在上面小节中,我们就说过MyBatis中的缓存比较简陋,就是一个Map,来看一下它的实现接口:

public interface Cache {
    String getId();

    void putObject(Object var1, Object var2);

    Object getObject(Object var1);

    Object removeObject(Object var1);

    void clear();

    int getSize();

    ReadWriteLock getReadWriteLock();
}

在前面说<cache>标签里面的type属性的时候就说过可以通过实现org.apache.ibatis.cache.Cache接口的方式自定义缓存方式,然后重写其中存取数据的方法putObjectremoveObject方法即可,现在比较流行的方式是用Redis、Ehcache以及MemCache用于数据的缓存,下面来看一下MyBatis整合Ehcache的流程,并且在MyBatis的github工程中为我们提供实现这个接口后的实体方法的重写,下拉至ehcache-cache,点击去可以查看它的源码,其中这里是MyBatis和Ehcache的整合官方文档

  • 导入适配器jar包mybatis-ehcache-1.0.3.jar以及Ehcache所需要的包ehcache-core-2.6.8.jar(注意最新的3.5版本包结构改了,使用的话会报找不到类的异常)、slf4j-api-1.7.25.jar和slf4j-log4j12-1.7.25.jar;
  • 在mapper映射文件中配置<cache>比前中的type属性,指定为这里的Ehcache在MyBatis适配器中的全类名即可:
<mapper namespace="org.acme.FooMapper">
  <!--这里使用Ecache做缓存,这里只需要配置一个type属性即可,
  其他属性放在Ecache配置文件中去配置-->
  <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>

  <!--注意这里还有一种标签的配置方法,如下,
    namespace:表示当前mapper引用com.hhu.dao.EmployeeDaol接口
            的缓存策略(即本mapper文件中的策略),在这里这么写现然
            是不对的,一般写在其他的mapper文件中


  <cache-ref namespace="com.hhu.dao.EmployeeDao"/>
  -->
  ...
</mapper>
  • 引入ecache的配置文件ecache.xml:
<ehcache>
    <!--配置磁盘的存储路径,默认是存储到缓存中,但是数据量比较多的时候会比较耗费内存,所以
        比较常规的做法是搞一个临界值,缓存量小于临界值可以直接放到内存中,超出临界值就将它
        放到硬盘上,这个路径就是配置的缓存到硬盘的某个路径下-->
    <diskStore path="F:\\testTemp"/>

    <!--默认的缓存策略,具体解释参照下面的自定缓存策略-->
    <defaultCache
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        overflowToDisk="true"
        />

    <!-- 设定具体的命名缓存的数据的过期策略,每个命名缓存代表一个缓存区域,

        缓存区域:是一个具具有名称的缓存块,可以给每个缓存块设置不同的缓存
        策略,如果没有设置任何缓存区域,则所有的缓存对象都将使用默认的缓存策略(就是上面的配置)

        Hibernate在不同的缓存区保存不同的类/集合
        对于类,区域的名称就是全类名
        对于集合,区域的名称就是集合所在类的全类名+集合属性名

        内部属性的各个具体含义:
        name              - 设置缓存区的名字,就是上面的缓存区域名
        maxInMemory       - 设置内存中最大可缓存的对象数量
        eternal           - 设置对象是否永久缓存,true表示永不过期
                            (timeToIdleSeconds和timeToLiveSeconds属性将失效),默认为false

        timeToIdleSeconds - 设置对象空闲的最长时间(单位为秒),缓存对象超出该时间会自动被清除,
                            默认为0,表示缓存对象可以无限时长存活在缓存中

        timeToLiveSeconds - 设置对象最长的生存周期(单位为秒),缓存对象超出该时间会自动被清除,
                            默认为0,表示缓存对象可以无限时长存活在缓存中,该值必须大于timeToIdleSeconds

        overflowToDisk    - 设置缓存对象在达到内存可允许存储的最大数量(即maxInMemory)后,
                            是否将溢出的缓存对象写入硬盘上
        -->
    <!--配置类的缓存区-->
    <cache name="com.hhu.entities.Employee"
        maxElementsInMemory="1"
        eternal="false"
        timeToIdleSeconds="300"
        timeToLiveSeconds="600"
        overflowToDisk="true"
        />

</ehcache>

好了配置完上面的东西后,跑案例即可发现在F:\testTemp文件夹创建并且有缓存文件。

7. MyBatis整合Spring

 MyBatis和Spring整合是实际开发中用的比较多,同样在MyBatis的github工程下面找到Spring-MyBatis整合工程,拉到页面下面点击See the docs查看文件教程,和上面整合Ehcache一样也需要有适配器才能完成整合,对于各自版本的要求如下:

MyBatis-Spring MyBatis Spring
1.0.0 and 1.0.1 3.0.1 to 3.0.5 3.0.0 or higher
1.0.2 3.0.6 3.0.0 or higher
1.1.0 or higher 3.1.0 or higher 3.0.0 or higher
1.3.0 or higher 3.4.0 or higher 3.0.0 or higher

根据上面版本要求,我这里用的是mybatis-3.4.6.jar,然后适配器用的是mybatis-spring-1.3.2.jar,c3p0为c3p0-0.9.1.2.jar,这里直接搞一个SSM的整合,将Spring MVC也搞进来,看一下整合的思路。

我必须强调一下,这一块非常重要!!!基本就是我以后搭建SSM项目全部步骤(基本都是修修改改就好了,值得珍藏)。之前一直用的maven,回顾Spring的时候,用的最low的本地包导入,发现上述的搭配好像不太行,一直报错(可能不是原配),异常如下:

java.lang.AbstractMethodError: com.mchange.v2.c3p0.impl.NewProxyPreparedStatement.isClosed()Z
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at net.bull.javamelody.JdbcWrapper$StatementInvocationHandler.invoke(JdbcWrapper.java:186)
at net.bull.javamelody.JdbcWrapper$DelegatingInvocationHandler.invoke(JdbcWrapper.java:251)
at $Proxy12.isClosed(Unknown Source)
at com.acteksoft.common.util.sql.ActekPreparedStatement.isClosed(ActekPreparedStatement.java:2437)
at com.acteksoft.common.util.DefaultDatabaseWorker$2.in(DefaultDatabaseWorker.java:153)
at com.acteksoft.common.util.jdbc.DataSourceConnectionProvider$1.doInConnection(DataSourceConnectionProvider.java:62)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:341)

想换个版本看看,最近Spring网页改了,下载jar包搞的特别麻烦,这里直接放出地址Spring各种版本集合,要啥就点啥,简单粗暴。我去,换一个还不行,此时的我已经有种预感,不会c3p0这货又抽抽了吧,又搞了下,发现这是c3p0 0.9.1.2抽抽了,在5+版本中已经修复了这个Bug,最终关于数据库连接池这一块选择如下版本的jar包:commons-logging-1.1.1.jarmchange-commons-java-0.2.11.jar,最终通过测试,所有的jar包如下:

c3p0-0.9.5.2.jar
com.springsource.net.sf.cglib-2.2.0.jar
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
commons-logging-1.1.1.jar
mchange-commons-java-0.2.11.jar
mybatis-3.4.6.jar
mybatis-spring-1.3.2.jar
mysql-connector-java-5.1.7-bin.jar
spring-aop-4.3.4.RELEASE.jar
spring-aspects-4.3.4.RELEASE.jar
spring-beans-4.3.4.RELEASE.jar
spring-context-4.3.4.RELEASE.jar
spring-core-4.3.4.RELEASE.jar
spring-expression-4.3.4.RELEASE.jar
spring-jdbc-4.3.4.RELEASE.jar
spring-orm-4.3.4.RELEASE.jar
spring-tx-4.3.4.RELEASE.jar
spring-web-4.3.4.RELEASE.jar
spring-webmvc-4.3.4.RELEASE.jar
taglibs-standard-impl-1.2.1.jar
taglibs-standard-spec-1.2.1.jar

【注意】上述jar包也是SSM框架整合都需要的jar包。

 下面我们来看一下完整的整合过程:

  • 数据库配置文件db.properties
# MySQL
jdbc.username=root
jdbc.password=921228jack
# allowMultiQueries=true表示开启一次指定多条SQL语句,用分号分割,默认是关闭的
jdbc.url=jdbc:mysql://localhost:3306/db_mybatis?allowMultiQueries=true
jdbc.driver=com.mysql.jdbc.Driver
  • MyBatis的全局配置文件mybatis-config.xml:
<!--只配置MyBatis不常用的属性-->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>


    <settings>
        <!--显式开启二级缓存-->
        <setting name="cacheEnabled" value="true"/>
        <!--开启下划线驼峰命名规则-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!--强制将null值装成每个数据库都能识别的NULL类型,MyBatis默认null转成OTHER类型
            但是有些数据库无法识别OTHER这中类型的,比如Oracle是无法识别的
        -->
        <setting name="jdbcTypeForNull" value="NULL"/>

        <!--开启延迟加载,注意是两个配置
            lazyLoadingEnabled:懒加载是否开启
            aggressiveLazyLoading:侵入懒加载,开启将不会进行懒加载,需要禁用
        -->
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="aggressiveLazyLoading" value="false"/>
    </settings>

    <typeAliases>
        <package name="com.hhu.entities"/>
    </typeAliases>

    <!--配置数据库厂商的别名-->
    <databaseIdProvider type="DB_VENDOR">
        <property name="MySQL" value="mysql"/>
        <property name="SQL Server" value="sql server"/>
    </databaseIdProvider>

</configuration>
  • Spring MVC的配置文件springmvc.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--组件扫描,这里只配置扫描控制器SpringMVC相关的注解,注意关闭默认的包扫描-->
    <context:component-scan base-package="com.hhu" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        <context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
    </context:component-scan>

    <!--配置视图解析器-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!--这里不要把斜杠弄丢了-->
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    
    <mvc:annotation-driven></mvc:annotation-driven>

    <!--解决静态资源无法访问的情况-->
    <mvc:default-servlet-handler/>

</beans>
  • Spring的配置文件applicationContext.xml,所有的整合工作都在这里做:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--组件扫描,这里只配置扫描控制器SpringMVC相关的注解,注意关闭默认的包扫描-->
    <context:component-scan base-package="com.hhu" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        <context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
    </context:component-scan>

    <!--配置视图解析器-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!--这里不要把斜杠弄丢了-->
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>


    <mvc:annotation-driven></mvc:annotation-driven>

    <!--解决静态资源无法访问的情况-->
    <mvc:default-servlet-handler/>

</beans>
  • WEB项目的配置文件web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!--配置Spring配置文件的位置-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>

    <!--配置Spring的IOC容器ServletContextListener-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>



    <!--配置Spring MVC-->
    <servlet>
        <servlet-name>springDispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--指定SpringMVC的配置文件,如果不指定,默认扫描/WEB-INF/springDispatcherServlet-servlet.xml
            必须将配置文件放在这个路径下面,并且命名为springDispatcherServlet-servlet.xml
        -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
    </servlet>

    <servlet-mapping>
        <servlet-name>springDispatcherServlet</servlet-name>
        <!--拦截所有请求-->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    
</web-app>

瞄一眼整体Module的结构:

这里写图片描述

好了,其实到这里,整个SSM的整合配置基本就已经结束了,剩余看你们自由开发就行了,这里把两个页面也贴出来:

<!--index.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>SSM</title>
</head>
<body>
<a href="getAllEmployee">查询所有员工</a>
</body>
</html>

<!--listAll.jsp-->
<%--
  Created by IntelliJ IDEA.
  User: WeiguoLiu
  Date: 2018/3/27
  Time: 21:10
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <title>员工列表</title>
</head>
<body>
<table>
    <tr>
        <th>lastName</th>
        <th>gender</th>
        <th>email</th>
        <c:forEach items="${allEmps}" var="emp">
            </tr>
                <td>${emp.lastName}</td>
                <c:if test="${emp.gender==0}">
                    <td>女</td>
                </c:if>
                <c:if test="${emp.gender==1}">
                    <td>男</td>
                </c:if>
                <td>${emp.email}</td>
            <tr>
        </c:forEach>
    </tr>
</table>
</body>
</html>

基本的控制器、Dao层的接口以及对应的mapper映射SQL文件、Service层怎么去写就不浪费时间了。

8. MyBatis-逆向工程

 MyBatis可以根据数据库的表生成对应的映射文件、接口、以及JavaBean,这个过程就是MyBatis的逆向工程MyBatis Generator,简称MBG(不是BGM…),他是一个为开发者提供的一个友好的代码生成器。但是表之间的连接、存储过程等复杂SQL需要我们手动编写。同样的,开头先上MBG文档

 直接看栗子,首先根据文档,我们先在原来的基础引入适配器的包:mybatis-generator-core-1.3.6.jar,在项目根目录(和src同级)下创建MBG的配置文件mbg.xml,如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <!--导入所需的jar,我们的jar都在Module的路径下,这一句可以省去
    <classPathEntry location="/Program Files/IBM/SQLLIB/java/db2java.zip" />
    -->

    <!--这里的targetRuntime是指定生成SQL映射文件的类型:
        MyBatis3:可以生成动态增删改查的映射文件;
        MyBatis3Simple:可以生成简单带有增删改查的Mapper的SQL标签
        其他值看文档
    -->
    <context id="DB2Tables" targetRuntime="MyBatis3Simple">
        <!--jdbcConnection指定如何连到数据库,就是配置数据库连接-->
        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/db_mybatis"
                        userId="root"
                        password="9j">
        </jdbcConnection>

        <!--Java类型解析器,一般使用默认-->
        <javaTypeResolver>
            <property name="forceBigDecimals" value="false" />
        </javaTypeResolver>

        <!--指定JavaBean的生成策略
            targetPackage:指定生成JavaBean的目标包名
            targetProject:指定目标工程
        -->
        <javaModelGenerator targetPackage="com.hhu.entities" targetProject=".\src">
            <property name="enableSubPackages" value="true" />
            <property name="trimStrings" value="true" />
        </javaModelGenerator>

        <!--SQL映射文件的生成策略,即mapper文件
            targetPackage:目标包
            targetProject:目标工程
        -->
        <sqlMapGenerator targetPackage="mapper"  targetProject=".\src">
            <property name="enableSubPackages" value="true" />
        </sqlMapGenerator>

        <!--指定接口生成策略,即Dao层-->
        <javaClientGenerator type="XMLMAPPER" targetPackage="com.hhu.dao"  targetProject=".\src">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>

        <!--指定逆向分析哪些表,创建JavaBean
            domainObjectName:指定对应表的JavaBean的类名
        -->
        <table tableName="tbl_employee" domainObjectName="Employee"></table>
        <table tableName="tbl_departments" domainObjectName="Department"></table>

    </context>
</generatorConfiguration>

注意将原来的mapper文件接口类全部删除,然后运行如下的代码:

    /**
     * 测试逆向工程是否可以创建接口和mapper
     * @throws Exception
     */
    @Test
    public void mbgTest() throws Exception {
        List<String> warnings = new ArrayList<String>();
        boolean overwrite = true;
        File configFile = new File("mbg.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);
    }

即可生成我们指定表的相关接口(包含基本的增删改查操作)和SQL映射文件。其他的MyBatis全局配置文件正常些即可,调用的时候直接调用即可。

 在正常开发中简单的增删改查可能无法满足我们的需求,所以MyBatis为我们提供了更为先进的方式,将上面的mbg.xml文件中的<context>标签中的targetRuntime属性指定为MyBatis3可以生成动态SQL,并且除了原来的接口类和SQL映射文件外,还会为每个JavaBean生成一个xxxExample类用于封装查询条件执行带条件的查询行为,来看一下用法:

   /**
     * 测试逆向工程生成的接口方法
     */
    @Test
    public void testSimpleMethod() {
        //注意将conf文件夹标记位为
        InputStream is = null;
        try {
            is = Resources.getResourceAsStream("mybatis-config.xml");
        } catch (IOException e) {
            e.printStackTrace();
        }
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //org.apache.ibatis.binding.BindingException: Invalid bound statement (not found):
        // com.hhu.dao.EmployeeMapper.selectAll
        EmployeeMapper eMapper = sqlSession.getMapper(EmployeeMapper.class);

        //使用带条件的查询,都是用生成的Example来封装查询条件
        EmployeeExample employeeExample = new EmployeeExample();
        //创建Criteria,用于拼装查询条件
        EmployeeExample.Criteria criteria = employeeExample.createCriteria();
        //比如按照姓名中有a的,且性别为男
        criteria.andLastNamemmLike("%a%").andGenderEqualTo(1);

        //上面封装的条件都是AND的关系,如果想中间夹杂OR的条件,可以创建新的查询条件Criteria
        //然后封装进入Example中,用or方法即可,那么criteria和criteria2之间就是或的关系,
        EmployeeExample.Criteria criteria2 = employeeExample.createCriteria();
        criteria2.andEmailIsNotNull();
        employeeExample.or(criteria2);


        //进行查询获取结果
        List<Employee> list = eMapper.selectByExample(employeeExample);
        System.out.println(list);
    }

注意下一条件之间ANDOR是怎么实现的。

提供一个实际工作中使用的逆向工程的配置文件:

文件名一般是generatorConfig.xml否则找不到配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <context id="docTables" targetRuntime="MyBatis3" defaultModelType="flat">

        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://xxx:3306/xxx"
                        userId="xx"
                        password="xx">
        </jdbcConnection>

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

        <!--使用IDEA这里一定要注意targetProject的路径,和Eclipse不一样-->
        <javaModelGenerator targetPackage="com.xxx.xxx.dao.model" targetProject="./src/main/java">
            <property name="enableSubPackages" value="true"/>
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>

        <!--使用IDEA这里一定要注意targetProject的路径,和Eclipse不一样-->
        <sqlMapGenerator targetPackage="com.xxx.xxx.dao.mapper" targetProject="./src/main/resources">
            <property name="enableSubPackages" value="true"/>
        </sqlMapGenerator>

        <!--使用IDEA这里一定要注意targetProject的路径,和Eclipse不一样-->
        <javaClientGenerator targetPackage="com.xxx.xxx.dao.mapper" targetProject="./src/main/java"
                             type="XMLMAPPER">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>

        <!--设置它为我们自动生成一些方法,可以根据需求选择,需要就设置为true,不需要就设置为false-->
        <table tableName="xxx_handle" domainObjectName="xxxHandle"
               enableInsert="true"
               enableSelectByPrimaryKey="true"
               enableUpdateByPrimaryKey="true"
               enableDeleteByPrimaryKey="true"
               enableSelectByExample="false"
               enableDeleteByExample="false"
               enableCountByExample="false"
               enableUpdateByExample="false"
               selectByPrimaryKeyQueryId="false"
               selectByExampleQueryId="false">
        </table>
    </context>
    <!-- 
    maven 运行逆向工程插件的命令: 
    mvn -Dmybatis.generator.overwrite=true mybatis-generator:generate
     -->
</generatorConfiguration>

【注意】

在实际开发中,可能存在对同一张表多次生成使用逆向插件生成,采用MyBatis逆向工程插件生成modelmapper以及interface,对于已有的表再次是使用逆向插件生成时,注意,modelinterface会重新生成,但不会覆盖已有的文件,同城会在指定的文件名帮你自动使用一个数字标记,比如在配置文件中指定的文件名为RfaParametric.java,如果指定目录下已经存在了,逆向工程插件会帮我们命名为RfaParametric.java.1
interface也是和model一样,以追加数字区分,但是mapper映射文件不会重新生成,只会在已有的mapper中追加新内容(如果有的话),已有的内容不会覆盖。

9. 插件开发

9.1插件的基本使用

 在MyBatis中提供了很多插件开发的机制,可以通过插件为目标对象创建代理对象,插件为四大对象创建出代理对象(代理对象可以拦截到四大对象的每个执行方法)。所以插件在一定程度上可以更改MyBatis的底层,来看一下具体的流程:

步骤:

  1. 编写插件类,实现org.apache.ibatis.plugin.Interceptor接口
  2. 使用@Intercepts注解完成插件签名的
  3. 将写好的插件在注册到全局配置文件中
package com.hhu.plugins;

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

import java.util.Properties;

/*
插件签名主要就是
使用注解的方式告诉MyBatis当前插件用来拦截哪个类的哪个方法
type:指定拦截的类
method:指定拦截的方法,这里是拦截往SQL语句中设值的方法
 */
@Intercepts({
        @Signature(type = StatementHandler.class, method = "parameterize", args = java.sql.Statement.class)
})
public class MyFirstPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //执行目标方法(有没有想到SpringAOP的执行过程呢,有点像)
        Object proceed = invocation.proceed();
        return proceed;
    }

    /*
    这个方法是用来包装目标对象的,为目标对象创建一个代理对象
     */
    @Override
    public Object plugin(Object o) {
        /*
         利用MyBatis的Plugin中静态方法封装我们的目标对象o为一个代理对象
        *当然这里也可以不使用MyBatis中的自带的类封装,可以自己手动封装出一个代理对象,功能一样
        * 不要忘了动态代理的创建过程,Proxy.newProxyInstance()
         */
        System.out.println("MyFirstPlugin...plugin:MyBatis将要包装的对象");
        Object wrap = Plugin.wrap(o, this);
        //返回目标对象的动态代理
        return wrap;
    }

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

【注意】插件中的三个方法的顺序依次为:setProperties()(获取配置信息)————>plugin()(创建代理对象)————>intercept()(拦截指定方法做完处理通过动态代理执行目标方法)。

然后在MyBatis的全局配置文件中注册我们自定义的插件即可:

    <!--注册自定义的插件,注意标签的顺序,不要将她塞到不该塞的位置-->
    <plugins>
        <plugin interceptor="com.hhu.plugins.MyFirstPlugin">
            <!--在定义的时候也可以传入一些参数,会调用插件中的setProperties方法设置-->
            <property name="userName" value="root"/>
            <property name="password" value="12345"/>
        </plugin>
    </plugins>

然后正常测试一个即可(带设置的SQL,比如带条件的查询)。

9.2多插件的执行

 在实际开发中,可能存在多个插,这里就涉及到它们的指定是顺序,开始直接给出结论:不是顺序执行也不是像之前的拦截器一样S形执行,比如在MyBtais的全局配置文件中注册了两个插件,在配置和为目标对象生成代理对象时,是按照全局配置文件中的配置顺序执行的,但是在通过代理对象执行目标方法时是按照逆序执行的,这个可以根据下图进行理解:
这里写图片描述
图中很清楚的标注插件的封装顺序和执行顺序。下面看一下具体代码:

/*插件部分直接将上面的插件类复制一份,稍微给一下输出语句以作区分,然后注册到
* MyBatis的全局配置文件中,即可
*/

在注册完有运行带有参数的查询即可实现上述的多插件的运行顺序。

9.3利用插件实现更改传入SQL的数值

 这种有点像AOP的意思,其他代码不变,主要在拦截到的时候利用动态代理执行目标方法前作处理(主要在intercept()方法中做处理),下面来看代码:

@Override
public Object intercept(Invocation invocation) throws Throwable {
    System.out.println("执行目标方法1..." + invocation.getMethod());
    //执行目标方法(有没有想到SpringAOP的执行过程呢,有点像,这里可以做一些非业务逻辑)

    //获取目标对象
    Object target = invocation.getTarget();
    System.out.println("获取目标对象:" + target);

    //获取目标对象的元数据
    MetaObject metaObject = SystemMetaObject.forObject(target);
    Object value = metaObject.getValue("parameterHandler.parameterObject");
    System.out.println("获取的源数据:" + value);

    //更改源数据,即MyBatis发送SQL传入的参数为该6
    metaObject.setValue("parameterHandler.parameterObject", 6);

    Object proceed = invocation.proceed();
    return proceed;
}

【注意】在上面的获取源数据metaObject时,如果是普通的参数是可以直接得到(比如是按id查询,那么这个就直接获取id),但是如果是带条件的查询(比如利用xxxExample查询),那么metaObject.getValue("parameterHandler.parameterObject")获取的是xxxExample对象而不是具体的类似于具体的’id’数值。

9.4 关于分页插件PageHelper

PageHelper是一个分页插件,比较好用,之前在PageHelper分页的实现就已经做了使用说明,下面记录一下完整的使用过程,官方使用说明文档

  1. 下载所需要的jar包,我这里使用的是pagehelper-5.1.2.jarjsqlparser-0.9.5.jar;
  2. 在MyBatis的全局配置文件中配置PageHelper的插件:
<plugins>
   <!--这个拦截器是PageHelper自带的,注意4.0版本前后的拦截器名字不一样!!-->
   <plugin interceptor="com.github.pagehelper.PageInterceptor">
       <!-- config params as the following -->
       <property name="param1" value="value1"/>
   </plugin>
</plugins>

3.使用,配置完上述的东西我们就可以使用了

@Test
public void testPage() throws IOException {
    //读取配置文件的流
    InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
    //利用配置文件创建Session工厂
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
    SqlSession sqlSession = sqlSessionFactory.openSession();

    EmployeeDao emapper = sqlSession.getMapper(EmployeeDao.class);

    //传入分页参数,第一个参数表示第几页(首页是1不是0),第二个参数表示每个显示几条记录,
    Page<Object> page = PageHelper.startPage(5, 1);
    List<Employee> allEmps = emapper.getAllEmps();

    System.out.println("当前页码:" + page.getPageNum());
    System.out.println("总记录数:" + page.getTotal());
    System.out.println("每页记录数:" + page.getPageSize());
    System.out.println("总页数:" + page.getPages());

    System.out.println("+++++++++++++++++++++");

    PageInfo<Employee> pageInfo = new PageInfo<>(allEmps);
    System.out.println("当前页码:" + pageInfo.getPageNum());
    System.out.println("总记录数:" + pageInfo.getTotal());
    System.out.println("每页记录数:" + pageInfo.getPageSize());
    System.out.println("总页数:" + pageInfo.getPages());
    System.out.println("是否首页:" + pageInfo.isIsFirstPage());
    System.out.println("是否末页:" + pageInfo.isIsLastPage());

    //在使用PageInfo封装List时,还可以指定连续显示多少页
    PageInfo<Employee> pageInfo2 = new PageInfo<>(allEmps, 2);
    int[] navigatepageNums = pageInfo2.getNavigatepageNums();
    for (int i = 0; i < navigatepageNums.length; i++) {
        System.out.println(navigatepageNums[i]);
    }
}

需要分页的时候,我们只需要在调用接口的查询方法前面加上一个Page<Object> page = PageHelper.startPage(5, 1);即可完成对紧跟在该语句后的查询方法进行分页(第一个参数是指查询第几页,第二个参数是每个显示多少条记录),然后输出查询方法得到的List结果就是第5页结果(只有一条记录);当然也可以将PageHelper.startPage(5, 1)的结果赋个一个Page<Object>对象,里面有很多详细信息,在上面测试了几个方法;最后,还有可以用PageInfo对象来封装查询出来的List结果,这个东西据说很重要o( ̄▽ ̄)o,可以用new PageInfo<>(allEmps)这样的形式封装,用法比第一种稍微丰富一点,当然除此之外还可以使用new PageInfo<>(allEmps, 2)这样的形式指定连续显示多少页,这里就连续显示2页,

   for (int i = 0; i < navigatepageNums.length; i++) {
        System.out.println(navigatepageNums[i]);
    }

这个代码就是获取连续显示的页码,这里需要注意一下连续的含义,比如上面的小栗子是获取的第5页的记录,然后它需要连续显示2页,那么就是显示的第4、5两页,而不是第5、6两页,就是以当前页为参照点,如果连续显示1页,那么就是显示第5页,如果连续显示2页,那么就是显示第4、5页,如果连续显示3页,那么就是显示第4、5、6页(先插左再插右),我是这么理解的,看图或许更好理解:
这里写图片描述

9.5关于批量操作

 在之前通过其他的一些方法实现不断拼接SQL语句达到批量操作的目的,一般情况下服务器都有限制SQL的长度,所以前面的方式只能做简单的一些批量操作,而不能做太复杂的批量操作,毕竟她不是专业的,MyBatis中提供批量操作,下面分作两部分来看一下:

 第一部分是MyBatis单独使用批量操作,不作其他框架的整合,非常简单只需要在获取SqlSession的时候指定获取批量操作的SqlSession即可,看一下代码:

@Test
public void batchTest() throws IOException {
    //读取配置文件的流
    InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
    //利用配置文件创建Session工厂
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
    //指定获取批量操作的SqlSession
    SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);

    EmployeeDao emapper = sqlSession.getMapper(EmployeeDao.class);
    for (int i = 0; i < 100; i++) {
        emapper.addEmp(new Employee(UUID.randomUUID().toString().substring(1,5),"1", "ptg@13.com"));
    }

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

代码和普通添加员工的代码是一样的,唯一不同的是sqlSessionFactory.openSession(ExecutorType.BATCH),指定批量SqlSession,在执行的时候,100次的时候控制台是设置了100次的参数,很快(SQL预编译1次 + 参数设置100次 + 数据库1次执行SQL);如果使用sqlSessionFactory.openSession()获取SqlSession,那么同样的插入操作,很费时间(SQL预编译100次 + 参数设置100次 + 数据库执行100次SQL)。

 其次我们来看一下在MyBatis在和Spring作整合工作时,SqlSession由Spring负责生成,那么我们需要在配置了SqlSessionFactory后指定SqlSession的生成方式,要上面的构造方法,看一下配置:

<!--指定批量操作的SqlSession的构造方法
SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType)-->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
    <constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory"/>
    <constructor-arg name="executorType" value="BATCH"/>
</bean>

通过以上的配置后,我们在SSM整合时就可以直接在Service层注入上述的SqlSession使用即可:

@Autowired
private SqlSession sqlSession;

//然后在某个逻辑方法中使用上述的注入的SqlSession获取mapper接口操作即可

9.6 MyBatis的调用存储过程

 在实际开发中会有一些很复杂的逻辑,存储过程为了避免反复去写这样的逻辑而诞生,下次有相同的逻辑调用存储过程即可。

9.7 MyBatis对自定义TypeHandler处理枚举类型

 TypeHandler在java和数据库交互的过程中,主要负责数据库类型和javaBean类型的映射。

展开阅读全文

没有更多推荐了,返回首页