01-mybatis
01-mybatis
Mybatis概述
一. mybatis参数处理
1. 单个参数
mybatis不会做特殊处理,#{参数名}
2. 多个参数
会做特殊处理
多个参数会被封装成一个map,#{}就是从map中获取指定的key的值
Key: param1…paramN, 或者参数的索引也可以
Value:传入的参数值
- 因此建议使用命名参数:
明确的指出封装参数时map的key, 使用@Param注解指定的值
Employee getEmpByIdAndLastname(@Param("id") Integer id, @Param("lastName") String lastName);
-
如果多个参数正好是我们业务逻辑的数据模型,我们就可以直接传入pojo
#{属性名},取出传入的pojo属性值
-
如果多个参数不是业务模型中的数据,没有对应的pojo,为了方便,我们也可以传入map
#{key}: 取出map中对应的值
-
如果多个参数不是业务模型中的数据,没有对应的pojo,但是经常使用, 推荐来编写一个TO(Transfer Object)
-
特别注意:如果是Collection类型(List, Set)或者是数组,也会特殊处理。也是把传入的list或者数组封装在map中。
Key: Collection(collection) 如果是List还可以使用这个key(list),数组(array)
public Employee getEmpById(List<Integer> ids); 取值:取出第一个id的值:#{list[0]}
3. 如何处理参数
在org.apache.ibatis.reflection.ParamNameResolver类中
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
//参数为null,直接返回
if (args == null || paramCount == 0) {
return null;
//如果只有一个元素,并且没有Param注解;arg[0]:单个参数返回
} else if (!hasParamAnnotation && paramCount == 1) {
return args[names.firstKey()];
//多个元素或者有Param标注
} else {
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
//遍历names集合
for (Map.Entry<Integer, String> entry : names.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
names
public ParamNameResolver(Configuration config, Method method) { final Class<?>[] paramTypes = method.getParameterTypes(); final Annotation[][] paramAnnotations = method.getParameterAnnotations(); final SortedMap<Integer, String> map = new TreeMap<Integer, String>(); int paramCount = paramAnnotations.length; // get names from @Param annotations for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) { if (isSpecialParameter(paramTypes[paramIndex])) { // skip special parameters continue; } String name = null; for (Annotation annotation : paramAnnotations[paramIndex]) { if (annotation instanceof Param) { hasParamAnnotation = true; name = ((Param) annotation).value(); break; } } if (name == null) { // @Param was not specified. if (config.isUseActualParamName()) { name = getActualParamName(method, paramIndex); } if (name == null) { // use the parameter index as the name ("0", "1", ...) // gcode issue #71 name = String.valueOf(map.size()); } } map.put(paramIndex, name); } names = Collections.unmodifiableSortedMap(map); }
获取每个标了param注解的参数的@Param的值,赋值给name
每次解析一个参数给map中保存信息:(key:参数索引,value:name的值)
name的值:
标注了param的注解:注解的值
没有标注:
全局配置:userActualParamName:name=参数名
name=map.size():相当于当前元素的索引
4. 参数的取值
-
#{}
是以预编译的形式,将参数设置到sql语句中,可以防止sql注入
-
可以规定参数的一些规则
jdbcType通常需要在某种特定的条件下设置:
在我们数据为null的时候,有些数据库可能不能识别mybatis对null的默认处理.
-
-
${}
取出的值直接拼装在sql语句中,会有安全问题
原生jdbc不支持占位符的地方我们就可以使用${}进行取值,比如说:
分表,排序
select * from tb1 order by ${salary} ${order}
二. 映射文件标签
1. select标签
-
返回list,resultType写集合中元素的类型
-
返回一条记录的map;key’是列名,值就是对应的值
/** * key为主键,value为Employee对象key * 告诉mybatis封装这个map 的时候使用哪个属性作为map 的 * @param id * @return */ @MapKey("id") Map<Integer, Employee> getEmpByLastNameLikeReturnMap(Integer id);
<!--getEmpByLastNameLikeReturnMap--> <select id="getEmpByLastNameLikeReturnMap" resultType="emp"> select * from tb1_employee where last_name like #{lastName} </select>
-
resultMap
- 自定义结果集映射规则
<!--type:自定义规则的java类型 id: 唯一id方便引用 --> <resultMap id="MyEmp" type="emp"> <!--column:指定那一列 property:指定对应的javaBean属性 --> <id column="id" property="id"/> <result column="last_name" property="lastName"/> <!--其他不指定的列会自动封装:我们只要写resultMap就把全部的映射规则都写上--> <result column="email" property="email"/> <result column="gender" property="gender"/> </resultMap>
-
联合查询
-
直接以属性.的方式级联
<resultMap id="MyEmp2" type="emp"> <id column="id" property="id"/> <result column="last_name" property="lastName"/> <result column="email" property="email"/> <result column="gender" property="gender"/> <result column="did" property="dept.id"/> <result column="dept_name" property="dept.departmentName"/> </resultMap>
-
使用association标签
<resultMap id="MyEmp2" type="emp"> <id column="id" property="id"/> <result column="last_name" property="lastName"/> <result column="email" property="email"/> <result column="gender" property="gender"/> <!-- property:指定那个属性是联合的对象 javaType:指定这个属性对象的类型[不能省略] --> <association property="dept" javaType="com.mahaonan.bean.Department"> <id column="did" property="id"/> <result column="dept_name" property="departmentName"/> </association>
- 使用association进行分步查询 ```xml <resultMap id="MyEmp2" type="emp"> <id column="id" property="id"/> <result column="last_name" property="lastName"/> <result column="email" property="email"/> <result column="gender" property="gender"/> <!--select: 表明当前属性是调用select指定的方法查出的结果 column:指定那一列的值传给这个方法,即将column的值作为查询条件进行二次查询 即:使用select指定的方法(传入column指定的这列参数的值)查出对象,并封装给property --> <association property="dept" select="com.mahaonan.mapper.DepartmentMapper.getDeptById" column="d_id"> </association> </resultMap>
-
使用延迟加载
在分步查询的基础上加上两个配置:
在全局配置文件中:
<settings> <!--懒加载开启--> <setting name="lazyLoadingEnabled" value="true"/> <setting name="aggressiveLazyLoading" value="false"/> </settings>
-
-
集合对象
<resultMap id="MyEmp2" type="emp"> <collection property="" ofType=""/> </resultMap>
ofType集合里面的元素类型
三. 动态sql
1. <if>
2. <where>
3. <sql>
抽取可重用的sql片段。方便后面引用
<include>
引用sql片段
四. mybatis缓存
1. 一级缓存
本地缓存
与数据库同一次会话期间查询到的数据会放在本地缓存中,以后如果要获取相同的数据,直接从缓存中拿,没必要去查数据库
-
sqlSession级别的缓存。一级缓存是一直开启的;
-
失效情况(没有使用到一级缓存)
-
sqlSession不同
-
sqlSession相同,查询条件不同
-
sqlSession相同,两次查询期间执行了增删改操作
-
sqlSession相同,手动清除了一级缓存
sqlSession.clearCache();
-
2. 二级缓存
基于namespace级别的缓存,一个namespace可以对应一个二级缓存
-
工作机制
- 一个会话,查询一条数据,这个数据就会被放在当前会话的一级缓存中
如果会话关闭
,一级缓存的数据会被保存到二级缓存中,新的会话查询信息,就可以参照二级缓存的信息- 不同namespace查出的数据会放在自己对应的缓存map中
-
使用
-
开启全局二级缓存配置
<setting name="cacheEnabled" value="true"/>
-
在每个mapper中配置使用缓存
<!--eviction 缓存的回收策略 flushInterval 缓存刷新间隔,缓存多长时间清空依次,默认不清空,毫秒单位 readOnly 是否只读, true 只读 mybatis认为所有从缓存中获取数据的操作都是只读操作,不会修改数据,mybatis为了加速获取速度,直接就会将数据 在缓存中的引用交给用户。不安全,速度快 false 非只读 mybatis会利用序列化&反序列化克隆一份新的数据给你。安全,速度慢,默认为这种 size: 缓存中存放多少元素 type: 指定自定义缓存的全类名 自定义实现Cache接口 --> <cache eviction="" flushInterval="" readOnly="" size="" type=""></cach> <cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1024"/>
-
我们的POJO需要实现序列化接口
-
-
注意事项
- 查出的数据都会被默认放在一级缓存中,只有会话提交或者关闭后,一级缓存中的数据才会被放到二级缓存中
3. 和缓存有关的属性和设置
-
cacheEnabled=true;
false关闭,首先关闭二级缓存,一级缓存一直可用
-
select标签都有useCache="true"属性
false关闭,一级缓存一直可用,关闭二级缓存
-
每个增删改标签:flushCache="true"
增删改执行完成后就会清空缓存(一二级都清空)
查询标签也有:flushCache="false"
-
sqlSession.clearCache()
只会清空一级缓存
-
localCacheScope
本地缓存作用域(一级缓存)
Session: 当前会话的所有数据保存在会话缓存中
Statement:可以禁用一级缓存
4. 缓存原理图示
缓存的顺序:
二级缓存->一级缓存->数据库
5. mybatis整合ehcache缓存
- 导入第三方缓存包即可
- 导入与第三方缓存整合的适配包
- mapper.xml中使用自定义缓存
-
在mapper配置文件中加标签
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
-
其他的mapper可以引用这个
<cache-ref namespace="com.mahaonan.mapper.EmployeeMapper"/>
-
ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="conf/ehcache.xsd">
<!-- 磁盘保存路径 -->
<diskStore path="ehcache" />
<defaultCache
maxElementsInMemory="1"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
<!--
属性说明:
l diskStore:指定数据在磁盘中的存储位置。
l defaultCache:当借助CacheManager.add("demoCache")创建Cache时,EhCache便会采用<defalutCache/>指定的的管理策略
以下属性是必须的:
l maxElementsInMemory - 在内存中缓存的element的最大数目
l maxElementsOnDisk - 在磁盘上缓存的element的最大数目,若是0表示无穷大
l eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断
l overflowToDisk - 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上
以下属性是可选的:
l timeToIdleSeconds - 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大
l timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大
diskSpoolBufferSizeMB 这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区.
l diskPersistent - 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。
l diskExpiryThreadIntervalSeconds - 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作
l memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)
-->
五. mybatis整合spring
1. 整合目的
- spring管理所有组件。mapper的实现类。@Autowired
- spring用来管理事务
2. 配置文件
<!--希望管理所有的业务逻辑组件-->
<context:component-scan base-package="com.mahaonan">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!--spring用来控制业务逻辑。数据源,事务控制,aop-->
<!--引入数据库配置文件-->
<context:property-placeholder location="jdbc.properties"/>
<!--配置数据源-->
<bean id="dataSource" destroy-method="close"
class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 基本属性:数据库驱动类、连接字符串、用户名、密码 -->
<property name="driverClass" value="${jdbc.driver}" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<!-- 连接数、最小连接数、最大连接数、最大空闲时间 -->
<property name="initialPoolSize" value="200"/>
<property name="minPoolSize" value="50"/>
<property name="maxPoolSize" value="300"/>
<property name="maxIdleTime" value="60"/>
</bean>
<!--spring的事务管理器-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
<!--整合mybatis
目的:1. spring管理所有组件
-->
<!--创建SqlSessionFactory-->
<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!--指定全局配置文件的位置-->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!--指定mapper文件的位置-->
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>
<!--扫描所有mapper接口的实现,让这些mapper能够自动注入-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.mahaonan.mapper" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryBean" />
</bean>
六. mybatis逆向工程
1. 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="DB2Tables" targetRuntime="MyBatis3">
<!--jdbcConnection: 指定如何连接到目标数据库-->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://mahaonan.com:3306/test"
userId="root"
password="mhn939510">
</jdbcConnection>
<!--java类型解析器-->
<javaTypeResolver >
<property name="forceBigDecimals" value="false" />
</javaTypeResolver>
<!--指定javaBean的生成策略-->
<!--targetPackage目标包名,即要生成bean的存放包位置
targetProject 目标工程,即所在的工程目录
-->
<javaModelGenerator targetPackage="com.mahaonan.mbg.bean" targetProject="src/main/java">
<property name="enableSubPackages" value="true" />
<property name="trimStrings" value="true" />
</javaModelGenerator>
<!--sqlMapGenerator sql映射生成策略-->
<sqlMapGenerator targetPackage="com.mahaonan.mbg.mapper" targetProject="src/main/java">
<property name="enableSubPackages" value="true" />
</sqlMapGenerator>
<!--javaClientGenerator: 指定mapper接口所在的位置-->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.mahaonan.mbg.mapper" targetProject="src/main/java">
<property name="enableSubPackages" value="true" />
</javaClientGenerator>
<!--指定要逆向分析哪些表-->
<table tableName="tb1_employee" domainObjectName="Employee"/>
</context>
</generatorConfiguration>
2. 测试类
@Test
public void test5() throws Exception {
List<String> warnings = new ArrayList<String>();
boolean overwrite = true;
InputStream inputStream = MyBatisTest.class.getClassLoader().getResourceAsStream("mybatis_generator.xml");
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(inputStream);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
myBatisGenerator.generate(null);
}
3. maven插件
<plugins>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.2</version>
<configuration>
<verbose>true</verbose>
<overwrite>true</overwrite>
<configurationFile>${basedir}/src/main/resources/mybatis/mybatis_generator.xml</configurationFile>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
</dependencies>
</plugin>
</plugins>
<resources>
<!--编译src/main/java目录下的xml文件-->
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
<!--指定资源位置-->
<resource>
<directory>src/main/resources</directory>
<includes>
<include>*.xml</include>
<include>**/*.xml</include>
<include>**/*.properties</include>
<include>**/*.ini</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
七. Mybatis运行原理
1. 框架分层图
2. 运行原理
2.1 创建SqlSessionFactory
Configuration
:封装所有配置信息的详细信息(包括全局配置文件和mapper文件)
MappedStatement
: 代表一个增删改查的详细信息
2.2 获取SqlSession对象
返回一个DefaultSqlSession对象,包含Executor和Configuration
2.3 获取Mapper代理对象
使用MapperProxyFactory创建一个MapperProxy的代理对象
MapperProxy中包含了Excutor
2.4 查询实现
2.5 查询总结
2.6 总结
-
根据配置文件(全局, sql映射)初始化Configuration对象
-
创建一个DefaultSqlSession对象,它里面包含Configuration一级Executor(根据全局配置文件中的defaultExecutorType创建出对应的Executor)
-
DefaultSqlSession.getMapper(),拿到Mapper接口对应的MapperProxy
-
MapperProxy里面有(DefaultSqlSession)
-
执行增删改查方法:
-
调用DefaultSqlSession的增删改查(Executor)
-
会创建一个StatementHandler对象,同时也会创建ParameterHandler和ResultSetHandler
-
调用StatementHandler预编译参数以及设置参数值;
使用ParameterHandler来给sql设置参数
-
调用StatementHandler的增删改查方法
-
ResultSetHandler封装结果
-
四大对象每个创建的时候都有一个interceptorChain.pluginAll(statementHandler)
八. Mybatis插件开发
1. 插件原理
/**
* 四大对象创建的时候
* 1. 每个创建出来的对象不是直接返回的,而是interceptorChain.pluginAll(statementHandler)
* 2. 获取到所有的Interceptor(拦截器)(插件需要实现的接口)
* 调用interceptor.plugin(target);返回target包装后的对象
* 3. 插件机制,我们可以使用插件为目标对象创建出一个代理对象;类似AOP
* 我们的插件可以为四大对象创建出代理对象
* 代理对象就可以拦截到四大对象的每一个执行;
*/
2. 插件编写
-
编写Interceptor的实现类
package com.mahaonan.interceptor; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.plugin.*; import java.util.Properties; /** * @Author: M˚Haonan * @Date: 2019-07-03 16:45 * @Description: */ /** * 插件签名 * 告诉mybatis当前插件拦截哪个对象的哪个方法 */ @Intercepts({ @Signature(type = StatementHandler.class, method = "parameterize", args = java.sql.Statement.class) }) public class MyFirstInterceptor implements Interceptor { /** * 拦截目标对象的目标方法的执行 * 这个方法实际上就是动态代理invoke方法,invocation.proceed()调用了原方法的执行,可以在原方法之 * 前之后添加自己的业务逻辑,介入到mybatis的实现 * @param invocation * @return * @throws Throwable */ @Override public Object intercept(Invocation invocation) throws Throwable { System.out.println("MyFirstInterceptor的 intercept方法"); Object proceed = invocation.proceed(); return proceed; } /** * 包装目标对象 * 为目标对象创建一个代理对象 * @param target 即需要介入的对象 * @return */ @Override public Object plugin(Object target) { //我们可以借助Plugin.wrap()来使用当前Interceptor包装我们目标对象 System.out.println("MyFirstInterceptor的 plugin 方法:mybatis将要包装的对象" + target); Object wrap = Plugin.wrap(target, this); //为当前target创建的动态代理 return wrap; } /** * 将插件注册时的property属性设置进来 * @param properties */ @Override public void setProperties(Properties properties) { System.out.println("插件配置的信息" + properties); } }
-
使用@Intercepts注解完成插件签名
@Intercepts({ @Signature(type = StatementHandler.class, method = "parameterize", args = java.sql.Statement.class) })
-
注册到全局配置文件中
<!--注册插件--> <plugins> <plugin interceptor="com.mahaonan.interceptor.MyFirstInterceptor"> <!--属性的值到时候会被注入到setProperties(Properties properties)方法中--> <property name="username" value="root"/> <property name="password" value="123456"/> </plugin> </plugins>
3. 多个插件
层层代理,多层代理
- 创建动态代理的时候,是按照插件配置顺序创建层层代理对象。
- 执行目标方法的时候,按照逆向顺序执行
4. 开发实例
package com.mahaonan.interceptor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.util.Properties;
/**
* @Author: M˚Haonan
* @Date: 2019-07-03 16:45
* @Description:
*/
/**
* 插件签名
* 告诉mybatis当前插件拦截哪个对象的哪个方法
*/
@Intercepts({
@Signature(type = StatementHandler.class, method = "parameterize", args = java.sql.Statement.class)
})
public class MyFirstInterceptor implements Interceptor {
/**
* 拦截目标对象的目标方法的执行
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("MyFirstInterceptor的 intercept方法");
Object target = invocation.getTarget();
//拿到target的元数据
MetaObject metaObject = SystemMetaObject.forObject(target);
//修改sql语句要用到的参数
metaObject.setValue("parameterHandler.parameterObject", 2);
Object proceed = invocation.proceed();
return proceed;
}
/**
* 包装目标对象
* 为目标对象创建一个代理对象
* @param target
* @return
*/
@Override
public Object plugin(Object target) {
//我们可以借助Plugin.wrap()来使用当前Interceptor包装我们目标对象
System.out.println("MyFirstInterceptor的 plugin 方法:mybatis将要包装的对象" + target);
Object wrap = Plugin.wrap(target, this);
//为当前target创建的动态代理
return wrap;
}
/**
* 将插件注册时的property属性设置进来
* @param properties
*/
@Override
public void setProperties(Properties properties) {
System.out.println("插件配置的信息" + properties);
}
}
九. Mybatis扩展
1. 分页插件PageHelper
-
maven依赖
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.0.0</version> </dependency>
-
mybatis配置文件
<plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"/> </plugins>
-
测试
@Test public void test7() { SqlSessionFactory sqlSessionFactory = getSqlSessionFactory(); SqlSession sqlSession = sqlSessionFactory.openSession(); try { EmpolyeeMapper empolyeeMapper = sqlSession.getMapper(EmpolyeeMapper.class); PageHelper.startPage(2, 1); List<Employee> emps = empolyeeMapper.getEmps(); emps.forEach(System.out::println); }finally { sqlSession.close(); } }