手把手教你Mybatis(2)

1,587 阅读5分钟

动态SQL

动态SQL是MyBatis强大特性之一。极大的简化我们拼装SQL的操作。动态SQL元素和使用JSTL或其他类似基于XML的文本处理器相似。 MyBatis采用功能强大的基于OGNL表达式来简化操作。

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

OGNL表达式

OGNL( Object Graph Navigation Language )对象图导航语言,这是一种强大的表达式语言,通过它可以非常方便的来操作对象属性。类似于我们的EL、SpEL等。

  • 访问对象属性
    • person.name
  • 调用方法
    • person.getName()
  • 调用静态属性/方法
    • @java.lang.Math@PI 静态属性
    • @java.util.UUID@randomUUID() 静态方法
  • 调用构造方法
    • new com.atguigu.bean.Person(‘admin’).name
  • 运算符
    • + - * / %
  • 逻辑运算符
    • in、not in、>、>=、<、<=、==、!=
    • 注意:xml中特殊符号如”、>、<等这些都需要使用转义字符

访问集合伪属性

类型伪属性伪属性对应的Java方法
List、Set、Mapsize、isEmptyList/Set/Map.size()、List/Set/Map.isEmpty()
List、SetiteratorList.iterator()、Set.iterator()
Mapkeys、valuesMap.keySet()、Map.values()
Iteratornext、hasNextIterator.next()Iterator.hasNext()

if 判断

使用动态SQL最常见情景是根据条件包含where子句的一部分

    <!--按条件查询
    public List<Employee> getEmpsByConditionIf(Employee employee);-->
    <select id="getEmpsByConditionIf" resultType="employee">
        select * from tbl_employee where
        <if test="id!=null">
            id=#{id} and
        </if>
        <if test="lastName!=null &amp;&amp; lastName!=&quot;&quot;">
            last_name like #{lastName} and
        </if>
        <if test="email!=null and email.trim()!=&quot;&quot;">
            email=#{email} and
        </if>
        <if test="gender==0 or gender==1">
            gender=#{gender}
        </if>
        1=1
    </select>
<select id="findActiveBlogWithTitleLike" resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <if test="title != null">
     AND title like #{title}
  </if>
</select>

where 查询条件

上面if里面如果某些条件没带,那么SQL拼装可能会出问题。把所有要拼装的动态条件全部放入where标签中(and写前面) 即可,用来去掉第一个多出来的and或者or

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

    <select id="getEmpsByConditionIf" resultType="employee">
        select * from tbl_employee
        <where>
            <if test="id!=null">
                and id=#{id}
            </if>
            <if test="lastName!=null &amp;&amp; lastName!=&quot;&quot;">
                and last_name like #{lastName}
            </if>
            <if test="email!=null and email.trim()!=&quot;&quot;">
                and email=#{email}
            </if>
            <if test="gender==0 or gender==1">
                and gender=#{gender}
            </if>
        </where>
    </select>

trim 自定义截取

  • prefix前缀。整个trim标签拼串后的字符串,加一个前缀。
  • prefixOverrides:前缀覆盖。trim标签拼串后的字符串多余的可以去掉,如and
  • suffix:后缀。整个trim标签拼串后的字符串,加一个后缀。
  • suffixOverrides后缀覆盖。trim标签拼串后的字符串多余的可以去掉,如and
    <!--public List<Employee> getEmpsByConditionTrim(Employee employee);-->
    <select id="getEmpsByConditionTrim" resultType="employee">
        select * from tbl_employee
        <trim prefix="where" suffixOverrides="and">
            <if test="id!=null">
                id=#{id} and
            </if>
            <if test="lastName!=null and lastName!=&quot;&quot;">
                last_name like #{lastName} and
            </if>
            <if test="email!=null and email.trim()!=&quot;&quot;">
                email=#{email} and
            </if>
            <if test="gender==0 or gender==1">
                gender=#{gender}
            </if>
        </trim>
    </select>

choose 分支选择

choose、when、otherwise: 想从多个条件中选择一个使用时

    <!--public List<Employee> getEmpsByConditionChoose(Employee employee);-->
    <select id="getEmpsByConditionChoose" resultType="employee">
        select * from tbl_employee
        <where>
            /*如果带了ID,则直接用ID查。如果没有带ID,再用lastName查*/
            <choose>
                <when test="id!=null">
                    id=#{id}
                </when>
                <when test="lastName!=null and lastName!=&quot;&quot;">
                    last_name=#{lastName}
                </when>
                <otherwise>
                    1=1 /*不满足上面俩条件时执行*/
                </otherwise>
            </choose>
        </where>
    </select>

