文章声明:仅供个人学习
Mybatis 教程网址:
https://www.yiibai.com/mybatis/mybatis_print_out_sql.html
mybatis 项目举例
1 创建db_mybatis 的脚本
/*
SQLyog 企业版 - MySQL GUI v8.14
MySQL - 5.1.49-community : Database - db_mybatis
*********************************************************************
*/
/*!40101 SET NAMES utf8 */;
/*!40101 SET SQL_MODE=''*/;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`db_mybatis` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `db_mybatis`;
/*Table structure for table `t_student` */
DROP TABLE IF EXISTS `t_student`;
CREATE TABLE `t_student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
/*Data for the table `t_student` */
insert into `t_student`(`id`,`name`,`age`) values (1,'张三',10),(2,'李四',11);
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
目录结构
这种方式非常好
mybatis-config.xml 讲解
可以配置一个是开发环境一个是测试环境
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- <properties resource="jdbc.properties"/> -->
<properties>
<property name="jdbc.driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="jdbc.url" value="jdbc:mysql://localhost:3306/db_mybatis"/>
<property name="jdbc.username" value="root"/>
<property name="jdbc.password" value="123456"/>
</properties>
<!-- <typeAliases>
<typeAlias alias="Student" type="com.java1234.model.Student"/>
</typeAliases> -->
<typeAliases>
<package name="com.java1234.model"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</dataSource>
</environment>
<environment id="test">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</dataSource>
</environment>
</environments>
<mappers>
<!-- <mapper resource="com/java1234/mappers/StudentMapper.xml" /> -->
<!-- <mapper class="com.java1234.mappers.StudentMapper"/> -->
<package name="com.java1234.mappers"/>
</mappers>
</configuration>
sqlSession 专题讲解
Sqlsession意味着创建数据库会话。mybatis中使用SqlsessionFactory的openSession方法创建。
public class TestMyBatis {
public static SqlSessionFactory sqlSessionFactory = null;
static{
sqlSessionFactory = MyBatisUtil.getSqlsessionfactory();
}
/**
*
*author: lee by 2016年6月20日
*description:mybatis插入用户数据
*@param
*@return void
*/
public static void testAdd() {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
TbUserMapper userMapper = sqlSession.getMapper(TbUserMapper.class);
TbUser user = new TbUser("liybk", "liybk","186..","123");
userMapper.insertUser(user);
sqlSession.commit();// 这里一定要提交,不然数据进不去数据库中,只要是配置了事务就一定是事务提交。
} finally {
sqlSession.close();
}
}
}
源码展示
/**
*通过MyBatis操作数据库用到的是一个叫SqlSession的类,
*
*这个类是通过SqlSessionFactory产生的,一般建议在全局维护一个SqlSessionFactory就可以了。
*/
public class MyBatisUtil {
private final static SqlSessionFactory sqlSessionFactory;
static{
String resource = "mybatis-config.xml";
Reader reader = null;
try {
reader = Resources.getResourceAsReader(resource);
} catch (IOException e) {
System.out.println(e.getMessage());
}
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
}
public static SqlSessionFactory getSqlsessionfactory() {
return sqlSessionFactory;
}
}
SqlSessionFactory中的openSession是关键点,mybatis源码实现如下:
@Override
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
DefaultSqlsessionFactory
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
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();
}
}
mybatis创建sqlsession经过了以下几个主要步骤:
1) 从核心配置文件mybatis-config.xml中获取Environment(这里面是数据源);
2) 从Environment中取得DataSource;
3) 从Environment中取得TransactionFactory;
4) 从DataSource里获取数据库连接对象Connection;
5) 在取得的数据库连接上创建事务对象Transaction;
6) 创建Executor对象(该对象非常重要,事实上sqlsession的所有操作都是通过它完成的);
7) 创建sqlsession对象。
Executor的创建
Sqlsession打开数据库,真正完成交互的是Executor,Sqlsession对数据库的操作都是通过Executor来完成的。与Sqlsession一样,Executor也是动态创建的:
[java] view plain copy
- public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
- executorType = executorType == null ? defaultExecutorType : executorType;
- executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
- Executor executor;
- if (ExecutorType.BATCH == executorType) {
- executor = new BatchExecutor(this, transaction);
- } else if (ExecutorType.REUSE == executorType) {
- executor = new ReuseExecutor(this, transaction);
- } else {
- executor = new SimpleExecutor(this, transaction);
- }
- if (cacheEnabled) {
- executor = new CachingExecutor(executor);
- }
- executor = (Executor) interceptorChain.pluginAll(executor);
- return executor;
- }
可以看出,如果不开启cache的话,创建的Executor只是3中基础类型之一,BatchExecutor专门用于执行批量sql操作,ReuseExecutor会重用statement执行sql操作,SimpleExecutor只是简单执行sql没有什么特别的。开启cache的话(默认是开启的并且没有任何理由去关闭它),就会创建CachingExecutor,它以前面创建的Executor作为唯一参数。CachingExecutor在查询数据库前先查找缓存,若没找到的话调用delegate(就是构造时传入的Executor对象)从数据库查询,并将查询结果存入缓存中。
Executor对象是可以被插件拦截的,如果定义了针对Executor类型的插件,最终生成的Executor对象是被各个插件插入后的代理对象(关于插件会有后续章节专门介绍,敬请期待)。
Mapper
Mybatis官方手册建议通过mapper对象访问mybatis,因为使用mapper看起来更优雅,就像下面这样:
[java] view plain copy
- session = sqlSessionFactory.openSession();
- UserDao userDao= session.getMapper(UserDao.class);
- UserDto user =new UserDto();
- user.setUsername("iMbatis");
- user.setPassword("iMbatis");
- userDao.insertUser(user);
那么这个mapper到底是什么呢,它是如何创建的呢,它又是怎么与sqlsession等关联起来的呢?下面为你一一解答。
创建
表面上看mapper是在sqlsession里创建的,但实际创建它的地方是MapperRegistry:
[java] view plain copy
- public <T>T getMapper(Class<T> type, SqlSession sqlSession) {
- if (!knownMappers.contains(type))
- thrownewBindingException("Type " + type + " isnot known to the MapperRegistry.");
- try {
- returnMapperProxy.newMapperProxy(type, sqlSession);
- } catch (Exceptione) {
- thrownewBindingException("Error getting mapper instance. Cause: " + e, e);
- }
- }
可以看到,mapper是一个代理对象,它实现的接口就是传入的type,这就是为什么mapper对象可以通过接口直接访问。同时还可以看到,创建mapper代理对象时中传入了sqlsession对象,这样就把sqlsession也关联起来了。我们进一步看看当把type 和sqlsession 之后发生了什么
MapperProxy.newMapperProxy(type,sqlSession);背后发生了什么事情:
[java] view plain copy
- publicstatic <T>T newMapperProxy(Class<T> mapperInterface, SqlSession sqlSession) {
- ClassLoaderclassLoader = mapperInterface.getClassLoader();
- Class<?>[] interfaces = new Class[]{mapperInterface};
- MapperProxyproxy = new MapperProxy(sqlSession);
- return (T) Proxy.newProxyInstance(classLoader,interfaces, proxy);
- }
看起来没什么特别的,和其他代理类的创建一样,我们重点关注一下MapperProxy的invoke方法
MapperProxy的invoke
我们知道对被代理对象的方法的访问都会落实到代理者的invoke上来,MapperProxy的invoke如下:
[java] view plain copy
- public Objectinvoke(Object proxy, Method method, Object[] args) throws Throwable{
- if (method.getDeclaringClass()== Object.class) {
- return method.invoke(this, args);
- }
- finalClass<?> declaringInterface = findDeclaringInterface(proxy, method);
- finalMapperMethod mapperMethod = newMapperMethod(declaringInterface, method, sqlSession);
- final Objectresult = mapperMethod.execute(args);
- if (result ==null && method.getReturnType().isPrimitive()&& !method.getReturnType().equals(Void.TYPE)) {
- thrownewBindingException("Mapper method '" + method.getName() + "'(" + method.getDeclaringClass()
- + ") attempted toreturn null from a method with a primitive return type ("
- + method.getReturnType() + ").");
- }
- return result;
- }
可以看到invoke把执行权转交给了MapperMethod,我们来看看MapperMethod里又是怎么运作的:
[java] view plain copy
- public Objectexecute(Object[] args) {
- Objectresult = null;
- if(SqlCommandType.INSERT == type) {
- Objectparam = getParam(args);
- result= sqlSession.insert(commandName, param);
- } elseif(SqlCommandType.UPDATE == type) {
- Object param = getParam(args);
- result= sqlSession.update(commandName, param);
- } elseif(SqlCommandType.DELETE == type) {
- Objectparam = getParam(args);
- result= sqlSession.delete(commandName, param);
- } elseif(SqlCommandType.SELECT == type) {
- if (returnsVoid &&resultHandlerIndex != null) {
- executeWithResultHandler(args);
- } elseif (returnsList) {
- result = executeForList(args);
- } elseif (returnsMap) {
- result = executeForMap(args);
- } else {
- Object param = getParam(args);
- result = sqlSession.selectOne(commandName, param);
- }
- } else {
- thrownewBindingException("Unknown execution method for: " + commandName);
- }
- return result;
- }
可以看到,MapperMethod就像是一个分发者,他根据参数和返回值类型选择不同的sqlsession方法来执行。这样mapper对象与sqlsession就真正的关联起来了。
Executor
前面提到过,sqlsession只是一个门面,真正发挥作用的是executor,对sqlsession方法的访问最终都会落到executor的相应方法上去。Executor分成两大类,一类是CacheExecutor,另一类是普通Executor。Executor的创建前面已经介绍了,下面介绍下他们的功能:
CacheExecutor
CacheExecutor有一个重要属性delegate,它保存的是某类普通的Executor,值在构照时传入。执行数据库update操作时,它直接调用delegate的update方法,执行query方法时先尝试从cache中取值,取不到再调用delegate的查询方法,并将查询结果存入cache中。代码如下:
[java] view plain copy
- public Listquery(MappedStatement ms, Object parameterObject, RowBounds rowBounds,ResultHandler resultHandler) throws SQLException {
- if (ms != null) {
- Cachecache = ms.getCache();
- if (cache != null) {
- flushCacheIfRequired(ms);
- cache.getReadWriteLock().readLock().lock();
- try {
- if (ms.isUseCache() && resultHandler ==null) {
- CacheKey key = createCacheKey(ms, parameterObject, rowBounds);
- final List cachedList = (List)cache.getObject(key);
- if (cachedList != null) {
- returncachedList;
- } else {
- List list = delegate.query(ms,parameterObject, rowBounds, resultHandler);
- tcm.putObject(cache,key, list);
- return list;
- }
- } else {
- returndelegate.query(ms,parameterObject, rowBounds, resultHandler);
- }
- } finally {
- cache.getReadWriteLock().readLock().unlock();
- }
- }
- }
- returndelegate.query(ms,parameterObject, rowBounds, resultHandler);
- }
普通Executor
普通Executor有3类,他们都继承于BaseExecutor,BatchExecutor专门用于执行批量sql操作,ReuseExecutor会重用statement执行sql操作,SimpleExecutor只是简单执行sql没有什么特别的。下面以SimpleExecutor为例:
[java] view plain copy
- public ListdoQuery(MappedStatement ms, Object parameter, RowBounds rowBounds,ResultHandler resultHandler) throws SQLException {
- Statementstmt = null;
- try {
- Configuration configuration = ms.getConfiguration();
- StatementHandler handler = configuration.newStatementHandler(this, ms,parameter, rowBounds,resultHandler);
- stmt =prepareStatement(handler);
- returnhandler.query(stmt, resultHandler);
- } finally {
- closeStatement(stmt);
- }
- }
可以看出,Executor本质上也是个甩手掌柜,具体的事情原来是StatementHandler来完成的。
StatementHandler
当Executor将指挥棒交给StatementHandler后,接下来的工作就是StatementHandler的事了。我们先看看StatementHandler是如何创建的。
创建
[java] view plain copy
- publicStatementHandler newStatementHandler(Executor executor, MappedStatementmappedStatement,
- ObjectparameterObject, RowBounds rowBounds, ResultHandler resultHandler) {
- StatementHandler statementHandler = newRoutingStatementHandler(executor, mappedStatement,parameterObject,rowBounds, resultHandler);
- statementHandler= (StatementHandler) interceptorChain.pluginAll(statementHandler);
- returnstatementHandler;
- }
可以看到每次创建的StatementHandler都是RoutingStatementHandler,它只是一个分发者,他一个属性delegate用于指定用哪种具体的StatementHandler。可选的StatementHandler有SimpleStatementHandler、PreparedStatementHandler和CallableStatementHandler三种。选用哪种在mapper配置文件的每个statement里指定,默认的是PreparedStatementHandler。同时还要注意到StatementHandler是可以被拦截器拦截的,和Executor一样,被拦截器拦截后的对像是一个代理对象。由于mybatis没有实现数据库的物理分页,众多物理分页的实现都是在这个地方使用拦截器实现的,本文作者也实现了一个分页拦截器,在后续的章节会分享给大家,敬请期待。
初始化
StatementHandler创建后需要执行一些初始操作,比如statement的开启和参数设置、对于PreparedStatement还需要执行参数的设置操作等。代码如下:
[java] view plain copy
- private StatementprepareStatement(StatementHandler handler) throwsSQLException {
- Statementstmt;
- Connectionconnection = transaction.getConnection();
- stmt =handler.prepare(connection);
- handler.parameterize(stmt);
- return stmt;
- }
statement的开启和参数设置没什么特别的地方,handler.parameterize倒是可以看看是怎么回事。handler.parameterize通过调用ParameterHandler的setParameters完成参数的设置,ParameterHandler随着StatementHandler的创建而创建,默认的实现是DefaultParameterHandler:
[java] view plain copy
- publicParameterHandler newParameterHandler(MappedStatement mappedStatement, ObjectparameterObject, BoundSql boundSql) {
- ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement,parameterObject,boundSql);
- parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
- returnparameterHandler;
- }
同Executor和StatementHandler一样,ParameterHandler也是可以被拦截的。
参数设置
DefaultParameterHandler里设置参数的代码如下:
[java] view plain copy
- publicvoidsetParameters(PreparedStatement ps) throwsSQLException {
- ErrorContext.instance().activity("settingparameters").object(mappedStatement.getParameterMap().getId());
- List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
- if(parameterMappings != null) {
- MetaObject metaObject = parameterObject == null ? null :configuration.newMetaObject(parameterObject);
- for (int i = 0; i< parameterMappings.size(); i++) {
- ParameterMapping parameterMapping = parameterMappings.get(i);
- if(parameterMapping.getMode() != ParameterMode.OUT) {
- Object value;
- String propertyName = parameterMapping.getProperty();
- PropertyTokenizer prop = newPropertyTokenizer(propertyName);
- if (parameterObject == null) {
- value = null;
- } elseif (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())){
- value = parameterObject;
- } elseif (boundSql.hasAdditionalParameter(propertyName)){
- value = boundSql.getAdditionalParameter(propertyName);
- } elseif(propertyName.startsWith(ForEachSqlNode.ITEM_PREFIX)
- && boundSql.hasAdditionalParameter(prop.getName())){
- value = boundSql.getAdditionalParameter(prop.getName());
- if (value != null) {
- value = configuration.newMetaObject(value).getValue(propertyName.substring(prop.getName().length()));
- }
- } else {
- value = metaObject == null ? null :metaObject.getValue(propertyName);
- }
- TypeHandler typeHandler = parameterMapping.getTypeHandler();
- if (typeHandler == null) {
- thrownew ExecutorException("Therewas no TypeHandler found for parameter " + propertyName + " of statement " + mappedStatement.getId());
- }
- typeHandler.setParameter(ps, i + 1, value,parameterMapping.getJdbcType());
- }
- }
- }
- }
这里面最重要的一句其实就是最后一句代码,它的作用是用合适的TypeHandler完成参数的设置。那么什么是合适的TypeHandler呢,它又是如何决断出来的呢?BaseStatementHandler的构造方法里有这么一句:
this.boundSql= mappedStatement.getBoundSql(parameterObject);
它触发了sql 的解析,在解析sql的过程中,TypeHandler也被决断出来了,决断的原则就是根据参数的类型和参数对应的JDBC类型决定使用哪个TypeHandler。比如:参数类型是String的话就用StringTypeHandler,参数类型是整数的话就用IntegerTypeHandler等。
参数设置完毕后,执行数据库操作(update或query)。如果是query最后还有个查询结果的处理过程。
结果处理
结果处理使用ResultSetHandler来完成,默认的ResultSetHandler是FastResultSetHandler,它在创建StatementHandler时一起创建,代码如下:
[java] view plain copy
- publicResultSetHandler newResultSetHandler(Executor executor, MappedStatementmappedStatement,
- RowBoundsrowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSqlboundSql) {
- ResultSetHandler resultSetHandler =mappedStatement.hasNestedResultMaps() ? newNestedResultSetHandler(executor, mappedStatement, parameterHandler,resultHandler, boundSql, rowBounds): new FastResultSetHandler(executor,mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
- resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
- returnresultSetHandler;
- }
可以看出ResultSetHandler也是可以被拦截的,可以编写自己的拦截器改变ResultSetHandler的默认行为。
[java] view plain copy
- ResultSetHandler内部一条记录一条记录的处理,在处理每条记录的每一列时会调用TypeHandler转换结果,如下:
- protectedbooleanapplyAutomaticMappings(ResultSet rs, List<String> unmappedColumnNames,MetaObject metaObject) throws SQLException {
- booleanfoundValues = false;
- for (StringcolumnName : unmappedColumnNames) {
- final Stringproperty = metaObject.findProperty(columnName);
- if (property!= null) {
- final ClasspropertyType =metaObject.getSetterType(property);
- if (typeHandlerRegistry.hasTypeHandler(propertyType)) {
- final TypeHandler typeHandler = typeHandlerRegistry.getTypeHandler(propertyType);
- final Object value = typeHandler.getResult(rs,columnName);
- if (value != null) {
- metaObject.setValue(property, value);
- foundValues = true;
- }
- }
- }
- }
- returnfoundValues;
- }
从代码里可以看到,决断TypeHandler使用的是结果参数的属性类型。因此我们在定义作为结果的对象的属性时一定要考虑与数据库字段类型的兼容性。
public class TestMyBatis {
public static SqlSessionFactory sqlSessionFactory = null;
static{
sqlSessionFactory = MyBatisUtil.getSqlsessionfactory();
}
/**
*
*author: lee by 2016年6月20日
*description:mybatis插入用户数据
*@param
*@return void
*/
public static void testAdd() {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
TbUserMapper userMapper = sqlSession.getMapper(TbUserMapper.class);
TbUser user = new TbUser("liybk", "liybk","186..","123");
userMapper.insertUser(user);
sqlSession.commit();// 这里一定要提交,不然数据进不去数据库中
} finally {
sqlSession.close();
}
}
}
SqlSessionFactoryUtil.java
解释:
SqlSessionFactoryBuilder- SqlSessionFactorysql Mapper-sqlSession
sqlSession 相当于是connection
类的详细信息:
package com.java1234.util;
import java.io.InputStream;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
public class SqlSessionFactoryUtil {
private static SqlSessionFactory sqlSessionFactory;
public static SqlSessionFactory getSqlSessionFactory(){
if(sqlSessionFactory==null){InputStream inputStream=null;
try{
inputStream=Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
}catch(Exception e){
e.printStackTrace();
}
}
return sqlSessionFactory;
}
public static SqlSession openSession(){
return getSqlSessionFactory().openSession();
}
}
增删改查
BLOB 大数据量的操作
数据库结构
/*
SQLyog 企业版 - MySQL GUI v8.14
MySQL - 5.1.49-community : Database - db_mybatis
*********************************************************************
*/
/*!40101 SET NAMES utf8 */;
/*!40101 SET SQL_MODE=''*/;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`db_mybatis` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `db_mybatis`;
/*Table structure for table `t_address` */
DROP TABLE IF EXISTS `t_address`;
CREATE TABLE `t_address` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`sheng` varchar(20) DEFAULT NULL,
`shi` varchar(20) DEFAULT NULL,
`qu` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
/*Data for the table `t_address` */
insert into `t_address`(`id`,`sheng`,`shi`,`qu`) values (1,'江苏省','苏州市','姑苏区'),(2,'江苏省','南京市','鼓楼区');
/*Table structure for table `t_grade` */
DROP TABLE IF EXISTS `t_grade`;
CREATE TABLE `t_grade` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`gradeName` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
/*Data for the table `t_grade` */
insert into `t_grade`(`id`,`gradeName`) values (1,'大学一年级'),(2,'大学二年级');
/*Table structure for table `t_student` */
DROP TABLE IF EXISTS `t_student`;
CREATE TABLE `t_student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`pic` longblob,
`remark` longtext,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
toString() 方法不含有pic 因为pic 要用流来处理。
<insert id="insertStudent" parameterType="Student" flushCache="true">
insert into t_student values(null,#{name},#{age},#{pic},#{remark});
</insert>
@Test
public void testInsertStudent(){
logger.info("添加学生");
Student student=new Student();
student.setName("张三4");
student.setAge(14);
student.setRemark("很长的本文...");
byte []pic=null;
try{
//把文件采用的二进制流
File file=new File("e://1.png");
InputStream inputStream=new FileInputStream(file);
pic=new byte[inputStream.available()];
inputStream.read(pic);//把读到的二进制放到数组当中去
inputStream.close();
}catch(Exception e){
e.printStackTrace();
}
student.setPic(pic);
studentMapper.insertStudent(student);
sqlSession.commit();
}
2018-06-29 23:34:51,087 [main] INFO [com.java1234.service.StudentTest2] - 添加学生
2018-06-29 23:34:51,206 [main] DEBUG [com.java1234.mappers.StudentMapper.insertStudent] - ==> Preparing: insert into t_student values(null,?,?,?,?);
2018-06-29 23:34:51,237 [main] DEBUG [com.java1234.mappers.StudentMapper.insertStudent] - ==> Parameters: 张三4(String), 14(Integer), [B@5bcbab86(byte[]), 很长的本文...(String)
2018-06-29 23:34:51,241 [main] DEBUG [com.java1234.mappers.StudentMapper.insertStudent] - <== Updates: 1
练习就是把图片存进到数据库中
student.setPic(pic); BLOB 放的数据是二进制数组 ,也可以是二进制字符串 student.setPic(pic.toString())
存进去是一个二进制的图片,那么取出来的也应该也是图片
<select id="getStudentById" parameterType="Integer" resultType="Student">
select * from t_student where id=#{id}
</select>
@Test
public void testGetStudentById(){
logger.info("通过ID查找学生");
Student student=studentMapper.getStudentById(6);
System.out.println(student);
byte []pic=student.getPic();
try{
File file=new File("e://2.png");//文件输出流就是把数据库中存的二进制文件取出来就是形成的就是图片
OutputStream outputStream=new FileOutputStream(file);
outputStream.write(pic);
outputStream.close();
}catch(Exception e){
e.printStackTrace();
}
}
2018-06-29 23:52:44,465 [main] INFO [com.java1234.service.StudentTest2] - 通过ID查找学生
2018-06-29 23:52:44,470 [main] DEBUG [com.java1234.mappers.StudentMapper] - Cache Hit Ratio [com.java1234.mappers.StudentMapper]: 0.0
2018-06-29 23:52:44,591 [main] DEBUG [com.java1234.mappers.StudentMapper.getStudentById] - ==> Preparing: select * from t_student where id=?
2018-06-29 23:52:44,623 [main] DEBUG [com.java1234.mappers.StudentMapper.getStudentById] - ==> Parameters: 6(Integer)
2018-06-29 23:52:44,644 [main] DEBUG [com.java1234.mappers.StudentMapper.getStudentById] - <== Total: 1
Student [id=6, name=张三4, age=14, remark=很长的本文...]
把取出来的二进制形成图片2.png
如果采用这两种数据的时候,从数据库取出来的二进制转化为图片,实体类一定要序列化
总结:
<resultMap type="Student" id="StudentResult"> 如果type 是Student 则,Student 里面的非级联属性可以写也可以不写就是可以写成下面这个样子
<resultMap type="Student" id="StudentResult">
<association property="address" column="addressId" select="com.java1234.mappers.AddressMapper.findById"></association>
<association property="grade" column="gradeId" select="com.java1234.mappers.GradeMapper.findById"></association>
</resultMap>
type="Student" 是不能去掉的,去掉就是报错的
<resultMap type="Student" id="StudentResult">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="age" column="age"/>
<association property="address" column="addressId" select="com.java1234.mappers.AddressMapper.findById"></association>
<association property="grade" column="gradeId" select="com.java1234.mappers.GradeMapper.findById"></association>
</resultMap>
mybatis分页
逻辑分页(如果是想用10条数据的话,就是先取出来100条数据放到内存当中,然后从内存中取10条,如果数据量特别大的情况是不行的,
是能是适用于小项目
@Test
public void testFindStudent(){
logger.info("查询学生");
int offset=0,limit=5;
RowBounds rowBounds=new RowBounds(offset,limit);
List<Student> studentList=studentMapper.findStudents(rowBounds);
for(Student student:studentList){
System.out.println(student);
}
}
没有参数传入进来
<select id="findStudents" resultType="Student">
select * from t_student
</select>
2018-07-01 16:18:14,720 [main] INFO [com.java1234.service.StudentTest3] - 查询学生
2018-07-01 16:18:14,724 [main] DEBUG [com.java1234.mappers.StudentMapper] - Cache Hit Ratio [com.java1234.mappers.StudentMapper]: 0.0
2018-07-01 16:18:14,839 [main] DEBUG [com.java1234.mappers.StudentMapper.findStudents] - ==> Preparing: select * from t_student
2018-07-01 16:18:14,871 [main] DEBUG [com.java1234.mappers.StudentMapper.findStudents] - ==> Parameters:
Student [id=6, name=张三4, age=14, remark=很长的本文...]
Student [id=7, name=张三4, age=14, remark=很长的本文...]
Student [id=8, name=张三4, age=14, remark=很长的本文...]
Student [id=9, name=张三4, age=14, remark=很长的本文...]
Student [id=10, name=张三4, age=14, remark=很长的本文...]
真正实现分页(采用map的方式)
@Test
public void testFindStudent2(){
logger.info("查询学生");
Map<String,Object> map=new HashMap<String,Object>();
map.put("start", 3);
map.put("size", 3);
List<Student> studentList=studentMapper.findStudents2(map);
for(Student student:studentList){
System.out.println(student);
}
<select id="findStudents2" parameterType="Map" resultMap="StudentResult">
select * from t_student
<if test="start!=null and size!=null">
limit #{start},#{size}
</if>
</select>
日志
2018-07-01 16:19:31,833 [main] INFO [com.java1234.service.StudentTest3] - 查询学生
2018-07-01 16:19:31,941 [main] DEBUG [com.java1234.mappers.StudentMapper] - Cache Hit Ratio [com.java1234.mappers.StudentMapper]: 0.0
2018-07-01 16:19:32,068 [main] DEBUG [com.java1234.mappers.StudentMapper.findStudents2] - ==> Preparing: select * from t_student limit ?,?
2018-07-01 16:19:32,101 [main] DEBUG [com.java1234.mappers.StudentMapper.findStudents2] - ==> Parameters: 3(Integer), 3(Integer)
2018-07-01 16:19:32,117 [main] DEBUG [com.java1234.mappers.StudentMapper.findStudents2] - <== Total: 3
Student [id=9, name=张三4, age=14, remark=很长的本文...]
Student [id=10, name=张三4, age=14, remark=很长的本文...]
Student [id=11, name=张三4, age=14, remark=很长的本文...]
mybatis 当中的缓存
高速缓存服务器(redis,mongodb)
原理:主存有M块分区,缓存有C块分区,M>>C缓存是从主存那里复制过来的,从哪里复制过来的会有标记
mybatis 只是提供缓存的配置,重点是提高缓存的命中率
命中率=缓存读取的信息量/cpu访问的总信息量
效率(e):缓存读取的信息的时间/cpu读取信息的总时间*100%
<!--
1,size:表示缓存cache中能容纳的最大元素数。默认是1024;
2,flushInterval:定义缓存刷新周期,以毫秒计;
3,eviction:定义缓存的移除机制;默认是LRU(least recently userd,最近最少使用),还有FIFO(first in first out,先进先出)
4,readOnly:默认值是false,假如是true的话,缓存只能读。
-->
<cache size="1024" flushInterval="60000" eviction="LRU" readOnly="false"/>
<!-- flushCache="false" 是否清空缓存,false 就是不清空缓存-->
<select id="findStudents" resultType="Student" useCache="true" flushCache="false">
select * from t_student
</select>
通过源码分析MyBatis的缓存
MyBatis缓存的介绍:
MyBatis支持声明式数据缓存(declarative data caching)。当一条SQL语句被标记为“可缓存”后,首次执行它时从数据库获取的所有数据会被存储在一段高速缓存中,今后执行这条语句时就会从高速缓存中读取结果,而不是再次命中数据库。MyBatis提供了默认下基于Java HashMap的缓存实现,以及用于与OSCache、Ehcache、Hazelcast和Memcached连接的默认连接器。MyBatis还提供API供其他缓存实现使用。
重点的那句话就是:MyBatis执行SQL语句之后,这条语句就是被缓存,以后再执行这条语句的时候,会直接从缓存中拿结果,而不是再次执行SQL
这也就是大家常说的MyBatis一级缓存,一级缓存的作用域scope是SqlSession。
MyBatis同时还提供了一种全局作用域global scope的缓存,这也叫做二级缓存,也称作全局缓存。
一级缓存
测试
同个session进行两次相同查询:
@Test
public void test() {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
User user = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
log.debug(user);
User user2 = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
log.debug(user2);
} finally {
sqlSession.close();
}
}
MyBatis只进行1次数据库查询:
==> Preparing: select * from USERS WHERE ID = ?
==> Parameters: 1(Integer)
<== Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
同个session进行两次不同的查询:
@Test
public void test() {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
User user = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
log.debug(user);
User user2 = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 2);
log.debug(user2);
} finally {
sqlSession.close();
}
}
MyBatis进行两次数据库查询:
==> Preparing: select * from USERS WHERE ID = ?
==> Parameters: 1(Integer)
<== Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
==> Preparing: select * from USERS WHERE ID = ?
==> Parameters: 2(Integer)
<== Total: 1
User{id=2, name='FFF', age=50, birthday=Sat Dec 06 17:12:01 CST 2014}
不同session,进行相同查询:
@Test
public void test() {
SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
try {
User user = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
log.debug(user);
User user2 = (User)sqlSession2.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
log.debug(user2);
} finally {
sqlSession.close();
sqlSession2.close();
}
}
MyBatis进行了两次数据库查询:
==> Preparing: select * from USERS WHERE ID = ?
==> Parameters: 1(Integer)
<== Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
==> Preparing: select * from USERS WHERE ID = ?
==> Parameters: 1(Integer)
<== Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
同个session,查询之后更新数据,再次查询相同的语句:
@Test
public void test() {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
User user = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
log.debug(user);
user.setAge(100);
sqlSession.update("org.format.mybatis.cache.UserMapper.update", user);
User user2 = (User)sqlSession.selectOne("org.format.mybatis.cache.UserMapper.getById", 1);
log.debug(user2);
sqlSession.commit();
} finally {
sqlSession.close();
}
}
更新操作之后缓存会被清除:
==> Preparing: select * from USERS WHERE ID = ?
==> Parameters: 1(Integer)
<== Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
==> Preparing: update USERS SET NAME = ? , AGE = ? , BIRTHDAY = ? where ID = ?
==> Parameters: format(String), 23(Integer), 2014-10-12 23:20:13.0(Timestamp), 1(Integer)
<== Updates: 1
==> Preparing: select * from USERS WHERE ID = ?
==> Parameters: 1(Integer)
<== Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
很明显,结果验证了一级缓存的概念,在同个SqlSession中,查询语句相同的sql会被缓存,但是一旦执行新增或更新或删除操作,缓存就会被清除
源码分析
在分析MyBatis的一级缓存之前,我们先简单看下MyBatis中几个重要的类和接口:
org.apache.ibatis.session.Configuration类:MyBatis全局配置信息类
org.apache.ibatis.session.SqlSessionFactory接口:操作SqlSession的工厂接口,具体的实现类是DefaultSqlSessionFactory
org.apache.ibatis.session.SqlSession接口:执行sql,管理事务的接口,具体的实现类是DefaultSqlSession
org.apache.ibatis.executor.Executor接口:sql执行器,SqlSession执行sql最终是通过该接口实现的,常用的实现类有SimpleExecutor和CachingExecutor,这些实现类都使用了装饰者设计模式
一级缓存的作用域是SqlSession,那么我们就先看一下SqlSession的select过程:
这是DefaultSqlSession(SqlSession接口实现类,MyBatis默认使用这个类)的selectList源码(我们例子上使用的是selectOne方法,调用selectOne方法最终会执行selectList方法):
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
return result;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
我们看到SqlSession最终会调用Executor接口的方法。
接下来我们看下DefaultSqlSession中的executor接口属性具体是哪个实现类。
DefaultSqlSession的构造过程(DefaultSqlSessionFactory内部):
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType, autoCommit);
return new DefaultSqlSession(configuration, executor);
} 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();
}
}
我们看到DefaultSqlSessionFactory构造DefaultSqlSession的时候,Executor接口的实现类是由Configuration构造的:
public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor, autoCommit);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
Executor根据ExecutorType的不同而创建,最常用的是SimpleExecutor,本文的例子也是创建这个实现类。 最后我们发现如果cacheEnabled这个属性为true的话,那么executor会被包一层装饰器,这个装饰器是CachingExecutor。其中cacheEnabled这个属性是mybatis总配置文件中settings节点中cacheEnabled子节点的值,默认就是true,也就是说我们在mybatis总配置文件中不配cacheEnabled的话,它也是默认为打开的。
现在,问题就剩下一个了,CachingExecutor执行sql的时候到底做了什么?
带着这个问题,我们继续走下去(CachingExecutor的query方法):
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
if (!dirty) {
cache.getReadWriteLock().readLock().lock();
try {
@SuppressWarnings("unchecked")
List<E> cachedList = (List<E>) cache.getObject(key);
if (cachedList != null) return cachedList;
} finally {
cache.getReadWriteLock().readLock().unlock();
}
}
List<E> list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578. Query must be not synchronized to prevent deadlocks
return list;
}
}
return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
其中Cache cache = ms.getCache();这句代码中,这个cache实际上就是个二级缓存,由于我们没有开启二级缓存(二级缓存的内容下面会分析),因此这里执行了最后一句话。这里的delegate也就是SimpleExecutor,SimpleExecutor没有Override父类的query方法,因此最终执行了SimpleExecutor的父类BaseExecutor的query方法。
所以一级缓存最重要的代码就是BaseExecutor的query方法!
BaseExecutor的属性localCache是个PerpetualCache类型的实例,PerpetualCache类是实现了MyBatis的Cache缓存接口的实现类之一,内部有个Map类型的属性用来存储缓存数据。 这个localCache的类型在BaseExecutor内部是写死的。 这个localCache就是一级缓存!
接下来我们看下为何执行新增或更新或删除操作,一级缓存就会被清除这个问题。
首先MyBatis处理新增或删除的时候,最终都是调用update方法,也就是说新增或者删除操作在MyBatis眼里都是一个更新操作。
我们看下DefaultSqlSession的update方法:
public int update(String statement, Object parameter) {
try {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
很明显,这里调用了CachingExecutor的update方法:
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
这里的flushCacheIfRequired方法清除的是二级缓存,我们之后会分析。 CachingExecutor委托给了(之前已经分析过)SimpleExecutor的update方法,SimpleExecutor没有Override父类BaseExecutor的update方法,因此我们看BaseExecutor的update方法:
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) throw new ExecutorException("Executor was closed.");
clearLocalCache();
return doUpdate(ms, parameter);
}
我们看到了关键的一句代码: clearLocalCache(); 进去看看:
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
没错,就是这条,sqlsession没有关闭的话,进行新增、删除、修改操作的话就是清除一级缓存,也就是SqlSession的缓存。
二级缓存
二级缓存的作用域是全局,换句话说,二级缓存已经脱离SqlSession的控制了。
在测试二级缓存之前,我先把结论说一下:
二级缓存的作用域是全局的,二级缓存在SqlSession关闭或提交之后才会生效。
在分析MyBatis的二级缓存之前,我们先简单看下MyBatis中一个关于二级缓存的类(其他相关的类和接口之前已经分析过):
org.apache.ibatis.mapping.MappedStatement:
MappedStatement类在Mybatis框架中用于表示XML文件中一个sql语句节点,即一个<select />、<update />或者<insert />标签。Mybatis框架在初始化阶段会对XML配置文件进行读取,将其中的sql语句节点对象化为一个个MappedStatement对象。
配置
二级缓存跟一级缓存不同,一级缓存不需要配置任何东西,且默认打开。 二级缓存就需要配置一些东西。
本文就说下最简单的配置,在mapper文件上加上这句配置即可:
<cache/>
其实二级缓存跟3个配置有关:
- mybatis全局配置文件中的setting中的cacheEnabled需要为true(默认为true,不设置也行)
- mapper配置文件中需要加入<cache>节点
- mapper配置文件中的select节点需要加上属性useCache需要为true(默认为true,不设置也行)
测试
不同SqlSession,查询相同语句,第一次查询之后commit SqlSession:
@Test
public void testCache2() {
SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
try {
String sql = "org.format.mybatis.cache.UserMapper.getById";
User user = (User)sqlSession.selectOne(sql, 1);
log.debug(user);
// 注意,这里一定要提交。 不提交还是会查询两次数据库
sqlSession.commit();
User user2 = (User)sqlSession2.selectOne(sql, 1);
log.debug(user2);
} finally {
sqlSession.close();
sqlSession2.close();
}
}
MyBatis仅进行了一次数据库查询:
==> Preparing: select * from USERS WHERE ID = ?
==> Parameters: 1(Integer)
<== Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
不同SqlSession,查询相同语句,第一次查询之后close SqlSession:
@Test
public void testCache2() {
SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
try {
String sql = "org.format.mybatis.cache.UserMapper.getById";
User user = (User)sqlSession.selectOne(sql, 1);
log.debug(user);
sqlSession.close();
User user2 = (User)sqlSession2.selectOne(sql, 1);
log.debug(user2);
} finally {
sqlSession2.close();
}
}
MyBatis仅进行了一次数据库查询:
==> Preparing: select * from USERS WHERE ID = ?
==> Parameters: 1(Integer)
<== Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
不同SqlSesson,查询相同语句。 第一次查询之后SqlSession不提交:
@Test
public void testCache2() {
SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
try {
String sql = "org.format.mybatis.cache.UserMapper.getById";
User user = (User)sqlSession.selectOne(sql, 1);
log.debug(user);
User user2 = (User)sqlSession2.selectOne(sql, 1);
log.debug(user2);
} finally {
sqlSession.close();
sqlSession2.close();
}
}
MyBatis执行了两次数据库查询:
==> Preparing: select * from USERS WHERE ID = ?
==> Parameters: 1(Integer)
<== Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
==> Preparing: select * from USERS WHERE ID = ?
==> Parameters: 1(Integer)
<== Total: 1
User{id=1, name='format', age=23, birthday=Sun Oct 12 23:20:13 CST 2014}
源码分析
我们从在mapper文件中加入的<cache/>中开始分析源码,关于MyBatis的SQL解析请参考另外一篇博客Mybatis解析动态sql原理分析。接下来我们看下这个cache的解析:
XMLMappedBuilder(解析每个mapper配置文件的解析类,每一个mapper配置都会实例化一个XMLMapperBuilder类)的解析方法:
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
我们看到了解析cache的那段代码:
private void cacheElement(XNode context) throws Exception {
if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
Properties props = context.getChildrenAsProperties();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);
}
}
解析完cache标签之后会使用builderAssistant的userNewCache方法,这里的builderAssistant是一个MapperBuilderAssistant类型的帮助类,每个XMLMappedBuilder构造的时候都会实例化这个属性,MapperBuilderAssistant类内部有个Cache类型的currentCache属性,这个属性也就是mapper配置文件中cache节点所代表的值:
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
Properties props) {
typeClass = valueOrDefault(typeClass, PerpetualCache.class);
evictionClass = valueOrDefault(evictionClass, LruCache.class);
Cache cache = new CacheBuilder(currentNamespace)
.implementation(typeClass)
.addDecorator(evictionClass)
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
ok,现在mapper配置文件中的cache节点被解析到了XMLMapperBuilder实例中的builderAssistant属性中的currentCache值里。
接下来XMLMapperBuilder会解析select节点,解析select节点的时候使用XMLStatementBuilder进行解析(也包括其他insert,update,delete节点):
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) return;
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? new Jdbc3KeyGenerator() : new NoKeyGenerator();
}
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
这段代码前面都是解析一些标签的属性,我们看到了最后一行使用builderAssistant添加MappedStatement,其中builderAssistant属性是构造XMLStatementBuilder的时候通过XMLMappedBuilder传入的,我们继续看builderAssistant的addMappedStatement方法:
进入setStatementCache:
private void setStatementCache(
boolean isSelect,
boolean flushCache,
boolean useCache,
Cache cache,
MappedStatement.Builder statementBuilder) {
flushCache = valueOrDefault(flushCache, !isSelect);
useCache = valueOrDefault(useCache, isSelect);
statementBuilder.flushCacheRequired(flushCache);
statementBuilder.useCache(useCache);
statementBuilder.cache(cache);
}
最终mapper配置文件中的<cache/>被设置到了XMLMapperBuilder的builderAssistant属性中,XMLMapperBuilder中使用XMLStatementBuilder遍历CRUD节点,遍历CRUD节点的时候将这个cache节点设置到这些CRUD节点中,这个cache就是所谓的二级缓存!
接下来我们回过头来看查询的源码,CachingExecutor的query方法:
进入TransactionalCacheManager的putObject方法:
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
private TransactionalCache getTransactionalCache(Cache cache) {
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
TransactionalCache的putObject方法:
public void putObject(Object key, Object object) {
entriesToRemoveOnCommit.remove(key);
entriesToAddOnCommit.put(key, new AddEntry(delegate, key, object));
}
我们看到,数据被加入到了entriesToAddOnCommit中,这个entriesToAddOnCommit是什么东西呢,它是TransactionalCache的一个Map属性:
private Map<Object, AddEntry> entriesToAddOnCommit;
AddEntry是TransactionalCache内部的一个类:
private static class AddEntry {
private Cache cache;
private Object key;
private Object value;
public AddEntry(Cache cache, Object key, Object value) {
this.cache = cache;
this.key = key;
this.value = value;
}
public void commit() {
cache.putObject(key, value);
}
}
好了,现在我们发现使用二级缓存之后:查询数据的话,先从二级缓存中拿数据,如果没有的话,去一级缓存中拿,一级缓存也没有的话再查询数据库。有了数据之后在丢到TransactionalCache这个对象的entriesToAddOnCommit属性中。
接下来我们来验证为什么SqlSession commit或close之后,二级缓存才会生效这个问题。
DefaultSqlSession的commit方法:
public void commit(boolean force) {
try {
executor.commit(isCommitOrRollbackRequired(force));
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
CachingExecutor的commit方法:
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
dirty = false;
}
tcm.commit即 TransactionalCacheManager的commit方法:
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
TransactionalCache的commit方法:
public void commit() {
delegate.getReadWriteLock().writeLock().lock();
try {
if (clearOnCommit) {
delegate.clear();
} else {
for (RemoveEntry entry : entriesToRemoveOnCommit.values()) {
entry.commit();
}
}
for (AddEntry entry : entriesToAddOnCommit.values()) {
entry.commit();
}
reset();
} finally {
delegate.getReadWriteLock().writeLock().unlock();
}
}
发现调用了AddEntry的commit方法:
public void commit() {
cache.putObject(key, value);
}
发现了! AddEntry的commit方法会把数据丢到cache中,也就是丢到二级缓存中!
关于为何调用close方法后,二级缓存才会生效,因为close方法内部会调用commit方法。本文就不具体说了。 读者有兴趣的话看一看源码就知道为什么了。
其他
Cache接口简介
org.apache.ibatis.cache.Cache是MyBatis的缓存接口,想要实现自定义的缓存需要实现这个接口。
MyBatis中关于Cache接口的实现类也使用了装饰者设计模式。
我们看下它的一些实现类:
简单说明:
LRU – 最近最少使用的:移除最长时间不被使用的对象。
FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
可以通过cache节点的eviction属性设置,也可以设置其他的属性。
cache-ref节点
mapper配置文件中还可以加入cache-ref节点,它有个属性namespace。
如果每个mapper文件都是用cache-ref,且namespace都一样,那么就代表着真正意义上的全局缓存。
如果只用了cache节点,那仅代表这个这个mapper内部的查询被缓存了,其他mapper文件的不起作用,这并不是所谓的全局缓存。
Mybatis数据源与连接池
对于ORM框架而言,数据源的组织是一个非常重要的一部分,这直接影响到框架的性能问题。本文将通过对MyBatis框架的数据源结构进行详尽的分析,并且深入解析MyBatis的连接池。
本文首先会讲述MyBatis的数据源的分类,然后会介绍数据源是如何加载和使用的。紧接着将分类介绍UNPOOLED、POOLED和JNDI类型的数据源组织;期间我们会重点讲解POOLED类型的数据源和其实现的连接池原理。
以下是本章的组织结构:
- 一、MyBatis数据源DataSource分类
- 二、数据源DataSource的创建过程
- 三、 DataSource什么时候创建Connection对象
- 四、不使用连接池的UnpooledDataSource
- 五、为什么要使用连接池?
- 六、使用了连接池的PooledDataSource
一、MyBatis数据源DataSource分类
MyBatis数据源实现是在以下四个包中:
MyBatis把数据源DataSource分为三种:
UNPOOLED 不使用连接池的数据源
POOLED 使用连接池的数据源
JNDI 使用JNDI实现的数据源
即:
相应地,MyBatis内部分别定义了实现了java.sql.DataSource接口的UnpooledDataSource,PooledDataSource类来表示UNPOOLED、POOLED类型的数据源。 如下图所示:
对于JNDI类型的数据源DataSource,则是通过JNDI上下文中取值。
二、数据源DataSource的创建过程
MyBatis 数据源 DataSource 对象的创建发生在 MyBatis 初始化的过程中。 下面让我们一步步地了解MyBatis是如何创建数据源DataSource的。
在mybatis的XML配置文件中,使用<dataSource>元素来配置数据源:
1. MyBatis在初始化时,解析此文件,根据<dataSource>的type属性来创建相应类型的的数据源DataSource,即:
- type=”POOLED” :MyBatis会创建PooledDataSource实例
- type=”UNPOOLED” :MyBatis会创建UnpooledDataSource实例
- type=”JNDI” :MyBatis会从JNDI服务上查找DataSource实例,然后返回使用
2. 顺便说一下,MyBatis是通过工厂模式来创建数据源DataSource对象的,MyBatis定义了抽象的工厂接口 : org.apache.ibatis.datasource.DataSourceFactory ,通过其getDataSource()方法返回数据源DataSource:
定义如下:
-
public interface DataSourceFactory {
-
void setProperties(Properties props);
-
//生产DataSource
-
DataSource getDataSource();
-
}
上述三种不同类型的type,则有对应的以下dataSource工厂:
- POOLED PooledDataSourceFactory
- UNPOOLED UnpooledDataSourceFactory
- JNDI JndiDataSourceFactory
其类图如下所示:
3. MyBatis创建了DataSource实例后,会将其放到Configuration对象内的Environment对象中, 供以后使用。
三、 DataSource什么时候创建Connection对象
当我们需要创建SqlSession对象并需要执行SQL语句时,这时候MyBatis才会去调用dataSource对象来创建java.sql.Connection对象。也就是说,java.sql.Connection对象的创建一直延迟到执行SQL语句的时候。
比如,我们有如下方法执行一个简单的SQL语句:
-
String resource = "mybatis-config.xml";
-
InputStream inputStream = Resources.getResourceAsStream(resource);
-
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
-
SqlSession sqlSession = sqlSessionFactory.openSession();
-
sqlSession.selectList("SELECT * FROM STUDENTS");
前4句都不会导致java.sql.Connection对象的创建,只有当第5句sqlSession.selectList("SELECT * FROM STUDENTS "),才会触发MyBatis在底层执行下面这个方法来创建java.sql.Connection对象:
-
protected void openConnection() throws SQLException {
-
if (log.isDebugEnabled()) {
-
log.debug("Opening JDBC Connection");
-
}
-
connection = dataSource.getConnection();
-
if (level != null) {
-
connection.setTransactionIsolation(level.getLevel());
-
}
-
setDesiredAutoCommit(autoCommmit);
-
}
而对于DataSource的UNPOOLED的类型的实现-UnpooledDataSource是怎样实现getConnection()方法的呢?请看下一节。
四、不使用连接池的UnpooledDataSource
当 <dataSource>的type属性被配置成了”UNPOOLED”,MyBatis首先会实例化一个UnpooledDataSourceFactory工厂实例,然后通过.getDataSource()方法返回一个UnpooledDataSource实例对象引用,我们假定为dataSource。
使用 UnpooledDataSource 的 getConnection(), 每调用一次就会产生一个新的 Connection 实例对象。
UnPooledDataSource的getConnection()方法实现如下:
-
/*
-
UnpooledDataSource的getConnection()实现
-
*/
-
public Connection getConnection() throws SQLException
-
{
-
return doGetConnection(username, password);
-
}
-
private Connection doGetConnection(String username, String password) throws SQLException
-
{
-
//封装username和password成properties
-
Properties props = new Properties();
-
if (driverProperties != null)
-
{
-
props.putAll(driverProperties);
-
}
-
if (username != null)
-
{
-
props.setProperty("user", username);
-
}
-
if (password != null)
-
{
-
props.setProperty("password", password);
-
}
-
return doGetConnection(props);
-
}
-
/*
-
* 获取数据连接
-
*/
-
private Connection doGetConnection(Properties properties) throws SQLException
-
{
-
//1.初始化驱动
-
initializeDriver();
-
//2.从DriverManager中获取连接,获取新的Connection对象
-
Connection connection = DriverManager.getConnection(url, properties);
-
//3.配置connection属性
-
configureConnection(connection);
-
return connection;
-
}
如上代码所示,UnpooledDataSource会做以下事情:
1. 初始化驱动: 判断driver驱动是否已经加载到内存中,如果还没有加载,则会动态地加载driver类,并实例化一个Driver对象,使用DriverManager.registerDriver()方法将其注册到内存中,以供后续使用。
2. 创建Connection对象: 使用DriverManager.getConnection()方法创建连接。
3. 配置Connection对象: 设置是否自动提交autoCommit和隔离级别isolationLevel。
4. 返回Connection对象。
上述的序列图如下所示:
总结:从上述的代码中可以看到, 我们每调用一次 getConnection() 方法,都会通过 DriverManager.getConnection() 返回新的 java.sql.Connection 实例。
五、为什么要使用连接池?
1. 创建一个java.sql.Connection实例对象的代价
首先让我们来看一下创建一个java.sql.Connection对象的资源消耗。我们通过连接Oracle数据库,创建创建Connection对象,来看创建一个Connection对象、执行SQL语句各消耗多长时间。代码如下:
public static void main(String[] args) throws Exception
{
String sql = "select * from hr.employees where employee_id < ? and employee_id >= ?";
PreparedStatement st = null;
ResultSet rs = null;
long beforeTimeOffset = -1L; //创建Connection对象前时间
long afterTimeOffset = -1L; //创建Connection对象后时间
long executeTimeOffset = -1L; //创建Connection对象后时间
Connection con = null;
Class.forName("oracle.jdbc.driver.OracleDriver");
beforeTimeOffset = new Date().getTime();
System.out.println("before:\t" + beforeTimeOffset);
con = DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521:xe", "louluan", "123456");
afterTimeOffset = new Date().getTime();
System.out.println("after:\t\t" + afterTimeOffset);
System.out.println("Create Costs:\t\t" + (afterTimeOffset - beforeTimeOffset) + " ms");
st = con.prepareStatement(sql);
//设置参数
st.setInt(1, 101);
st.setInt(2, 0);
//查询,得出结果集
rs = st.executeQuery();
executeTimeOffset = new Date().getTime();
System.out.println("Exec Costs:\t\t" + (executeTimeOffset - afterTimeOffset) + " ms");
}
static void main(String[] args) throws Exception
{
String sql = "select * from hr.employees where employee_id < ? and employee_id >= ?";
PreparedStatement st = null;
ResultSet rs = null;
long beforeTimeOffset = -1L; //创建Connection对象前时间
long afterTimeOffset = -1L; //创建Connection对象后时间
long executeTimeOffset = -1L; //创建Connection对象后时间
Connection con = null;
Class.forName("oracle.jdbc.driver.OracleDriver");
beforeTimeOffset = new Date().getTime();
System.out.println("before:\t" + beforeTimeOffset);
con = DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521:xe", "louluan", "123456");
afterTimeOffset = new Date().getTime();
System.out.println("after:\t\t" + afterTimeOffset);
System.out.println("Create Costs:\t\t" + (afterTimeOffset - beforeTimeOffset) + " ms");
st = con.prepareStatement(sql);
//设置参数
st.setInt(1, 101);
st.setInt(2, 0);
//查询,得出结果集
rs = st.executeQuery();
executeTimeOffset = new Date().getTime();
System.out.println("Exec Costs:\t\t" + (executeTimeOffset - afterTimeOffset) + " ms");
}
上述程序在我笔记本上的执行结果为:
从此结果可以清楚地看出,创建一个Connection对象,用了 250 毫秒 ;而执行SQL的时间用了 170毫秒 。
创建一个Connection对象用了250毫秒!这个时间对计算机来说可以说是一个 非常奢侈 的!
这仅仅是一个Connection对象就有这么大的代价,设想一下另外一种情况:如果我们在Web应用程序中,为用户的每一个请求就操作一次数据库,当有10000个在线用户并发操作的话,对计算机而言,仅仅创建Connection对象不包括做业务的时间就要损耗10000×250ms= 250 0000 ms = 2500 s = 41.6667 min,竟然要 41 分钟!!!如果对高用户群体使用这样的系统,简直就是开玩笑!
2. 问题分析:
创建一个java.sql.Connection对象的代价是如此巨大,是因为创建一个Connection对象的过程,在底层就相当于和数据库建立的通信连接,在建立通信连接的过程,消耗了这么多的时间,而往往我们建立连接后(即创建Connection对象后),就执行一个简单的SQL语句,然后就要抛弃掉,这是一个非常大的资源浪费!
3.解决方案:
对于需要频繁地跟数据库交互的应用程序,可以在创建了Connection对象,并操作完数据库后,可以不释放掉资源,而是将它放到内存中,当下次需要操作数据库时,可以直接从内存中取出Connection对象,不需要再创建了,这样就极大地节省了创建Connection对象的资源消耗。由于内存也是有限和宝贵的,这又对我们对内存中的Connection对象怎么有效地维护提出了很高的要求。我们将在内存中存放Connection对象的容器称之为 连接池(Connection Pool)。下面让我们来看一下MyBatis的线程池是怎样实现的。
六、使用了连接池的PooledDataSource
同样地,我们也是使用PooledDataSource的getConnection()方法来返回Connection对象。现在让我们看一下它的基本原理:
PooledDataSource将java.sql.Connection对象包裹成PooledConnection对象放到了PoolState类型的容器中维护。 MyBatis将连接池中的PooledConnection分为两种状态: 空闲状态(idle)和活动状态(active),这两种状态的PooledConnection对象分别被存储到PoolState容器内的 idleConnections 和 activeConnections 两个List集合中:
idleConnections :空闲(idle)状态PooledConnection对象被放置到此集合中,表示当前闲置的没有被使用的PooledConnection集合,调用PooledDataSource的getConnection()方法时,会优先从此集合中取PooledConnection对象。当用完一个java.sql.Connection对象时,MyBatis会将其包裹成PooledConnection对象放到此集合中。
activeConnections :活动(active)状态的PooledConnection对象被放置到名为activeConnections的ArrayList中,表示当前正在被使用的PooledConnection集合,调用PooledDataSource的getConnection()方法时,会优先从idleConnections集合中取PooledConnection对象,如果没有,则看此集合是否已满,如果未满,PooledDataSource会创建出一个PooledConnection,添加到此集合中,并返回。
PoolState连接池的大致结构如下所示:
6.1 获取java.sql.Connection对象的过程
下面让我们看一下PooledDataSource 的getConnection()方法获取Connection对象的实现:
-
public Connection getConnection() throws SQLException {
-
return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
-
}
-
public Connection getConnection(String username, String password) throws SQLException {
-
return popConnection(username, password).getProxyConnection();
-
}
上述的popConnection()方法,会从连接池中返回一个可用的PooledConnection对象,然后再调用getProxyConnection()方法最终返回Conection对象。(至于为什么会有getProxyConnection(),请关注下一节)
现在让我们看一下popConnection()方法到底做了什么:
1. 先看是否有空闲(idle)状态下的PooledConnection对象,如果有,就直接返回一个可用的PooledConnection对象;否则进行第2步。
2. 查看活动状态的PooledConnection池activeConnections是否已满;如果没有满,则创建一个新的PooledConnection对象,然后放到activeConnections池中,然后返回此PooledConnection对象;否则进行第三步;
3. 看最先进入activeConnections池中的PooledConnection对象是否已经过期:如果已经过期,从activeConnections池中移除此对象,然后创建一个新的PooledConnection对象,添加到activeConnections中,然后将此对象返回;否则进行第4步。
4. 线程等待,循环2步
-
/*
-
* 传递一个用户名和密码,从连接池中返回可用的PooledConnection
-
*/
-
private PooledConnection popConnection(String username, String password) throws SQLException
-
{
-
boolean countedWait = false;
-
PooledConnection conn = null;
-
long t = System.currentTimeMillis();
-
int localBadConnectionCount = 0;
-
while (conn == null)
-
{
-
synchronized (state)
-
{
-
if (state.idleConnections.size() > 0)
-
{
-
// 连接池中有空闲连接,取出第一个
-
conn = state.idleConnections.remove(0);
-
if (log.isDebugEnabled())
-
{
-
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
-
}
-
}
-
else
-
{
-
// 连接池中没有空闲连接,则取当前正在使用的连接数小于最大限定值,
-
if (state.activeConnections.size() < poolMaximumActiveConnections)
-
{
-
// 创建一个新的connection对象
-
conn = new PooledConnection(dataSource.getConnection(), this);
-
@SuppressWarnings("unused")
-
//used in logging, if enabled
-
Connection realConn = conn.getRealConnection();
-
if (log.isDebugEnabled())
-
{
-
log.debug("Created connection " + conn.getRealHashCode() + ".");
-
}
-
}
-
else
-
{
-
// Cannot create new connection 当活动连接池已满,不能创建时,取出活动连接池的第一个,即最先进入连接池的PooledConnection对象
-
// 计算它的校验时间,如果校验时间大于连接池规定的最大校验时间,则认为它已经过期了,利用这个PoolConnection内部的realConnection重新生成一个PooledConnection
-
//
-
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
-
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
-
if (longestCheckoutTime > poolMaximumCheckoutTime)
-
{
-
// Can claim overdue connection
-
state.claimedOverdueConnectionCount++;
-
state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
-
state.accumulatedCheckoutTime += longestCheckoutTime;
-
state.activeConnections.remove(oldestActiveConnection);
-
if (!oldestActiveConnection.getRealConnection().getAutoCommit())
-
{
-
oldestActiveConnection.getRealConnection().rollback();
-
}
-
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
-
oldestActiveConnection.invalidate();
-
if (log.isDebugEnabled())
-
{
-
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
-
}
-
}
-
else
-
{
-
//如果不能释放,则必须等待有
-
// Must wait
-
try
-
{
-
if (!countedWait)
-
{
-
state.hadToWaitCount++;
-
countedWait = true;
-
}
-
if (log.isDebugEnabled())
-
{
-
log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
-
}
-
long wt = System.currentTimeMillis();
-
state.wait(poolTimeToWait);
-
state.accumulatedWaitTime += System.currentTimeMillis() - wt;
-
}
-
catch (InterruptedException e)
-
{
-
break;
-
}
-
}
-
}
-
}
-
//如果获取PooledConnection成功,则更新其信息
-
if (conn != null)
-
{
-
if (conn.isValid())
-
{
-
if (!conn.getRealConnection().getAutoCommit())
-
{
-
conn.getRealConnection().rollback();
-
}
-
conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
-
conn.setCheckoutTimestamp(System.currentTimeMillis());
-
conn.setLastUsedTimestamp(System.currentTimeMillis());
-
state.activeConnections.add(conn);
-
state.requestCount++;
-
state.accumulatedRequestTime += System.currentTimeMillis() - t;
-
}
-
else
-
{
-
if (log.isDebugEnabled())
-
{
-
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
-
}
-
state.badConnectionCount++;
-
localBadConnectionCount++;
-
conn = null;
-
if (localBadConnectionCount > (poolMaximumIdleConnections + 3))
-
{
-
if (log.isDebugEnabled())
-
{
-
log.debug("PooledDataSource: Could not get a good connection to the database.");
-
}
-
throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
-
}
-
}
-
}
-
}
-
}
-
if (conn == null)
-
{
-
if (log.isDebugEnabled())
-
{
-
log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
-
}
-
throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
-
}
-
return conn;
-
}
对应的处理流程图如下所示:
如上所示,对于PooledDataSource的getConnection()方法内,先是调用类PooledDataSource的popConnection()方法返回了一个PooledConnection对象,然后调用了PooledConnection的getProxyConnection()来返回Connection对象。
6.2java.sql.Connection对象的回收
当我们的程序中使用完Connection对象时,如果不使用数据库连接池,我们一般会调用 connection.close()方法,关闭connection连接,释放资源。如下所示:
-
private void test() throws ClassNotFoundException, SQLException
-
{
-
String sql = "select * from hr.employees where employee_id < ? and employee_id >= ?";
-
PreparedStatement st = null;
-
ResultSet rs = null;
-
Connection con = null;
-
Class.forName("oracle.jdbc.driver.OracleDriver");
-
try
-
{
-
con = DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521:xe", "louluan", "123456");
-
st = con.prepareStatement(sql);
-
//设置参数
-
st.setInt(1, 101);
-
st.setInt(2, 0);
-
//查询,得出结果集
-
rs = st.executeQuery();
-
//取数据,省略
-
//关闭,释放资源
-
con.close();
-
}
-
catch (SQLException e)
-
{
-
con.close();
-
e.printStackTrace();
-
}
-
}
调用过close()方法的Connection对象所持有的资源会被全部释放掉,Connection对象也就不能再使用。
那么,如果我们使用了连接池,我们在用完了Connection对象时,需要将它放在连接池中,该怎样做呢?
可能大家第一个在脑海里闪现出来的想法就是:我在应该调用con.close()方法的时候,不调用close()f方法,将其换成将Connection对象放到连接池容器中的代码!
好,我们将上述的想法实现,首先定义一个简易连接池Pool,然后将上面的代码改写:
-
package com.foo.jdbc;
-
import java.sql.Connection;
-
import java.sql.DriverManager;
-
import java.sql.SQLException;
-
import java.util.Vector;
-
/**
-
*
-
* 一个线程安全的简易连接池实现,此连接池是单例的
-
* putConnection()将Connection添加到连接池中
-
* getConnection()返回一个Connection对象
-
*/
-
public class Pool {
-
private static Vector<Connection> pool = new Vector<Connection>();
-
private static int MAX_CONNECTION =100;
-
private static String DRIVER="oracle.jdbc.driver.OracleDriver";
-
private static String URL = "jdbc:oracle:thin:@127.0.0.1:1521:xe";
-
private static String USERNAME = "louluan";
-
private static String PASSWROD = "123456";
-
static {
-
try {
-
Class.forName(DRIVER);
-
} catch (ClassNotFoundException e) {
-
e.printStackTrace();
-
}
-
}
-
/**
-
* 将一个Connection对象放置到连接池中
-
*/
-
public static void putConnection(Connection connection){
-
synchronized(pool)
-
{
-
if(pool.size()<MAX_CONNECTION)
-
{
-
pool.add(connection);
-
}
-
}
-
}
-
/**
-
* 返回一个Connection对象,如果连接池内有元素,则pop出第一个元素;
-
* 如果连接池Pool中没有元素,则创建一个connection对象,然后添加到pool中
-
* @return Connection
-
*/
-
public static Connection getConnection(){
-
Connection connection = null;
-
synchronized(pool)
-
{
-
if(pool.size()>0)
-
{
-
connection = pool.get(0);
-
pool.remove(0);
-
}
-
else
-
{
-
connection = createConnection();
-
pool.add(connection);
-
}
-
}
-
return connection;
-
}
-
/**
-
* 创建一个新的Connection对象
-
*/
-
private static Connection createConnection()
-
{
-
Connection connection = null;
-
try {
-
connection = DriverManager.getConnection(URL, USERNAME,PASSWROD);
-
} catch (SQLException e) {
-
e.printStackTrace();
-
}
-
return connection;
-
}
-
}
-
package com.foo.jdbc;
-
import java.sql.Connection;
-
import java.sql.DriverManager;
-
import java.sql.PreparedStatement;
-
import java.sql.ResultSet;
-
import java.sql.SQLException;
-
import java.util.Vector;
-
public class PoolTest
-
{
-
private void test() throws ClassNotFoundException, SQLException
-
{
-
String sql = "select * from hr.employees where employee_id < ? and employee_id >= ?";
-
PreparedStatement st = null;
-
ResultSet rs = null;
-
Connection con = null;
-
Class.forName("oracle.jdbc.driver.OracleDriver");
-
try
-
{
-
con = DriverManager.getConnection("jdbc:oracle:thin:@127.0.0.1:1521:xe", "louluan", "123456");
-
st = con.prepareStatement(sql);
-
//设置参数
-
st.setInt(1, 101);
-
st.setInt(2, 0);
-
//查询,得出结果集
-
rs = st.executeQuery();
-
//取数据,省略
-
//将不再使用的Connection对象放到连接池中,供以后使用
-
Pool.putConnection(con);
-
}
-
catch (SQLException e)
-
{
-
e.printStackTrace();
-
}
-
}
-
}
上述的代码就是将我们使用过的Connection对象放到Pool连接池中,我们需要Connection对象的话,只需要使用Pool.getConnection()方法从里面取即可。
是的,上述的代码完全可以实现此能力,不过有一个很不优雅的实现: 就是我们需要手动地将Connection对象放到Pool连接池中,这是一个很傻的实现方式。这也和一般使用Connection对象的方式不一样:一般使用Connection的方式是使用完后,然后调用.close()方法释放资源。
为了和一般的使用Conneciton对象的方式保持一致,我们希望当Connection使用完后,调用.close()方法,而实际上Connection资源并没有被释放,而实际上被添加到了连接池中。这样可以做到吗?答案是可以。上述的要求从另外一个角度来描述就是:能否提供一种机制,让我们知道Connection对象调用了什么方法,从而根据不同的方法自定义相应的处理机制。恰好代理机制就可以完成上述要求.
怎样实现Connection对象调用了close()方法,而实际是将其添加到连接池中
这是要使用代理模式,为真正的Connection对象创建一个代理对象,代理对象所有的方法都是调用相应的真正Connection对象的方法实现。当代理对象执行close()方法时,要特殊处理,不调用真正Connection对象的close()方法,而是将Connection对象添加到连接池中。
MyBatis的PooledDataSource的PoolState内部维护的对象是PooledConnection类型的对象,而PooledConnection则是对真正的数据库连接java.sql.Connection实例对象的包裹器。
PooledConnection对象内持有一个真正的数据库连接java.sql.Connection实例对象和一个java.sql.Connection的代理:
其部分定义如下:
-
class PooledConnection implements InvocationHandler {
-
//......
-
//所创建它的datasource引用
-
private PooledDataSource dataSource;
-
//真正的Connection对象
-
private Connection realConnection;
-
//代理自己的代理Connection
-
private Connection proxyConnection;
-
//......
-
}
PooledConenction 实现了 InvocationHandler 接口,并且, proxyConnection 对象也是根据这个它来生成的代理对象:
public PooledConnection(Connection connection, PooledDataSource dataSource) {
this.hashCode = connection.hashCode();
this.realConnection = connection;
this.dataSource = dataSource;
this.createdTimestamp = System.currentTimeMillis();
this.lastUsedTimestamp = System.currentTimeMillis();
this.valid = true;
this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
}
PooledConnection(Connection connection, PooledDataSource dataSource) {
this.hashCode = connection.hashCode();
this.realConnection = connection;
this.dataSource = dataSource;
this.createdTimestamp = System.currentTimeMillis();
this.lastUsedTimestamp = System.currentTimeMillis();
this.valid = true;
this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
}
实际上,我们调用PooledDataSource的getConnection()方法返回的就是这个proxyConnection对象。
当我们调用此proxyConnection对象上的任何方法时,都会调用PooledConnection对象内invoke()方法。
让我们看一下PooledConnection类中的invoke()方法定义:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
//当调用关闭的时候,回收此Connection到PooledDataSource中
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
dataSource.pushConnection(this);
return null;
} else {
try {
if (!Object.class.equals(method.getDeclaringClass())) {
checkConnection();
}
return method.invoke(realConnection, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
//当调用关闭的时候,回收此Connection到PooledDataSource中
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
dataSource.pushConnection(this);
return null;
} else {
try {
if (!Object.class.equals(method.getDeclaringClass())) {
checkConnection();
}
return method.invoke(realConnection, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
从上述代码可以看到,当我们使用了pooledDataSource.getConnection()返回的Connection对象的close()方法时,不会调用真正Connection的close()方法,而是将此Connection对象放到连接池中。
七、JNDI类型的数据源DataSource
对于JNDI类型的数据源DataSource的获取就比较简单,MyBatis定义了一个JndiDataSourceFactory工厂来创建通过JNDI形式生成的DataSource。
下面让我们看一下JndiDataSourceFactory的关键代码:
if (properties.containsKey(INITIAL_CONTEXT)
&& properties.containsKey(DATA_SOURCE))
{
//从JNDI上下文中找到DataSource并返回
Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT));
dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE));
}
else if (properties.containsKey(DATA_SOURCE))
{
// //从JNDI上下文中找到DataSource并返回
dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE));
}
(properties.containsKey(INITIAL_CONTEXT)
&& properties.containsKey(DATA_SOURCE))
{
//从JNDI上下文中找到DataSource并返回
Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT));
dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE));
}
else if (properties.containsKey(DATA_SOURCE))
{
// //从JNDI上下文中找到DataSource并返回
dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE));
}
mybatis 与spring springmvc 整合