十、MyBatis-插件开发
MyBatis在四大对象的创建过程中,都会有插件进行 介入。插件可以利用动态代理机制一层层的包装目标对象,而实现在目标对象执行目标方法之前进行拦截 的效果。
MyBatis 允许在已映射语句执行过程中的某一点进行 拦截调用。
默认情况下,MyBatis 允许使用插件来拦截的方法调 用包括:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)
10.1、插件开发
插件开发步骤
1)、编写插件实现Interceptor接口,并使用 @Intercepts注解完成插件签名。
2)、在全局配置文件中注册插件。
/**
* 完成插件签名:
* 告诉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", 3);
//执行目标方法
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、获取sqlSessionFactory对象:
* 解析文件的每一个信息保存在Configuration中,返回包含Configuration的DefaultSqlSession;
* 注意:【MappedStatement】:代表一个增删改查的详细信息
*
* 2、获取sqlSession对象
* 返回一个DefaultSQlSession对象,包含Executor和Configuration;
* 这一步会创建Executor对象;
*
* 3、获取接口的代理对象(MapperProxy)
* getMapper,使用MapperProxyFactory创建一个MapperProxy的代理对象
* 代理对象里面包含了,DefaultSqlSession(Executor)
* 4、执行增删改查方法
*
* 总结:
* 1、根据配置文件(全局,sql映射)初始化出Configuration对象
* 2、创建一个DefaultSqlSession对象,
* 他里面包含Configuration以及
* Executor(根据全局配置文件中的defaultExecutorType创建出对应的Executor)
* 3、DefaultSqlSession.getMapper():拿到Mapper接口对应的MapperProxy;
* 4、MapperProxy里面有(DefaultSqlSession);
* 5、执行增删改查方法:
* 1)、调用DefaultSqlSession的增删改查(Executor);
* 2)、会创建一个StatementHandler对象。
* (同时也会创建出ParameterHandler和ResultSetHandler)
* 3)、调用StatementHandler预编译参数以及设置参数值;
* 使用ParameterHandler来给sql设置参数
* 4)、调用StatementHandler的增删改查方法;
* 5)、ResultSetHandler封装结果
* 注意:
* 四大对象每个创建的时候都有一个interceptorChain.pluginAll(parameterHandler);
*
* @throws IOException
*/
@Test
public void test01() throws IOException {
// 1、获取sqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
// 2、获取sqlSession对象
SqlSession openSession = sqlSessionFactory.openSession();
try {
// 3、获取接口的实现类对象
//会为接口自动的创建一个代理对象,代理对象去执行增删改查方法
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
Employee employee = mapper.getEmpById(1);
System.out.println(mapper);
System.out.println(employee);
} finally {
openSession.close();
}
}
/**
* 插件原理
* 在四大对象创建的时候
* 1、每个创建出来的对象不是直接返回的,而是
* interceptorChain.pluginAll(parameterHandler);
* 2、获取到所有的Interceptor(拦截器)(插件需要实现的接口);
* 调用interceptor.plugin(target);返回target包装后的对象
* 3、插件机制,我们可以使用插件为目标对象创建一个代理对象;AOP(面向切面)
* 我们的插件可以为四大对象创建出代理对象;
* 代理对象就可以拦截到四大对象的每一个执行;
*
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
*/
全局配置文件mybatis-config.xml中配置注册 插件
<!--plugins:注册插件 -->
<plugins>
<plugin interceptor="com.atguigu.mybatis.dao.MyFirstPlugin">
<property name="username" value="root"/>
<property name="password" value="root"/>
</plugin>
<plugin interceptor="com.atguigu.mybatis.dao.MySecondPlugin"></plugin>
</plugins>
输出结果:
插件配置的信息:{password=root, username=root}
MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.executor.CachingExecutor@56ac3a89
MySecondPlugin...plugin:org.apache.ibatis.executor.CachingExecutor@56ac3a89
MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.scripting.defaults.DefaultParameterHandler@3632be31
MySecondPlugin...plugin:org.apache.ibatis.scripting.defaults.DefaultParameterHandler@3632be31
MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.executor.resultset.DefaultResultSetHandler@13a5fe33
MySecondPlugin...plugin:org.apache.ibatis.executor.resultset.DefaultResultSetHandler@13a5fe33
MyFirstPlugin...plugin:mybatis将要包装的对象org.apache.ibatis.executor.statement.RoutingStatementHandler@370736d9
MySecondPlugin...plugin:org.apache.ibatis.executor.statement.RoutingStatementHandler@370736d9
DEBUG 09-29 21:10:52,348 ==> Preparing: select id,last_name lastName,email,gender from tbl_employee where id = ? (BaseJdbcLogger.java:145)
MySecondPlugin...intercept:public abstract void org.apache.ibatis.executor.statement.StatementHandler.parameterize(java.sql.Statement) throws java.sql.SQLException
MyFirstPlugin...intercept:public abstract void org.apache.ibatis.executor.statement.StatementHandler.parameterize(java.sql.Statement) throws java.sql.SQLException
当前拦截到的对象:org.apache.ibatis.executor.statement.RoutingStatementHandler@370736d9
sql语句用的参数是:1
DEBUG 09-29 21:10:52,372 ==> Parameters: 3(Integer) (BaseJdbcLogger.java:145)
DEBUG 09-29 21:10:52,391 <== Total: 1 (BaseJdbcLogger.java:145)
org.apache.ibatis.binding.MapperProxy@33723e30
Employee [id=3, lastName=jerry4, email=null, gender=1]
10.2、插件原理
- 1)、按照插件注解声明,按照插件配置顺序调用插件plugin方 法,生成被拦截对象的动态代理
- 2)、多个插件依次生成目标对象的代理对象,层层包裹,先声 明的先包裹;形成代理链
- 3)、目标方法执行时依次从外到内执行插件的intercept方法。
- 4)、多个插件情况下,我们往往需要在某个插件中分离出目标对象。可以借助MyBatis提供的SystemMetaObject类来进行获 取最后一层的h以及target属性的值。
10.3、Interceptor接口
Intercept:拦截目标方法执行。
plugin:生成动态代理对象,可以使用MyBatis提 供的Plugin类的wrap方法。
setProperties:注入插件配置时设置的属性。
十一、扩展:MyBatis实用场景
- 1)、PageHelper插件进行分页
- 2)、批量操作
- 3)、存储过程
- 4)、typeHandler处理枚举
11.1、PageHelper插件进行分页
PageHelper是MyBatis中非常方便的第三方分页 插件。
官方文档:
https://github.com/pagehelper/MybatisPageHelper/blob/master/README_zh.md
使用步骤:
- 1、导入相关包pagehelper-x.x.x.jar 和 jsqlparser0.9.5.jar。
- 2、在MyBatis全局配置文件中配置分页插件。
- 3、使用PageHelper提供的方法进行分页。
- 4、可以使用更强大的PageInfo封装返回结果。
11.2、批量操作
默认的 openSession() 方法没有参数,它会创建有如下特性的会开启一个事务(也就是不自动提交) 连接对象会从由活动环境配置的数据源实例得到。 事务隔离级别将会使用驱动或数据源的默认设置。 预处理语句不会被复用,也不会批量处理更新。
openSession 方法的 ExecutorType类型的参数,枚举类型:
ExecutorType.SIMPLE: 这个执行器类型不做特殊的事情(这是默认装配 的)。它为每个语句的执行创建一个新的预处理语句。
ExecutorType.REUSE: 这个执行器类型会复用预处理语句。
ExecutorType.BATCH: 这个执行器会批量执行所有更新语句。
批量操作我们是使用MyBatis提供的BatchExecutor进行的,他的底层就是通过jdbc攒sql的方式进行的。我们可以让他攒够一定数量后发给数据库一次。
@Test
public void testBatch() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
//可以执行批量操作的sqlSession
SqlSession openSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
long start = System.currentTimeMillis();
try{
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
for (int i = 0; i < 10000; i++) {
mapper.addEmp(new Employee(UUID.randomUUID().toString().substring(0, 5), "b", "1"));
}
openSession.commit();
long end = System.currentTimeMillis();
//批量:(预编译sql一次==>设置参数===>10000次===>执行(1次))
//Parameters: 616c1(String), b(String), 1(String)==>4598
//非批量:(预编译sql=设置参数=执行)==》10000 10200
System.out.println("执行时长:"+(end-start));
}finally{
openSession.close();
}
}
与Spring整合中,我们推荐,额外的配置一个可以专门用来执行批量操作的sqlSession。
需要用到批量操作的时候,我们可以注入配置的这个批量 SqlSession。通过他获取到mapper映射器进行操作。
注意:
1、批量操作是在session.commit()以后才发送sql语句给数、据库进行执行的。
2、如果我们想让其提前执行,以方便后续可能的查询操作 获取数据,我们可以使用sqlSession.flushStatements()方法,让其直接冲刷到数据库进行执行。
11.3、存储过程
实际开发中,我们通常也会写一些存储过程, MyBatis也支持对存储过程的调用。
一个最简单的存储过程:
delimiter $$
create procedure test()
begin
select 'hello';
end $$
delimiter ;
存储过程的调用
1、select标签中
statementType=“CALLABLE”
2、标签体中调用语法:
{call procedure_name(#{param1_info},#{param2_info})}
存储过程过-游标处理
MyBatis对存储过程的游标提供了一个JdbcType=CURSOR的支持, 可以智能的把游标读取到的数据,映射到我们声明的结果集中。
调用实例:
<!-- public void getPageByProcedure();
1、使用select标签定义调用存储过程
2、statementType="CALLABLE":表示要调用存储过程
3、{call procedure_name(params)}
-->
<select id="getPageByProcedure" statementType="CALLABLE" databaseId="oracle">
{call hello_test(
#{start,mode=IN,jdbcType=INTEGER},
#{end,mode=IN,jdbcType=INTEGER},
#{count,mode=OUT,jdbcType=INTEGER},
#{emps,mode=OUT,jdbcType=CURSOR,javaType=ResultSet,resultMap=PageEmp}
)}
</select>
<resultMap type="com.atguigu.mybatis.bean.Employee" id="PageEmp">
<id column="EMPLOYEE_ID" property="id"/>
<result column="LAST_NAME" property="email"/>
<result column="EMAIL" property="email"/>
</resultMap>
测试代码:
/**
* oracle分页:
* 借助rownum:行号;子查询;
* 存储过程包装分页逻辑
* @throws IOException
*/
@Test
public void testProcedure() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try{
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
OraclePage page = new OraclePage();
page.setStart(1);
page.setEnd(5);
mapper.getPageByProcedure(page);
System.out.println("总记录数:"+page.getCount());
System.out.println("查出的数据:"+page.getEmps().size());
System.out.println("查出的数据:"+page.getEmps());
}finally{
openSession.close();
}
}
OraclePage:
/**
* 封装分页查询数据
* @author lfy
*
*/
public class OraclePage {
private int start;
private int end;
private int count;
private List<Employee> emps;
}
存储过程:
create or replace procedure
hello_test(p_start in int,
p_end in int,
P_count out int,
ref_cur out sys_refcursor) AS
BEGIN
select count(*) into p_count from emp;
open ref_cur for
select * from (select e.*,rownum as r1 from emp e where rownum<p_end)where r1>p_start;
END hello_test;
11.4、自定义TypeHandler处理枚举
可以通过自定义TypeHandler的形式来在设置参数或 者取出结果集的时候自定义参数封装策略。
步骤:
- 1、实现TypeHandler接口或者继承BaseTypeHandler。
- 2、使用@MappedTypes定义处理的java类型 使用@MappedJdbcTypes定义jdbcType类型。
- 3、在自定义结果集标签或者参数处理的时候声明使用自定义 TypeHandler进行处理 或者在全局配置TypeHandler要处理的javaType。
测试实例
1、一个枚举类
/**
* 希望数据库保存的是100,200这些状态码,而不是默认0,1或者枚举的名
* @author lfy
*
*/
public enum EmpStatus {
LOGIN(100,"用户登录"),LOGOUT(200,"用户登出"),REMOVE(300,"用户不存在");
private Integer code;
private String msg;
private EmpStatus(Integer code,String msg){
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
//按照状态码返回枚举对象
public static EmpStatus getEmpStatusByCode(Integer code){
switch (code) {
case 100:
return LOGIN;
case 200:
return LOGOUT;
case 300:
return REMOVE;
default:
return LOGOUT;
}
}
}
2、配置
<typeHandlers>
<!--1、配置我们自定义的TypeHandler -->
<typeHandler handler="com.atguigu.mybatis.typehandler.MyEnumEmpStatusTypeHandler" javaType="com.atguigu.mybatis.bean.EmpStatus"/>
<!--2、也可以在处理某个字段的时候告诉MyBatis用什么类型处理器
保存:#{empStatus,typeHandler=xxxx}
查询:
<resultMap type="com.atguigu.mybatis.bean.Employee" id="MyEmp">
<id column="id" property="id"/>
<result column="empStatus" property="empStatus" typeHandler=""/>
</resultMap>
注意:如果在参数位置修改TypeHandler,应该保证保存数据和查询数据用的TypeHandler是一样的。
-->
</typeHandlers>
3、测试代码
/**
* 1、实现TypeHandler接口。或者继承BaseTypeHandler
* @author lfy
*/
public class MyEnumEmpStatusTypeHandler implements TypeHandler<EmpStatus> {
/**
* 定义当前数据如何保存到数据库中
*/
@Override
public void setParameter(PreparedStatement ps, int i, EmpStatus parameter,
JdbcType jdbcType) throws SQLException {
// TODO Auto-generated method stub
System.out.println("要保存的状态码:"+parameter.getCode());
ps.setString(i, parameter.getCode().toString());
}
@Override
public EmpStatus getResult(ResultSet rs, String columnName)
throws SQLException {
// TODO Auto-generated method stub
//需要根据从数据库中拿到的枚举的状态码返回一个枚举对象
int code = rs.getInt(columnName);
System.out.println("从数据库中获取的状态码:"+code);
EmpStatus status = EmpStatus.getEmpStatusByCode(code);
return status;
}
@Override
public EmpStatus getResult(ResultSet rs, int columnIndex)
throws SQLException {
// TODO Auto-generated method stub
int code = rs.getInt(columnIndex);
System.out.println("从数据库中获取的状态码:"+code);
EmpStatus status = EmpStatus.getEmpStatusByCode(code);
return status;
}
@Override
public EmpStatus getResult(CallableStatement cs, int columnIndex)
throws SQLException {
// TODO Auto-generated method stub
int code = cs.getInt(columnIndex);
System.out.println("从数据库中获取的状态码:"+code);
EmpStatus status = EmpStatus.getEmpStatusByCode(code);
return status;
}
}
测试代码:
/**
* 默认mybatis在处理枚举对象的时候保存的是枚举的名字:EnumTypeHandler
* 改变使用:EnumOrdinalTypeHandler:
* @throws IOException
*/
@Test
public void testEnum() throws IOException{
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try{
EmployeeMapper mapper = openSession.getMapper(EmployeeMapper.class);
Employee employee = new Employee("test_enum", "enum@atguigu.com","1");
mapper.addEmp(employee);
System.out.println("保存成功"+employee.getId());
openSession.commit();
// Employee empById = mapper.getEmpById(30026);
// System.out.println(empById.getEmpStatus());
}finally{
openSession.close();
}
}
评论