MyBatis学习笔记

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

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

MyBatis学习笔记_第1张图片

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

MyBatis学习笔记_第2张图片

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

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

MyBatis学习笔记_第3张图片

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学习笔记_第4张图片

  • 创建MyBatis的核心配置文件(全局配置文件):mybatis-config.xml


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

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

configuration>

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




<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的全局配置文件需要写的东西基本都涵盖在了标签中,下面着重对这个标签中的各个子标签做说明。

 这里重要的地方再说一遍,被坑过啊,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>
        
        <setting name="mapUnderscoreToCamelCase" value="true"/>
settings>

3.3 typeAliases标签

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

<typeAliases>
    
    <typeAlias type="com.hhu.entitieses.Employee" alias="employee"/>
typeAliases>

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

<typeAliases>
    
    <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">
    
    <environment id="dev_mysql">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            
            <property name="driver" value="${jdbc.driver}"/>
            .
            .
            .
        dataSource>
    environment>

    
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和数据库底层的解耦,一般的配置如下:


<databaseIdProvider type="DB_VENDOR">
    
    <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">

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

        
    <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指定数据库厂商的别名:
    
    <select id="getEmployeeById" resultType="employee" databaseId="mysql">
        select * from tbl_employee where id = #{id}
    select>

    
    <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标签,并不符合程序员偷懒的好习惯,改造一下如下:


<select id="getEmployeeById" resultType="employee">
    
    <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>


<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文件的一些写法,该文件中的主体是标签,里面还有其他的一些常用的标签:

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

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




<mapper namespace="com.hhu.dao.EmployeeDao">
    
    <select id="getEmployeeById" resultType="com.hhu.entities.Employee">
        
        select id, last_namemm lastName, gender,email from tbl_employee where id = #{id}
    select>

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

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

mapper>

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


<sql id="insertSql">
    last_NameMm, gender, email
sql>

<insert id="insertTest">
    INSERT INTO tbl_employee(
    
    <include refid="insertSql">include>
    ) VALUES (#{lastName}, #{gender}, #{email})
insert>

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


<sql id="insertSql">
    
    last_NameMm, gender, email, ${testProperty}
sql>

<insert id="insertTest">
    INSERT INTO tbl_employee(
    
    <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("[email protected]");
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">
        
        <selectKey keyProperty="id" order="BEFORE">
            SELECT EMPLOYEES_SEQ.nextval FROM dual
        selectKey>
        
        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">
    
    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中:


<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值转换的处理:

<insert id="saveEmp" parameterType="employee" databaseId="oracle">
    INSERT  INTO employees(last_name,gender,email) VALUES
        (#{lastName}, #{gender}, #{email, jdbcType=NULL})
insert>
  • 当然,还可以直接在MyBatis的全局配置文件中的中进行设置,这是针对所有的null设置,而不是像第一种方法对某个字段进行null的处理:
<settings>
    <setting name="jdbcTypeForNull" value="NULL"/>
settings>

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

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

 在接口的查询的方法中会有返回值,也有可能没有返回值,返回值可以是一个基本数据对象,也可以是一个对象(pojo对象直接指定为设定的别名),下面对接口中不同方法的不同返回值的类型的处理做一个探讨;在mapper映射文件中存在返回值的只有标签中的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文件中为:


<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的形式,以Employee的主键id作为Map的Key,如下:

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

mapper文件:


<select id="getEmps" resultType="employee">
    SELECT  * FROM tbl_employee
select>

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

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

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

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

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

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


<resultMap id="MyEmp" type="employee">
    
    <id column="id" property="id"/>
    
    <result column="last_nameMm" property="lastName"/>
    <result column="gender" property="gender"/>
    <result column="email" property="email"/>
resultMap>


<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
MyBatis学习笔记_第5张图片
这里主要是针对在查询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"/>
    
    <result column="d_id" property="dep.dId"/>
    <result column="dp_Name" property="dep.dpName"/>
resultMap>



<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 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文件如下(接口自己脑补下):




<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="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"/>

    
    <collection property="employees" ofType="employee">
        
        <id column="id" property="id"/>
        <result column="last_nameMm" property="lastName"/>
        <result column="gender" property="gender"/>
        <result column="email" property="email"/>
    collection>
resultMap>



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


<select id="getAllEmployeeByDep" resultType="employee">
    select id, last_nameMm lastName, gender,email from tbl_employee WHERE department=#{id}
select>

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


<resultMap id="DepWithEmpsStep" type="department">
    <id column="d_id" property="dId"/>
    <result column="dp_Name" property="dpName"/>
    
    <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>

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

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


<resultMap id="EmpGen" type="employee">
    <id column="id" property="id"/>
    <result column="gender" property="gender"/>

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

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

5.4 set标签

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

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

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


<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,如下:


<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 getBatchEmployee(List ids);

对应的SQL映射文件:


<select id="getBatchEmployee" resultType="employee">
    SELECT * FROM tbl_employee WHERE id IN
    
    <foreach collection="collection" item="id" separator="," open="(" close=")">
        #{id}
    foreach>
select>

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

INSERT INTO tbl_employee (last_nameMM, gender, email) VALUES
("jack","0","[email protected]"),("air","1","[email protected]")

可以看出来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.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.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='[email protected]'}
Employee{id=1, lastName='jacksonary', gender=0, email='[email protected]'}
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 namespace="com.hhu.dao.EmployeeDao">
    
    <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='[email protected]'}
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='[email protected]'}
false

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

6.3 和缓存相关的属性配置

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

  • 关于全局配置文件中标签中的cacheEnabled属性,它是用来设置是否启用二级缓存,跟一级缓存是否启用没啥关系;
  • 手动清空缓存的方式SqlSession.clearCache()方法是清空的一级缓存,二级缓存不受其影响;
  • 在mapper映射SQL文件中,每个查询标签标签中其实都有一个flushCache属性,表示该方法执行后是否清空缓存(这里是清空所有缓存,包含了一级缓存和二级缓存),其中