set 修改条件

set元素可以用于动态包含需要更新的列,忽略其它不更新的列

    <!--public void updateEmp(Employee employee);-->
    <update id="updateEmp" parameterType="employee">
        update tbl_employee
        <set>
            <if test="lastName!=null">
                last_name=#{lastName},
            </if>
            <if test="gender!=null and (gender==0 or gender==1)">
                gender=#{gender},
            </if>
            <if test="email!=null">
                email=#{email}
            </if>
        </set>
        where id=#{id}
    </update>

也可以使用trim标签来实现更新操作

foreach

动态SQL的另外一个常用的必要操作是需要对一个集合进行遍历,通常是在构建IN条件语句的时候

    <!--public List<Employee> getEmpsByConditionForeach(List<Integer> ids);-->
    <select id="getEmpsByConditionForeach" resultType="employee">
        /*原语句:select * from tbl_employee where id in(1,2,3)*/
        select * from tbl_employee where id in
        <foreach collection="ids" item="item_id" open="(" separator="," close=")">
            #{item_id}
        </foreach>
    </select>
  • 元素
    • collection:指定要遍历的集合
    • list类型的参数会特殊处理封装在map中,map的key就叫list
    • item:将当前遍历出的元素赋值给指定的变量
    • separator:每个元素之间的分隔符
    • open:遍历出所有结果拼接一个开始的字符
    • close:遍历出所有结果拼接一个结束的字符
  • 当迭代列表、集合等可迭代对象或者数组时:
    • index是当前迭代的次数,item的值是本次迭代获取的元素
  • 当使用字典(或者Map.Entry对象的集合)时
    • index是键,item是值

foreach批量插入

