MyBatis持久层框架深入解析与实践

2024-08-03 09:56:26 浏览数 (1)

前言

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() 方法。

代码语言:java复制
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.configurationexecutorautoCommit 参数。在此之前,我们获取了XML中的配置并开启了数据库事务。最终,返回生成的 SqlSession

接下来,让我们再详细分析第三步操作,即通过 sqlSession.selectOne("com.jiagouedu.mybatis.UserMapper.selectUser", 3); 这条语句来获取数据。

代码语言:java复制
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); 方法的实现细节。

代码语言:java复制
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); 方法的具体实现。

代码语言:java复制
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 技术的奥秘。我热爱技术交流与分享,对开源社区充满热情。身兼掘金优秀作者、腾讯云内容共创官、阿里云专家博主、华为云云享专家等多重身份。

0 人点赞