Mybatis源码解析:
Mybatis是支持定制化SQL、存储过程和高级映射的持久型框架,主要完成两件事:
1、封装JDBC的操作
2、利用反射完成Java类和SQL之间的转换
Mybatis的主要目的就是管理执行SQL参数的输入和输出,编写SQL和结果集的映射是mybatis的主要优点
Mybatis中主要类和接口:
1、Configuration:将Mybatis配置文件中的信息保存到该类中
2、SqlSessionFactory:解析Configuration类中的配置信息,获取SqlSession
3、SqlSession:负责和数据库的交互,完成增删改查
4、Executor:Mybatis的调度核心,负责Sql的生成
5、StatementHandler:封装了JDBC的statement操作
6、ResultSetHandler:负责完成结果集到Java Bean的转换
7、MappedStatement:代表一个select|update|insert|delete元素
8、SqlSource:根据传入的ParamterObject生成SQL
9、BoundSql:包含SQL和参数信息
#{}和${}的区别
${}是字符串替换,相当于直接显示数据,#{}是预编译处理,相当于对数据加上双引号,即#{}是将传入的值先替换为?号,然后调用PreparedStatement的set方法来赋值,而¥{}是将传入的数据直接显示生成sql语句。
--Mybatis在处理#{}时
select id,name,age from student where id =#{id}
当前端把id值1传入到后台的时候,就相当于:
select id,name,age from student where id ='1'
--Mybatis在处理${}时
select id,name,age from student where id =${id}
当前端把id值1传入到后台的时候,就相当于:
select id,name,age from student where id = 1
使用#{}可以有效防止SQL注入,提高系统的安全性(是进行字符串的拼接)如果在SQL语句中使用Order By就需要使用${}。
{}传入值的时候,sql解析参数是带双引号的,而${}传入值的时候,sql解析参数是不带引号的。
对于Mybatis的理解:
Mybatis内部封装了jdbc,开发者只需要关注sql语句本身,而不需要去花费精力去处理加载驱动、创建连接、创建statement等连接数据库的过程;
Mybatis通过xml或者注解的方式将要执行的各种statement配置起来,并通过java对象和statement中的sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。
Mybatis支持定制化SQL、存储过程以及高级映射。Mybatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。Mybatis可以使用简单的XML或者注解来配置和映射原生信息,将接口和Java的POJO映射程数据库中的记录。
Mybatis中的一级缓存和二级缓存以及它们之间的区别:
缓存的概念:合理的使用缓存是优化中最常见的方法之一,将从数据库中查询出来的数据放入缓存中,下次使用时不必再从数据库中查询,而是直接从缓存中读取,避免频繁操作数据库,减轻数据库的压力,提高系统的性能。
一级缓存是sqlSession级别的缓存:
Mybatis对缓存提供支持,但是再没有配置的默认情况下,它只默认开启一级缓存。一级缓存再操作数据库时需要构造sqlSession对象,在对象中有一个数据结构用于存储缓存数据;不同的sqlSession之间的缓存数据取余是互不影响的。也就是它只能作用再同一个sqlSession中,不同的sqlSession中的缓存是互相不能读取的。
一级缓存的工作原理
二级缓存是mapper级别的缓存:
Mybatis的二级缓存是mapper级别的缓存,它可以提高对数据库查询的效率,以提高应用的性能。多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用一个二级缓存,二级缓存是跨SqlSession的。
二级缓存工作原理
如何开启二级缓存:
1、在Mybatis.xml配置文件中加入:
<span style="font-size:18px;"><settings> <!--开启二级缓存--> <setting name="cacheEnabled" value="true"/>
</settings>
</span>
2、在需要开启二级缓存的mapper.xml中加入caceh标签:
<span style="font-size:18px;"><cache/></span>
3、让使用二级缓存的POJO类实现Serializable接口:
<span style="font-size:18px;">public class User implements Serializable {}</span>
使用Mybatis的mapper接口调用的时候有哪些要求:
1、Mapper接口的方法名和Mappe.xml中定义的每个sql的id相同
2、Mapper接口方法的输入参数类型和Mapper.xml中定义的每个sql的parameterType的类型相同
3、Mapper接口方法的输出参数类型和Mapper.xml中定义的每个sql的resultType的类型相同
4、Mapper.xml文件中的nameSpace即是Mapper接口的类路径
Mybatis中接口绑定有几种实现方式,是怎么实现的?
1、通过注解绑定,在接口的方法上加上@Select@Update等注解,里面包含Sql语句来绑定(Sql语句比较简单的时候,推荐注解绑定);
2、通过XML里写SQL的方式进行绑定,指定xml映射文件里面的namespace必须为接口的全路径名(Sql语句比较复杂的时候推荐使用xml绑定);
Mybatis的XML映射文件中常用的标签:
trim/where/set/foreach/if/choose/when/otherwise/bind
Mybatis的批量新增:
Mybatis的一对一,一对多:
Mybatis一对一关联查询总结:
Mybatis中使用association标签来解决一对一的关联查询,association标签可用的属性有:
1、property:对象属性的名称
2、javaType: 对象属性的类型
3、column: 所对应的外键字段名称
4、select:使用另一个查询(子查询)来封装查询的结果
Mybatis多对多关联查询:
Mybatis中使用collection标签来解决一对多的关联查询,collection标签可用的属性有:
1、property:对象属性的名称
2、ofType: 对象属性的类型
3、column: 所对应的外键字段名称
4、select:使用另一个查询(子查询)来封装查询的结果
Mybatis中的懒加载:
通俗的来说懒加载就是按需加载,我们什么时候需要什么时候再去进行什么操作,先从单表查询,需要的时候再从关联表去关联查询,能大大提高数据库的性能。 在mybatis中,resultMap可以实现高级映射(使用association、collection实现一对一及一对多映射),association、collection具备延迟加载功能。
<mapper namespace="com.agesun.attendance.privilege.provider.mapper.OrgMapper">
<resultMap id="BaseResultMap" type="com.agesun.attendance.privilege.provider.model.Org">
<id column="org_id" jdbcType="INTEGER" property="orgId" />
<result column="parent_id" jdbcType="INTEGER" property="parentId" />
<result column="org_name" jdbcType="VARCHAR" property="orgName" />
<result column="state" jdbcType="INTEGER" property="state" />
<result column="orgFullName" jdbcType="VARCHAR" property="orgFullName" />
<collection property="psList" column="org_id" fetchType="lazy" select="com.agesun.attendance.privilege.provider.mapper.PersonMapper.selectByOrgId">
</collection> //单个resultMap中的懒加载设置 lazy为懒加载,不调用(get()),不从数据查询
</resultMap> eager急加载,查询主表时,就把子集合查询出来
</mapper>
Mybatis开启懒加载:
//在mybatis配置文件 mybatis-configuration.xml中,配置懒加载
<!-- 开启懒加载配置 -->
<settings>
<!-- 全局性设置懒加载。如果设为‘false',则所有相关联的都会被初始化加载。 --> //可以配置lazyLoadingEnabled 值为true,不设置aggressiveLazyLoading,为全局设置
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当设置为‘true'的时候,懒加载的对象可能被任何懒属性全部加载。否则,每个属性都按需加载。 -->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
Mybatis和Spring的整合:
spring和Mybatis整合,就是将SqlMapConfig中的配置文件转移到spring配置文件的过程:
1、数据源DateSource之间写在SqlMapConfig下,现在交给spring容器;
2、mybatis手动创建SqlSessionFactory,现在交给spring
3、之前mybatis的配置文件SqlMapConfig主动加载xxxMapper.xml现在交给spring
4、起别名配置,加载db.properties配置文件
理解的Mybatis的编程步骤:
1、首先,SqlSessionFactoryBuilder去读取Mybatis的配置文件,然后build出一个DefaultSqlSessionFactory:
/**
* 一系列的构造方法最终都会调用本方法(配置文件为Reader时会调用本方法,还有一个InputStream方法与此对应)
* @param reader
* @param environment
* @param properties
* @return
*/
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
//通过XMLConfigBuilder解析配置文件,解析的配置相关信息都会封装为一个Configuration对象
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
//这儿创建DefaultSessionFactory对象
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
//字符流关闭
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
2、当我们获取到SqlSessionFactory之后,就可以通过SqlSessionFactory去获取SqlSession对象:
/**
* 通常一系列openSession方法最终都会调用本方法
* @param execType
* @param level
* @param autoCommit
* @return
*/
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
//通过Confuguration对象去获取Mybatis相关配置信息, Environment对象包含了数据源和事务的配置
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//之前说了,从表面上来看,咱们是用sqlSession在执行sql语句, 实际呢,其实是通过excutor执行, excutor是对于Statement的封装
final Executor executor = configuration.newExecutor(tx, execType);
//关键看这儿,创建了一个DefaultSqlSession对象
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
3、通过SqlSession对象来进行数据库增删改查的操作
4、调用session.commit()提交事务
5、调用session.close()关闭会话
Mybatis的MapperProxy:
在mybatis中通过MapperProxy动态代理dao层,意思就是,当我们执行自己写的dao层中的方法的时候,其实对应的是mapperProxy在代理。
Mybatis中获取MapperProxy代理对象的过程:
1、通过sqlSession从Configuration中获取
/**
* sqlSession什么都不做,直接去configuration中找
*/
@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
2、Configuration中多mapper对象的操作
/**
*Configuration也不做处理 ,去找mapperRegistry去要
* @param type
* @param sqlSession
* @return
*/
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
3、MapperRegistry中对mapper对象的操作
/**
* 烂活净让我来做了,没法了,下面没人了,我不做谁来做
* @param type
* @param sqlSession
* @return
*/
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//能偷懒的就偷懒,俺把粗活交给MapperProxyFactory去做
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
//关键在这儿 生成一个mapperProxy对象
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
4、最终mapperProxy的生成是由MapperProxyFactory生成的
/**
* 别人虐我千百遍,我待别人如初恋
* @param mapperProxy
* @return
*/
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
//动态代理我们写的dao接口 (jdk动态代理 只实现接口)
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
通过动态代理 我们就可以方便使用dao层接口了:
UserDao userMapper = sqlSession.getMapper(UserDao.class);
User insertUser = new User();
使用Excutor执行sql语句:
sql的真正执行过程:
通过MapperProxyFactory获得的每一个MapperProxy对应一个dao层接口:
MapperProxy:
/**
* MapperProxy在执行时会触发此方法
*/
@Override
//java中的invoke反射方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//判断当前这个方法是那个类的方法
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
//二话不说,主要交给MapperMethod自己去管
return mapperMethod.execute(sqlSession, args);
}
-
我们知道这个MapperProxy就是一个InvocationHandler(他的作用是jdk创建动态代理时用的,不清楚动态代理,自己补习一下),也就是我们会根据当前的接口创建一个这个接口的动态代理对象,使用动态代理对象再利用反射调用实际对象的目标方法。
然而动态代理对象里面的方法都是Interface规定的。但是动态代理对象也能调用比如toString(),hashCode()等这些方法呀,这些方法是所有类从Object继承过来的。
所以这个判断的根本作用就是,如果利用动态代理对象调用的是toString,hashCode,getClass等这些从Object类继承过来的方法,就直接反射调用。如果调用的是接口规定的方法。我们就用MapperMethod来执行。
结论:
1)、method.getDeclaringClass用来判断当前这个方法是哪个类的方法。
2)、接口创建出的代理对象不仅有实现接口的方法,也有从Object继承过来的方法
3)、实现的接口的方法method.getDeclaringClass是接口类型,比如com.atguigu.dao.EmpDao
从Object类继承过来的方法类型是java.lang.Object类型
4)、如果是Object类继承来的方法,直接反射调用
如果是实现的接口规定的方法,利用Mybatis的MapperMethod调用
MapperMethod(mapper接口中的增删改查方法):
/**
* 看着代码不少,不过其实就是先判断CRUD类型,然后根据类型去选择到底执行sqlSession中的哪个方法,绕了一圈,又转回sqlSession了
* @param sqlSession
* @param args
* @return
*/
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
if (SqlCommandType.INSERT == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
} else if (SqlCommandType.UPDATE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
} else if (SqlCommandType.DELETE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
} else if (SqlCommandType.SELECT == command.getType()) {
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
} else {
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
又回到了SqlSession了,就是SqlSession中的增删改查(CRUD)方法,比如使用selectList方法来分析:
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
//CRUD实际上是交给Excetor去处理, excutor其实也只是穿了个马甲而已
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
通过一层层的调用最终会来到doQuery方法,随便找一个Excutor看看doQuery方法的实现:
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
//StatementHandler封装了Statement, 让 StatementHandler 去处理
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
接下来,咱们看看StatementHandler 的一个实现类 PreparedStatementHandler(这也是我们最常用的,封装的是PreparedStatement), 看看它使怎么去处理的:
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
//到此,原形毕露, PreparedStatement, 这个大家都已经滚瓜烂熟了吧
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
//结果交给了ResultSetHandler 去处理
return resultSetHandler.<E> handleResultSets(ps);
}
PreparedStatement 预编译声明,PreparedStatement是Statement的子接口
//Connection创建PreparedStatement对象时的SQL模板
String sql = “select * from tab_student where s_number=?”;
PreparedStatement pstmt = con.prepareStatement(sql);
//给?号赋值
pstmt.setString(1, “S_1001”);
ResultSet rs = pstmt.executeQuery();
rs.close();
pstmt.clearParameters();
pstmt.setString(1, “S_1002”);
rs = pstmt.executeQuery();
在使用Connection创建PreparedStatement对象时需要给出一个SQL模板,所谓SQL模板就是有“?”的SQL语句,其中“?”就是参数。
在得到PreparedStatement对象后,调用它的setXXX()方法为“?”赋值,这样就可以得到把模板变成一条完整的SQL语句,然后再调用PreparedStatement对象的executeQuery()方法获取ResultSet对象。
注意PreparedStatement对象独有的executeQuery()方法是没有参数的,而Statement的executeQuery()是需要参数(SQL语句)的。因为在创建PreparedStatement对象时已经让它与一条SQL模板绑定在一起了,所以在调用它的executeQuery()和executeUpdate()方法时就不再需要参数了。
PreparedStatement最大的好处就是在于重复使用同一模板,给予其不同的参数来重复的使用它。这才是真正提高效率的原因。
所以,建议大家在今后的开发中,无论什么情况,都去需要PreparedStatement,而不是使用Statement。
executeQuery();
在使用Connection创建PreparedStatement对象时需要给出一个SQL模板,所谓SQL模板就是有“?”的SQL语句,其中“?”就是参数。
在得到PreparedStatement对象后,调用它的setXXX()方法为“?”赋值,这样就可以得到把模板变成一条完整的SQL语句,然后再调用PreparedStatement对象的executeQuery()方法获取ResultSet对象。
注意PreparedStatement对象独有的executeQuery()方法是没有参数的,而Statement的executeQuery()是需要参数(SQL语句)的。因为在创建PreparedStatement对象时已经让它与一条SQL模板绑定在一起了,所以在调用它的executeQuery()和executeUpdate()方法时就不再需要参数了。
PreparedStatement最大的好处就是在于重复使用同一模板,给予其不同的参数来重复的使用它。这才是真正提高效率的原因。
所以,建议大家在今后的开发中,无论什么情况,都去需要PreparedStatement,而不是使用Statement。