方式一:(推荐)

    <!--批量保存:
        public void addEmps(@Param("emps") List<Employee> emps);-->
    <insert id="addEmps">
        /*insert into tbl_employee(last_name, email, gender, d_id)
        values (lastName, email, gender, dept.id),(lastName, email, gender, dept.id);*/
        
        insert into tbl_employee(last_name, email, gender, d_id) values
        <foreach collection="emps" item="emp" separator=",">
            (#{emp.lastName}, #{emp.email}, #{emp.gender}, #{emp.dept.id})
        </foreach>
    </insert>

方式二:

    <insert id="addEmps">
        <foreach collection="emps" item="emp" separator=";">
            insert into tbl_employee(last_name, email, gender, d_id)
            values(#{emp.lastName}, #{emp.email}, #{emp.gender}, #{emp.dept.id})
        </foreach>
    </insert>

备注:这种方式要加上允许多条语句执行:
jdbc.url=jdbc:mysql://localhost:3306/mybatis_demo?allowMultiQueries=true

内置参数

  • _parameter:代表整个参数
    • 如果是单个参数,那么_parameter就是传递过来的参数
    • 如果是多个参数,参数被封装为一个map,那么_parameter就是代表这个map
  • _databaseId:数据库标识
    • 若在mybatis配置文件中配置了databaseIdProvider , 则可以使用 “_databaseId”变量,这样就可以根据不同的数据库 厂商构建特定的语句
    <!--public List<Employee> getEmps4InnerParameter(Employee employee);-->
    <select id="getEmps4InnerParameter" resultType="employee">
        <if test="_databaseId=='mysql'">
            select * from tbl_employee
            <if test="_parameter!=null">
                where last_name like "%add%"
            </if>
        </if>
        <if test="_databaseId=='oracle'">
            select * from employees
        </if>
    </select>

bind 绑定

bind元素可以从OGNL表达式中创建一个变量并将其绑定到上下文

    <!--public List<Employee> getEmps4InnerParameter(Employee employee);-->
    <select id="getEmps4InnerParameter" resultType="employee">
        <bind name="myLastName" value="'%'+lastName+'%'"/>
        <if test="_databaseId=='mysql'">
            select * from tbl_employee
            <if test="_parameter!=null">
                /*where last_name like "%add%"*/
                where last_name like #{myLastName}
            </if>
        </if>
        <if test="_databaseId=='oracle'">
            select * from employees
        </if>
    </select>
<select id="selectBlogsLike" resultType="Blog">
  <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
  SELECT * FROM BLOG
  WHERE title LIKE #{pattern}
</select>

sql可重用

这个元素可以用来定义可重用的SQL代码片段,以便在其它语句中使用

<sql>标签里面也可以写<if test="XXX">

    <sql id="table_name">
        tbl_employee
    </sql>
    <sql id="Emp_Column_List">
        last_name, email, gender, d_id
    </sql>
    <!--public List<Employee> getEmps();-->
    <select id="getEmps" resultType="employee">
        select <include refid="Emp_Column_List" /> from <include refid="table_name" />
    </select>

参数可以静态地(在加载的时候)确定下来,并且可以在不同的 include 元素中定义不同的参数值。比如:

<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

这个 SQL 片段可以在其它语句中使用,例如:
<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"><property name="alias" value="t1"/></include>,
    <include refid="userColumns"><property name="alias" value="t2"/></include>
  from some_table t1
    cross join some_table t2
</select>

缓存机制

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

  • 默认情况下,只有一级缓存(SqlSession级别的缓存, 也称为本地缓存)开启。
  • 二级缓存需要手动开启和配置,他是基于namespace级别的缓存。
  • 为了提高扩展性。MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存。

一级缓存(本地缓存)

一级缓存(local cache),即本地缓存,作用域默认为sqlSession

与数据库同一次会话期间查询到的数据会放在本地缓存中,后面如果需要获取相同的数据,直接从缓存拿而不再去查询数据库。当 Session flush或close 后, 该Session中的所有Cache将被清空。 本地缓存不能被关闭, 但可以调用 clearCache() 来清空本地缓存,或者改变缓存的作用域。

连续查询两次:
Employee emp1 = employeeMapper.getEmpById(2);
System.out.println(emp1);
Employee emp2 = employeeMapper.getEmpById(2);
System.out.println(emp2);

结果(可以看到只查询了一次)
DEBUG 10-20 09:58:40,284 ==>  Preparing: select * from tbl_employee where id=?  (BaseJdbcLogger.java:137) 
DEBUG 10-20 09:58:40,369 ==> Parameters: 2(Integer)  (BaseJdbcLogger.java:137) 
DEBUG 10-20 09:58:40,418 <==      Total: 1  (BaseJdbcLogger.java:137) 
Employee [id=2, lastName=Mikel, email=Mikel@qq.com, gender=1]
Employee [id=2, lastName=Mikel, email=Mikel@qq.com, gender=1]

一级缓存默认开启,无法关闭。 在mybatis3.1之后,可以配置本地缓存的作用域。在mybatis.xml中配置localCacheScope( SESSION | STATEMENT )

MyBatis利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询。默认值为SESSION,这种情况下会缓存一个会话中执行的所有查询。若设置值为STATEMENT,本地会话仅用在语句执行上,对相同SqlSession的不同调用将不会共享数据。

同一次会话期间只要查询过的数据都会保存在当前SqlSession的一个Map中:key=hashCode+查询的SqlId+编写的sql查询语句+参数。

一级缓存失效的四种情况:

  1. 不同的SqlSession对应不同的一级缓存

  2. 同一个SqlSession但是查询条件不同

  3. 同一个SqlSession两次查询期间执行了任何一次增删改操作

  4. 同一个SqlSession两次查询期间手动清空了缓存(clearCache方法)

二级缓存(全局缓存)

二级缓存(secondlevelcache),全局作用域缓存基于namespace级别的

二级缓存默认不开启,需要手动配置。MyBatis提供二级缓存的接口以及实现,缓存实现要求POJO实现Serializable接口。二级缓存在SqlSession关闭或提交之后才会生效(因为查出的数据都会先被放在一级缓存中)。

使用步骤

  1. 全局配置文件中开启二级缓存
    • <setting name="cacheEnabled" value="true"/>
  2. 需要使用二级缓存的映射文件处使用cache配置缓存
    • <cache />
  3. 注意:POJO需要实现Serializable接口
mybatis-config.xml中配置:
<settings>
	<setting name="cacheEnabled" value="true"/> <!--开启二级缓存-->
</settings>

    
在EmployeeMapper.xml中配置:
    <cache eviction="FIFO"
           flushInterval="60000"
           readOnly="true"
           size="100">
    </cache>
    @Test
    public void testSecondLevelCache() throws IOException {
        // 1、获取SqlSessionFactory
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        // 2、获取SqlSession对象
        SqlSession openSession1 = sqlSessionFactory.openSession(true);
        SqlSession openSession2 = sqlSessionFactory.openSession(true);

        EmployeeMapper employeeMapper1 = openSession1.getMapper(EmployeeMapper.class);
        EmployeeMapper employeeMapper2 = openSession2.getMapper(EmployeeMapper.class);

        Employee emp1 = employeeMapper1.getEmpById(2);
        System.out.println(emp1);
        openSession1.close();

        Employee emp2 = employeeMapper2.getEmpById(2);
        System.out.println(emp2);
        openSession2.close();
    }

输出如下:
DEBUG 10-20 15:07:07,449 Cache Hit Ratio [com.code.mybatis.dao.EmployeeMapper]: 0.0  (LoggingCache.java:60) 
DEBUG 10-20 15:07:07,458 ==>  Preparing: select * from tbl_employee where id=?  (BaseJdbcLogger.java:137) 
DEBUG 10-20 15:07:07,523 ==> Parameters: 2(Integer)  (BaseJdbcLogger.java:137) 
DEBUG 10-20 15:07:07,559 <==      Total: 1  (BaseJdbcLogger.java:137) 
Employee [id=2, lastName=Mikel, email=Mikel@qq.com, gender=1]
DEBUG 10-20 15:07:07,560 Cache Hit Ratio [com.code.mybatis.dao.EmployeeMapper]: 0.5  (LoggingCache.java:60) 
Employee [id=2, lastName=Mikel, email=Mikel@qq.com, gender=1]

<cache>相关属性

  • eviction="FIFO":缓存回收策略

    • LRU – 最近最少使用的:移除最长时间不被使用的对象。
    • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
    • SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
    • WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
    • 默认的是 LRU。
  • flushInterval:刷新间隔(多长时间清空一次),单位毫秒

    • 默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新
  • size:引用数目,正整数

    • 代表缓存最多可以存储多少个对象,太大容易导致内存溢出
  • readOnly:只读,true/false

    • true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。
    • false:读写缓存;会返回缓存对象的拷贝(通过序列化)。这会慢一些, 但是安全,因此默认是 false
  • type:指定自定义缓存的全类名

缓存有关的设置

  1. 全局setting的cacheEnable
    1. 配置二级缓存的开关cacheEnable=true
    2. 设置cacheEnable=false,则关闭的是二级缓存,一级缓存一直是打开的
  2. select标签的useCache属性
    1. 配置这个select是否使用二级缓存。一级缓存一直是使用的
  3. sql标签的flushCache属性
    1. 增删改默认flushCache=true。sql执行以后,会同时清空一级和二级缓存(后面发新的SQL进行执行)查询默认flushCache=false
  4. sqlSession.clearCache()
    1. 只是用来清除一级缓存
  5. 当在某一个作用域 (一级缓存Session/二级缓存 Namespaces) 进行了C/U/D操作后,默认该作用域下所有select中的缓存将被clear
    <select id="getEmpById" resultType="employee" useCache="true" flushCache="false">
        select * from <include refid="table_name" /> where id=#{id}
    </select>

缓存原理

第三方缓存EhCache

EhCache是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider。MyBatis定义了Cache接口方便我们进行自定义扩展。

步骤:

  1. 导入ehcache包,以及整合包,日志包 ehcache-core-2.6.8.jar、mybatis-ehcache-1.0.3.jar、slf4j-api-1.6.1.jar、slf4j-log4j12-1.6.2.jar
  2. 编写ehcache.xml配置文件
  3. 配置cache标签

参照缓存:若想在命名空间中共享相同的缓存配置和实例。 可以使用 cache-ref 元素来引用另外一个缓存。

整合Spring

下载适配包

1、查看不同MyBatis版本整合Spring时使用的适配包:

www.mybatis.org/spring/

2、下载整合适配包

github.com/mybatis/spr…

3、官方整合示例jpetstore

github.com/mybatis/jpe…

整合配置

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:mybatis-spring="http://mybatis.org/schema/mybatis-spring"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring-1.2.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

	<!-- Spring希望管理所有的业务逻辑组件,等。。。 -->
	<context:component-scan base-package="com.code.mybatis">
		<context:exclude-filter type="annotation"
			expression="org.springframework.stereotype.Controller" />
	</context:component-scan>

	<!-- 引入数据库的配置文件 -->
	<context:property-placeholder location="classpath:dbconfig.properties" />
	<!-- Spring用来控制业务逻辑。数据源、事务控制、aop -->
	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
		<property name="jdbcUrl" value="${jdbc.url}"></property>
		<property name="driverClass" value="${jdbc.driver}"></property>
		<property name="user" value="${jdbc.username}"></property>
		<property name="password" value="${jdbc.password}"></property>
	</bean>
	<!-- spring事务管理 -->
	<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"></property>
	</bean>

	<!-- 开启基于注解的事务 -->
	<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
	
	<!-- 
	整合mybatis 
		目的:1、spring管理所有组件。mapper的实现类。
				service==>Dao   @Autowired:自动注入mapper;
			2、spring用来管理事务,spring声明式事务
	-->
	<!--创建出SqlSessionFactory对象  -->
	<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource"></property>
		<!-- configLocation指定全局配置文件的位置 -->
		<property name="configLocation" value="classpath:mybatis-config.xml"></property>
		<!--mapperLocations: 指定mapper文件的位置-->
		<property name="mapperLocations" value="classpath:com/code/mybatis/dao/*.xml"></property>
	</bean>
	
	<!--配置一个可以进行批量执行的sqlSession  -->
	<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
		<constructor-arg name="sqlSessionFactory" ref="sqlSessionFactoryBean"></constructor-arg>
		<constructor-arg name="executorType" value="BATCH"></constructor-arg>
	</bean>
	
	<!-- 扫描所有的mapper接口的实现,让这些mapper能够自动注入;
	base-package:指定mapper接口的包名
	 -->
	<mybatis-spring:scan base-package="com.code.mybatis.dao"/>
	<!-- <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="basePackage" value="com.atguigu.mybatis.dao"></property>
	</bean> -->
	
</beans>

逆向工程

MBG简介

MyBatisGenerator:简称MBG,是一个专门为MyBatis框架使用者定制的代码生成器,可以快速的根据表生成对应的映射文件、接口、以及bean类。支持基本的增删改查,以及QBC风格的条件查询。但是表连接、 存储过程等这些复杂sql的定义需要我们手工编写。

官方文档地址 www.mybatis.org/generator/

官方工程地址 github.com/mybatis/gen…

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>
    <!--
        targetRuntime
            MyBatis3Simple:生成简单版的CRUD(基本的增删改查)
            MyBatis3:豪华版CRUD(带条件的增删改查)
     -->
    <context id="DB2Tables" targetRuntime="MyBatis3">
        <!-- jdbcConnection:指定如何连接到目标数据库,配置数据库连接信息 -->
        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/mybatis_demo?allowMultiQueries=true"
                        userId="root"
                        password="root1234">
        </jdbcConnection>

        <!-- Java类型解析器 -->
        <javaTypeResolver >
            <property name="forceBigDecimals" value="false" />
        </javaTypeResolver>

        <!--
        javaModelGenerator:配置javaBean的生成策略
            targetPackage="test.model":目标包名
            targetProject="\MBGTestProject\src":目标工程(生成在哪个工程下)
        -->
        <javaModelGenerator targetPackage="com.code.mybatis.bean"
                            targetProject="src">
            <property name="enableSubPackages" value="true" />
            <property name="trimStrings" value="true" />
        </javaModelGenerator>

        <!-- sqlMapGenerator:配置sql映射文件生成策略 -->
        <sqlMapGenerator targetPackage="com.code.mybatis.dao"
                         targetProject="src">
            <property name="enableSubPackages" value="true" />
        </sqlMapGenerator>

        <!-- javaClientGenerator:配置Mapper接口的生成策略 -->
        <javaClientGenerator type="XMLMAPPER" targetPackage="com.code.mybatis.dao"
                             targetProject="src">
            <property name="enableSubPackages" value="true" />
        </javaClientGenerator>

        <!--
            指定要逆向分析哪些表:配置要逆向解析的数据表
                tableName:表名
                domainObjectName:对应的javaBean名
         -->
        <table tableName="tbl_dept" domainObjectName="Department"></table>
        <table tableName="tbl_employee" domainObjectName="Employee"></table>
    </context>
</generatorConfiguration>

生成器代码

    /*生成器代码*/
    @Test
    public void testMBG() 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);
    }

