前言
MyBatis是一个流行的Java持久层框架,它简化了数据库操作,支持SQL定制化、存储过程和高级映射。本文将深入探讨MyBatis的工作原理,并通过实例演示如何配置和使用MyBatis。MyBatis提供了一种半自动的ORM(对象关系映射)实现,允许开发者自定义SQL语句,同时避免了JDBC代码的冗余和手动处理数据库连接的复杂性。MyBatis通过XML或注解配置,将Java对象映射到数据库记录。
MyBatis关键类
MyBatis源码解析
代码语言:java复制@Slf4j
public class MybatisTest {
//一级缓存
@Test
public void test() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory =new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
sqlSession.selectOne("com.jiagouedu.mybatis.UserMapper.selectUser",3);
//log.info("user1:{}", sqlSession.selectOne("com.jiagouedu.mybatis.UserMapper.selectUser", 3));
//log.info("user2:{}", sqlSession.selectOne("com.jiagouedu.mybatis.UserMapper.selectUser", 3));
//sqlSession.commit();
// log.info("user3:{}", sqlSession.selectOne("com.jiagouedu.mybatis.UserMapper.selectUser", 3));
// log.info("user4:{}", sqlSession.selectOne("com.jiagouedu.mybatis.UserMapper.selectUser", 3));
}
}
我们采用常见的XML格式来进行配置MyBatis的相关属性以及编写SQL语句。
mybatis-config.xml配置如下:
代码语言: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>
<!-- <settings>
<setting name="cacheEnabled" value="true" />
</settings>-->
<typeAliases>
<typeAlias type="com.xiaoyu.pojo.User" alias="user"></typeAlias>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/xiaoyu"/>
<property name="username" value="root"/>
<property name="password" value="20152974"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mybatis/UserMapper.xml"/>
</mappers>
</configuration>
SQL映射文件(如UserMapper.xml)定义了操作数据库的SQL语句和结果映射。MyBatis支持一级缓存和自定义缓存机制,以提高性能。
代码语言:xml复制1 <?xml version="1.0" encoding="UTF-8" ?>
2 <!DOCTYPE mapper
3 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
4 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
5 <mapper namespace="com.xiaoyu.mybatis.UserMapper">
6 <!--<cache eviction="LRU" type="com.xiaoyu.cache.MybatisRedisCache"/>-->
7 <select id="selectUser" parameterType="integer" resultType="user">
8 select * from user where id = #{id}
9 </select>
10
11 </mapper>
通过分析MyBatis的源码,我们可以了解其构建过程、会话管理、SQL执行等关键操作。例如,SqlSessionFactoryBuilder的build方法解析XML配置,创建SqlSessionFactory实例。
代码语言:java复制public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
SqlSessionFactory var5;
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
var5 = this.build(parser.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException var13) {
;
}
}
return var5;
}
让我们再来仔细研究一下 parser.parse()
方法。
public Configuration parse() {
if (this.parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
} else {
this.parsed = true;
this.parseConfiguration(this.parser.evalNode("/configuration"));
return this.configuration;
}
}
this.parser.evalNode("/configuration")这行代码的作用是查找XML文件中的configuration节点。如果你不确定的话,可以进入this.parseConfiguration方法查看具体实现。
代码语言:java复制private void parseConfiguration(XNode root) {
try {
this.propertiesElement(root.evalNode("properties"));
Properties settings = this.settingsAsProperties(root.evalNode("settings"));
this.loadCustomVfs(settings);
this.typeAliasesElement(root.evalNode("typeAliases"));
this.pluginElement(root.evalNode("plugins"));
this.objectFactoryElement(root.evalNode("objectFactory"));
this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
this.settingsElement(settings);
this.environmentsElement(root.evalNode("environments"));
this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
this.typeHandlerElement(root.evalNode("typeHandlers"));
this.mapperElement(root.evalNode("mappers"));
} catch (Exception var3) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " var3, var3);
}
}
properties、settings、typeAliases等属性,大家都应该知道如何配置。因此,builder方法的主要作用是从XML文件中读取数据并将其加载到内存中。
在解析完第一步的源码后,接下来我们要分析第二步的sqlSessionFactory.openSession()的源码,查看它具体执行了哪些操作和工作。
代码语言:java复制private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
DefaultSqlSession var8;
try {
Environment environment = this.configuration.getEnvironment();
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
Executor executor = this.configuration.newExecutor(tx, execType);
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
} catch (Exception var12) {
this.closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " var12, var12);
} finally {
ErrorContext.instance().reset();
}
return var8;
}
关键的一步是创建 DefaultSqlSession
对象,使用 this.configuration
、executor
和 autoCommit
参数。在此之前,我们获取了XML中的配置并开启了数据库事务。最终,返回生成的 SqlSession
。
接下来,让我们再详细分析第三步操作,即通过 sqlSession.selectOne("com.jiagouedu.mybatis.UserMapper.selectUser", 3);
这条语句来获取数据。
public <T> T selectOne(String statement, Object parameter) {
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " list.size());
} else {
return null;
}
}
代码语言:java复制public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
List var5;
try {
MappedStatement ms = this.configuration.getMappedStatement(statement);
var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception var9) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " var9, var9);
} finally {
ErrorContext.instance().reset();
}
return var5;
}
这段代码的主要功能是通过 this.configuration.getMappedStatement(statement)
方法来获取我们编写的 mapper XML 对象,这样可以为后续的 SQL 拼写和其他操作提供必要的数据和配置信息。
this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);这个操作被频繁使用,直接查看源码来进一步了解。
代码语言:java复制public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
boundSql 是一个常见的对象,通常指的是我们自己编写的 SQL 查询语句,并且它也包含了相应的参数信息。
this.createCacheKey方法非常强大,建议我们查看其源代码以深入了解其实现原理。
代码语言:java复制public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
Iterator var8 = parameterMappings.iterator();
while(var8.hasNext()) {
ParameterMapping parameterMapping = (ParameterMapping)var8.next();
if (parameterMapping.getMode() != ParameterMode.OUT) {
String propertyName = parameterMapping.getProperty();
Object value;
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = this.configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (this.configuration.getEnvironment() != null) {
cacheKey.update(this.configuration.getEnvironment().getId());
}
return cacheKey;
}
}
这段代码展示了 MyBatis 的一级缓存机制,它使用了 id、offset、limit 和 SQL 语句作为组合的关键字。接下来我们可以深入了解的是 this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
方法的实现细节。
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) {
this.flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
this.ensureNoOutParams(ms, parameterObject, boundSql);
List<E> list = (List)this.tcm.getObject(cache, key);
if (list == null) {
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
this.tcm.putObject(cache, key, list);
}
return list;
}
}
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
我们可以观察到,在执行 SQL 之前,MyBatis 会先检查缓存中是否已经存在数据。如果缓存命中,它会直接返回缓存中的数据;如果未命中,则会执行 SQL 查询。执行完 SQL 后,会将结果再次存入缓存中。让我们来查看源码确认这个流程是否如此实现。
代码语言:java复制public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
this.clearLocalCache();
}
List list;
try {
this.queryStack;
list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
if (list != null) {
this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
--this.queryStack;
}
if (this.queryStack == 0) {
Iterator var8 = this.deferredLoads.iterator();
while(var8.hasNext()) {
BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
deferredLoad.load();
}
this.deferredLoads.clear();
if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
this.clearLocalCache();
}
}
return list;
}
}
让我们再深入研究一下 this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
方法的具体实现。
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);
List list;
try {
list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
this.localCache.removeObject(key);
}
this.localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
this.localOutputParameterCache.putObject(key, parameter);
}
return list;
}
正如我们所推测的,该方法在查询完毕后会将结果存入缓存,以便在下次查询时提升效率,避免再次访问数据库,从而有效减轻数据库的负载压力。
总结
MyBatis是一个强大且灵活的Java持久层框架,它通过简化数据库操作和提供高度定制化的SQL管理,极大地简化了开发者的工作流程。本文深入探讨了MyBatis的核心组件和工作原理,从XML配置到SQL执行再到结果映射,逐步揭示了其内部运行机制。
通过对MyBatis关键类的分析,我们了解到了SqlSessionFactory的创建过程以及SqlSession的使用方式。配置文件mybatis-config.xml和SQL映射文件UserMapper.xml的详细说明,展示了如何配置数据源、事务管理和SQL语句,以及如何将Java对象映射到数据库记录。
进一步分析MyBatis的源码实现,我们探讨了SqlSessionFactoryBuilder的build方法、SqlSession的创建过程以及SQL执行的详细流程。通过缓存机制的介绍,我们了解到MyBatis如何通过一级缓存和二级缓存提升查询性能,减少数据库访问次数,从而优化应用的整体性能表现。
总结来说,MyBatis以其灵活的配置、强大的定制能力和高效的SQL执行能力,成为了Java开发中不可或缺的持久化框架之一。通过本文的学习,读者可以更深入地理解和应用MyBatis在实际项目中的优势和特性,从而提升开发效率和应用性能。
我是努力的小雨,一名 Java 服务端码农,潜心研究着 AI 技术的奥秘。我热爱技术交流与分享,对开源社区充满热情。身兼掘金优秀作者、腾讯云内容共创官、阿里云专家博主、华为云云享专家等多重身份。