动态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、Map | size、isEmpty | List/Set/Map.size()、List/Set/Map.isEmpty() |
| List、Set | iterator | List.iterator()、Set.iterator() |
| Map | keys、values | Map.keySet()、Map.values() |
| Iterator | next、hasNext | Iterator.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 && lastName!=""">
last_name like #{lastName} and
</if>
<if test="email!=null and email.trim()!=""">
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 && lastName!=""">
and last_name like #{lastName}
</if>
<if test="email!=null and email.trim()!=""">
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!=""">
last_name like #{lastName} and
</if>
<if test="email!=null and email.trim()!=""">
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!=""">
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查询语句+参数。
一级缓存失效的四种情况:
-
不同的SqlSession对应不同的一级缓存
-
同一个SqlSession但是查询条件不同
-
同一个SqlSession两次查询期间执行了任何一次增删改操作
-
同一个SqlSession两次查询期间手动清空了缓存(clearCache方法)
二级缓存(全局缓存)
二级缓存(secondlevelcache),全局作用域缓存 ,基于namespace级别的
二级缓存默认不开启,需要手动配置。MyBatis提供二级缓存的接口以及实现,缓存实现要求POJO实现Serializable接口。二级缓存在SqlSession关闭或提交之后才会生效(因为查出的数据都会先被放在一级缓存中)。
使用步骤
- 全局配置文件中开启二级缓存
- <setting name="cacheEnabled" value="true"/>
- 需要使用二级缓存的映射文件处使用cache配置缓存
- <cache />
- 注意: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:指定自定义缓存的全类名
缓存有关的设置
- 全局setting的cacheEnable
- 配置二级缓存的开关cacheEnable=true。
- 设置cacheEnable=false,则关闭的是二级缓存,一级缓存一直是打开的
- select标签的useCache属性
- 配置这个select是否使用二级缓存。一级缓存一直是使用的
- sql标签的flushCache属性
- 增删改默认flushCache=true。sql执行以后,会同时清空一级和二级缓存(后面发新的SQL进行执行) 。 查询默认flushCache=false。
- sqlSession.clearCache()
- 只是用来清除一级缓存
- 当在某一个作用域 (一级缓存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接口方便我们进行自定义扩展。
步骤:
- 导入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
- 编写ehcache.xml配置文件
- 配置cache标签
参照缓存:若想在命名空间中共享相同的缓存配置和实例。 可以使用 cache-ref 元素来引用另外一个缓存。
整合Spring
下载适配包
1、查看不同MyBatis版本整合Spring时使用的适配包:
2、下载整合适配包
3、官方整合示例jpetstore
整合配置
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方法里面会判断类型:
运行流程总结:
- 根据配置文件(全局和sql映射)初始化出Configuration对象
- 创建一个DefaultSqlSession,里面包含Configuration以及Executor(根据全局配置文件中的defaultExecutorType创建出对应的Executor)
- DefaultSqlSession.getMapper(),拿到Mapper接口对应的MapperProxy
- MapperProxy里面有DefaultSqlSession
- 执行增删改查,代理对象调用DefaultSqlSession的增删改查方法(最终会调用Executor的增删改查方法)
- Executor创建一个Statement对象(创建一个StatementHandler对象,同时也会创建出ParameterHandler和ResultSetHandler)
- 调用StatementHandler的预编译参数以及设置参数值(使用ParameterHandler给SQL语句设置参数),调用完毕后去调用StatementHandler的增删改查方法
- 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);
}
}
插件原理
- 按照插件注解声明,按照插件配置顺序调用插件plugin方 法,生成被拦截对象的动态代理
- 多个插件依次生成目标对象的代理对象,层层包裹,先声明的先包裹;形成代理链
- 目标方法执行时依次从外到内执行插件的intercept方法。
-
多个插件情况下,我们往往需要在某个插件中分离出目标对象。可以借助MyBatis提供的SystemMetaObject类来进行获取最后一层的h以及target属性的值。
Mybatis实用场景
- PageHelper插件进行分页
- 批量操作
- 存储过程
- typeHandler处理枚举
PageHelper分页插件
PageHelper是MyBatis中非常方便的第三方分页插件。
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>