测试代码

    @Test
    public void testSelect() throws IOException {
        // 获取SqlSession对象
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        SqlSession openSession = sqlSessionFactory.openSession();
        EmployeeMapper employeeMapper = openSession.getMapper(EmployeeMapper.class);

        List<Employee> employees = employeeMapper.selectByExample(null);
        System.out.println(employees);
        openSession.close();
    }

    @Test
    public void testSelect2() throws IOException {
        // 获取SqlSession对象
        SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
        SqlSession openSession = sqlSessionFactory.openSession();
        EmployeeMapper employeeMapper = openSession.getMapper(EmployeeMapper.class);

        EmployeeExample employeeExample = new EmployeeExample();
        // 所有的条件都在example中封装
        EmployeeExample.Criteria criteria = employeeExample.createCriteria();
        criteria.andLastNameLike("%ad%");
        criteria.andIdGreaterThan(5);
        // 查询
        List<Employee> employees = employeeMapper.selectByExample(employeeExample);
        System.out.println(employees);
        openSession.close();
    }

Mybatis运行原理

框架图

运行流程图

运行时序图

1、根据配置文件创建SQLSessionFactory(最终返回的是DefaultSqlSessionFactory):解析文件的每一个信息保存在Configuration对象中,然后返回包含了Configuration的DefaultSqlSessionFactory对象。

一个MappedStatement代表一个增删改查标签的详细信息:

Configuration对象保存了所有配置文件的详细信息:

全局Configuration中的一个重要属性:

全局Configuration中的一个重要属性:

2、返回SqlSession的实现类DefaultSqlSession对象。他里面包含了Executor和Configuration;Executor会在这一步被创建:

3、getMapper使用MapperProxyFactory返回接口的代理对象MapperProxy,包含了SqlSession对象(实际上是DefaultSqlSession):

4、查询流程:

execute方法里面会判断类型:

运行流程总结:

  1. 根据配置文件(全局和sql映射)初始化出Configuration对象
  2. 创建一个DefaultSqlSession,里面包含Configuration以及Executor(根据全局配置文件中的defaultExecutorType创建出对应的Executor)
  3. DefaultSqlSession.getMapper(),拿到Mapper接口对应的MapperProxy
  4. MapperProxy里面有DefaultSqlSession
  5. 执行增删改查,代理对象调用DefaultSqlSession的增删改查方法(最终会调用Executor的增删改查方法)
  6. Executor创建一个Statement对象(创建一个StatementHandler对象,同时也会创建出ParameterHandler和ResultSetHandler)
  7. 调用StatementHandler的预编译参数以及设置参数值(使用ParameterHandler给SQL语句设置参数),调用完毕后去调用StatementHandler的增删改查方法
  8. ResultSetHandler封装结果

查询流程总结

插件

MyBatis在四大对象的创建过程中,都会有插件进行介入。插件可以利用动态代理机制一层层的包装目标对象,而实现在目标对象执行目标方法之前进行拦截的效果。 pluginAll()方法

MyBatis允许在已映射语句执行过程中的某一点进行拦截调用。

默认情况下,MyBatis允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

  • ParameterHandler (getParameterObject, setParameters)

  • ResultSetHandler (handleResultSets, handleOutputParameters)

  • StatementHandler (prepare, parameterize, batch, update, query)

插件开发步骤

1、编写插件实现Interceptor接口,并使用 @Intercepts注解完成插件签名

package com.code.mybatis.plugin;

import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import java.util.Properties;

/**
 * type:要拦截四大对象的哪个对象
 * method:拦截对象的哪个方法
 * args:当前方法的参数列表
 */
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare",
                args = {java.sql.Connection.class, java.lang.Integer.class})
})
public class MyFirstPlugin implements Interceptor {
    /**
     * 拦截方法,用来拦截目标对象的目标方法的执行
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("MyFirstPlugin---intercept---Invocation:" + invocation.getMethod());
        // 执行目标方法
        Object proceed = invocation.proceed();
        return proceed;
    }

    /**
     * 包装目标对象的:包装(为目标对象创建一个代理对象)
     */
    @Override
    public Object plugin(Object target) {
        System.out.println("MyFirstPlugin---plugin:" + target);
        // 返回包装后的对象
        Object wrap = Plugin.wrap(target, this);
        return wrap;
    }

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

2、在全局配置文件中注册插件

    <!--插件配置-->
    <plugins>
        <plugin interceptor="com.code.mybatis.plugin.MyFirstPlugin">
            <property name="username" value="tomcat"/>
            <property name="password" value="123456"/>
        </plugin>
        <plugin interceptor="com.code.mybatis.plugin.MySecondPlugin">
        </plugin>
    </plugins>

插件会产生目标对象的代理对象:

多个插件就会产生多层代理:

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

Interceptor接口

Intercept:拦截目标方法执行

plugin:生成动态代理对象,可以使用MyBatis提供的Plugin类的wrap方法

setProperties:注入插件配置时设置的属性

/**
 * 完成插件签名:
 *		告诉MyBatis当前插件用来拦截哪个对象的哪个方法
 */
@Intercepts(
		{
			@Signature(type=StatementHandler.class,method="parameterize",args=java.sql.Statement.class)
		})
public class MyFirstPlugin implements Interceptor{

	/**
	 * intercept:拦截:
	 * 		拦截目标对象的目标方法的执行;
	 */
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		// TODO Auto-generated method stub
		System.out.println("MyFirstPlugin...intercept:"+invocation.getMethod());
		//动态的改变一下sql运行的参数:以前1号员工,实际从数据库查询3号员工
		Object target = invocation.getTarget();
		System.out.println("当前拦截到的对象:"+target);
		//拿到:StatementHandler==>ParameterHandler===>parameterObject
		//拿到target的元数据
		MetaObject metaObject = SystemMetaObject.forObject(target);
		Object value = metaObject.getValue("parameterHandler.parameterObject");
		System.out.println("sql语句用的参数是:"+value);
		//修改完sql语句要用的参数
		metaObject.setValue("parameterHandler.parameterObject", 11);
		//执行目标方法
		Object proceed = invocation.proceed();
		//返回执行后的返回值
		return proceed;
	}

	/**
	 * plugin:
	 * 		包装目标对象的:包装:为目标对象创建一个代理对象
	 */
	@Override
	public Object plugin(Object target) {
		// TODO Auto-generated method stub
		//我们可以借助Plugin的wrap方法来使用当前Interceptor包装我们目标对象
		System.out.println("MyFirstPlugin...plugin:mybatis将要包装的对象"+target);
		Object wrap = Plugin.wrap(target, this);
		//返回为当前target创建的动态代理
		return wrap;
	}

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

}

插件原理

  1. 按照插件注解声明,按照插件配置顺序调用插件plugin方 法,生成被拦截对象的动态代理
  2. 多个插件依次生成目标对象的代理对象,层层包裹,先声明的先包裹;形成代理链
  3. 目标方法执行时依次从外到内执行插件的intercept方法。

  1. 多个插件情况下,我们往往需要在某个插件中分离出目标对象。可以借助MyBatis提供的SystemMetaObject类来进行获取最后一层的h以及target属性的值。

Mybatis实用场景

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

PageHelper分页插件

PageHelper是MyBatis中非常方便的第三方分页插件。

官方文档:github.com/pagehelper/…

使用说明:github.com/pagehelper/…

1、导入jar包

2、在MyBatis全局配置文件中配置分页插件

<plugins>
    <!-- com.github.pagehelper为PageHelper类所在包名 -->
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
      <!-- 使用下面的方式配置参数 -->
      <property name="param1" value="value1"/>
    </plugin>
</plugins>

3、使用PageHelper提供的方法进行分页(更多方法请查看github的使用说明)

@Test
public void testSelectOffsetPage() throws IOException {
	SqlSession openSession = getSqlSession();
	EmployeeMapper employeeMapper = openSession.getMapper(EmployeeMapper.class);

	PageHelper.startPage(3,3);
	List<Employee> allEmp = employeeMapper.getAllEmp();
	System.out.println(allEmp.size());
	for (Employee e: allEmp) {
    	System.out.println(e);
	}
	openSession.close();
}

4、可以使用更强大的PageInfo封装返回结果

    @Test
    public void testSelectPageInfo() throws IOException {
        SqlSession openSession = getSqlSession();
        EmployeeMapper employeeMapper = openSession.getMapper(EmployeeMapper.class);

        PageHelper.startPage(1,5);
        List<Employee> allEmp = employeeMapper.getAllEmp();
        // 使用PageInfo对结果进行包装
        PageInfo page = new PageInfo(allEmp);
        //测试PageInfo全部属性,PageInfo包含了非常全面的分页属性
        assertEquals(1, page.getPageNum());
        assertEquals(5, page.getPageSize());
        assertEquals(1, page.getStartRow());
        assertEquals(5, page.getEndRow());
        assertEquals(21, page.getTotal());
        assertEquals(5, page.getPages());
        assertEquals(1, page.getFirstPage());
        assertEquals(5, page.getLastPage());
        assertEquals(true, page.isIsFirstPage());
        assertEquals(false, page.isIsLastPage());
        assertEquals(false, page.isHasPreviousPage());
        assertEquals(true, page.isHasNextPage());

        openSession.close();
    }

批量操作

默认的openSession()方法没有参数,它会创建有如下特性的:

  • 会开启一个事务(也就是不自动提交

  • 连接对象会从由活动环境配置的数据源实例得到

  • 事务隔离级别将会使用驱动或数据源的默认设置

  • 预处理语句不会被复用,也不会批量处理更新

openSession 方法的 ExecutorType 类型的参数,枚举类型:

  • ExecutorType.SIMPLE:这个执行器类型不做特殊的事情(这是默认装配的)。它为每个语句的执行创建一个新的预处理语句
  • ExecutorType.REUSE:这个执行器类型会复用预处理语句
  • ExecutorType.BATCH:这个执行器会批量执行所有更新语句

批量操作我们是使用MyBatis提供的BatchExecutor进行的,他的底层就是通过jdbc攒sql的方式进行的。我们可以让他攒够一定数量后发给数据库一次。

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

        // 设置SqlSession为可以批量操作:ExecutorType.BATCH
        SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
        EmployeeMapper employeeMapper = openSession.getMapper(EmployeeMapper.class);

        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            Employee employee = new Employee(null, "Insert" + i, "qq.com", "1");
            employeeMapper.addEmp(employee);
        }
        openSession.commit();
        openSession.close();
        long endTime = System.currentTimeMillis();
        // 批量时间:2510    非批量时间:4898
        System.out.println("插入一万条数据耗时:" + (endTime - startTime));
    }

与Spring整合中,推荐额外配置一个可以专门用来执行批量操作的sqlSession。需要用到批量操作的时候,我们可以注入配置的这个批量 SqlSession,通过他获取到mapper映射器进行操作。

注意:

1、批量操作是在session.commit()以后才发送sql语句给数据库进行执行的

2、如果我们想让其提前执行,以方便后续可能的查询操作获取数据,我们可以使用sqlSession.flushStatements()方法,让其直接冲刷到数据库进行执行。

存储过程

实际开发中,我们通常也会写一些存储过程, MyBatis也支持对存储过程的调用。一个最简单的存储过程:

delimiter $$ 
create procedure test() 
begin
    select 'hello'; 
end $$ 
delimiter ; 

存储过程的调用

1、select标签中statementType="CALLABLE”" (statementType:可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED)

2、标签体中调用语法:

{call procedure_name(#{param1_info},#{param2_info})} 

typeHandler处理枚举

我们可以通过自定义TypeHandler的形式来在设置参数或者取出结果集的时候自定义参数封装策略。

使用typeHandler处理枚举步骤:

1、实现TypeHandler接口或者继承BaseTypeHandler

2、使用 @MappedTypes定义处理的java类型,使用 @MappedJdbcTypes定义jdbcType类型

3、在自定义结果集标签或者参数处理的时候声明使用自定义TypeHandler进行处理,或者在全局配置TypeHandler要处理的javaType

测试实例:一个代表状态的枚举类

public enum EmpStatus { LOGIN, LOGOUT,REMOVE; }

1、测试全局配置EnumOrdinalTypeHandler( 保存的是枚举的索引)

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

2、测试全局配置EnumTypeHandler (不配置typeHandlers,默认就是这个。保存的是名字)

    <typeHandlers>
        <typeHandler handler="org.apache.ibatis.type.EnumTypeHandler"
                     javaType="com.code.mybatis.bean.EmpStatus"/>
    </typeHandlers>

3、测试参数位置设置自定义TypeHandler

首先将枚举类修改一下:

public enum EmpStatus {
    LOGIN(101, "用户登录"),
    LOGOUT(202, "用户登出"),
    REMOVE(303, "用户移除");

    private Integer code;
    private String msg;

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

    ...

    // 按照状态码返回枚举对象
    public static EmpStatus getEmpStatusByCode(Integer code){
        switch(code){
            case 101:
                return LOGIN;
            case 202:
                return LOGOUT;
            case 303:
                return REMOVE;
            default:
                return LOGOUT;
        }
    }
}

然后自定义一个TypeHandler:

public class MyEmpStatusTypeHandler implements TypeHandler<EmpStatus> {

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

    // 按照列名取
    @Override
    public EmpStatus getResult(ResultSet resultSet, String s) throws SQLException {
        int code = resultSet.getInt(s);
        EmpStatus empStatusByCode = EmpStatus.getEmpStatusByCode(code);
        return empStatusByCode;
    }

    // 按照索引取
    @Override
    public EmpStatus getResult(ResultSet resultSet, int i) throws SQLException {
        int code = resultSet.getInt(i);
        EmpStatus empStatusByCode = EmpStatus.getEmpStatusByCode(code);
        return empStatusByCode;
    }

    @Override
    public EmpStatus getResult(CallableStatement callableStatement, int i) throws SQLException {
        int code = callableStatement.getInt(i);
        EmpStatus empStatusByCode = EmpStatus.getEmpStatusByCode(code);
        return empStatusByCode;
    }
}

然后配置类型处理器:

    <typeHandlers>
        <typeHandler handler="com.code.mybatis.dao.MyEmpStatusTypeHandler"
                     javaType="com.code.mybatis.bean.EmpStatus"/>
    </typeHandlers>

还有一种方法不用全局注册,在处理字段时指定typeHandler:

    <!--public Integer addEmp(Employee employee);-->
    <insert id="addEmp" parameterType="employee">
        insert into <include refid="table_name" />(last_name, gender, email, emp_status)
        values(#{lastName}, #{gender}, #{email},
        #{empStatus, typeHandler=com.code.mybatis.dao.MyEmpStatusTypeHandler})
    </insert>