聊聊Mybatis的实现原理

聊聊,mybatis,实现,原理 · 浏览次数 : 225

小编点评

**MyBatis 使用示例解析通过代码可以清晰地看出,MyBatis的操作主要分为两大阶段:** **1. MyBatis 初始化阶段** * 读取配置并初始化 SqlSessionFactory * 创建数据库连接 * 创建 MapperProxy,用于执行 SQL **2. 数据读写阶段** * 创建 OpenSession,打开数据库连接 * 创建 Statement 对象,执行 SQL 语句 * 处理结果,并将结果返回 **关键代码解析:** * **Mapper接口中的方法:** * 由于 Mapper 接口中没有实现,在调用方法时需要通过实例方法的反射机制找到要执行的 SQL。 * 每个 <mapper>节点对应着一个 MapperProxy,该代理对象负责执行 SQL 语句。 * **<mapper>节点解析:** * 在解析 <mapper>节点时,根据节点的属性设置,例如 id、fetchSize、timeout等,构建 Statement 对象和 Result 对象。 * **Mapper文件与 Mapper 接口的关联:** * Mapper接口中定义了多个方法,这些方法对应着不同的 SQL 语句。 * 每个 <mapper>节点对应一个 MapperProxy,该代理对象将实际执行对应方法。 **总结:** * MyBatis 的初始化阶段负责配置和创建数据库连接,创建 MapperProxy。 * 数据读写阶段,使用 OpenSession、Statement 和 Result 对象执行 SQL 语句,并处理结果。

正文

使用示例

平时我们使用的一般是集成了Spring或是Spring Boot的Mybatis,封装了一层,看源码不直接;如下,看看原生的Mybatis使用示例

image

示例解析

通过代码可以清晰地看出,MyBatis的操作主要分为两大阶段:

  • 第一阶段:MyBatis初始化阶段。该阶段用来完成MyBatis运行环境的准备工作,读取配置并初始化关键的对象,提供给后续使用,只在 MyBatis启动时运行一次。
  • 第二阶段:数据读写阶段。该阶段由数据读写操作触发,将根据要求完成具体的增、删、改、查等数据库操作。

在第一阶段,最关键的就是SqlSessionFactory对象。在Spring集成Mybatis的源码中,SqlSessionFactoryBean也是做这个事情,读取配置并初始化构建SqlSessionFactory

public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
    // Spring Bean的生命周期会调用此方法
    public void afterPropertiesSet() throws Exception {
        this.sqlSessionFactory = this.buildSqlSessionFactory();
    }
    protected SqlSessionFactory buildSqlSessionFactory(){
        // 构建Configuration....
        Configuration configuration;
        if (this.configuration != null) {
            configuration = this.configuration;
            if (configuration.getVariables() == null) {
                configuration.setVariables(this.configurationProperties);
            } else if (this.configurationProperties != null) {
                configuration.getVariables().putAll(this.configurationProperties);
            }
        } else if (this.configLocation != null) {
            xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), (String)null, this.configurationProperties);
            configuration = xmlConfigBuilder.getConfiguration();
        } else {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Property `configuration` or 'configLocation' not specified, using default MyBatis Configuration");
            }

            configuration = new Configuration();
            configuration.setVariables(this.configurationProperties);
        }

        /// 其它code...

        return this.sqlSessionFactoryBuilder.build(configuration);
    }
}

配置文件的解析,最终会生成一个Configuration对象。

private void parseConfiguration(XNode root) {
    try {
        Properties settings = this.settingsAsPropertiess(root.evalNode("settings"));
        this.propertiesElement(root.evalNode("properties"));
        this.loadCustomVfs(settings);
        this.typeAliasesElement(root.evalNode("typeAliases"));
        this.pluginElement(root.evalNode("plugins"));
        this.objectFactoryElement(root.evalNode("objectFactory"));
        this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        this.reflectionFactoryElement(root.evalNode("reflectionFactory"));
        this.settingsElement(settings);
        this.environmentsElement(root.evalNode("environments"));
        this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        this.typeHandlerElement(root.evalNode("typeHandlers"));
        // 解析mappers节点
        this.mapperElement(root.evalNode("mappers"));
    } catch (Exception var3) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
    }
}

前期的准备已就绪,关键的配置已解析且构建并初始化了SqlSessionFactory了。接下来就是创建数据库连接并执行业务的CRUD。在第二阶段的OpenSession方法负责创建并打开数据库链接。

public SqlSession openSession(Connection connection) {
    return this.openSessionFromConnection(this.configuration.getDefaultExecutorType(), connection);
}

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;
}

最后就是调用Mapper接口的业务方法,返回业务数据。

//SqlSession.getMapper()
public <T> T getMapper(Class<T> type) {
    return this.configuration.getMapper(type, this);
}

// configuration.getMapper()
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return this.mapperRegistry.getMapper(type, sqlSession);
}

// mapperRegistry.getMapper()
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
    if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    } else {
        try {
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception var5) {
            throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
        }
    }
}

// mapperProxyFactory.newInstance()
protected T newInstance(MapperProxy<T> mapperProxy) {
    return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}

public T newInstance(SqlSession sqlSession) {
    MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
    return this.newInstance(mapperProxy);
}

最后,打完收工,示例代码所涉及到的关键代码就这些。

反思

上面的示例是比较简单的,那么其实现思路到底是什么样的?首先就有几个问题:

  1. Mapper接口中的方法没有实现,那客户端调用接口的方法时,返回的数据是从哪来的?
  2. Mapper文件与Mapper接口是怎么关联绑定上的?

第一个问题,绝对离不开动态代理,因为只有接口的时候,那么一定会有动态代理生成代理类同时有拦截处理器(InvocationHandler)来增强其执行逻辑。

第二个问题,Mapper文件中有一个<mapper>节点,其namespace就是接口的全限定名称,而其下节点<select>|<update>|..都有一个id值,该值与接口的方法是一致的。因此从这里就可以看出来,业务的crud操作节点是通过namespace+id来对应mapper接口及其方法的。那么我们就可以考虑到,在第一个问题中的拦截处理器执行方法method时,我们就可以通过此关联关系找到要执行的SQL。

如上,这么一分析来看,其实大概的实现思路已经出来了。就是动态代理+<mapper>节点解析实现了接口方法的调用与业务SQL的执行。

因此在第一阶段的解析时,Mapper文件里的<Mapper>节点解析出来的对象就起到了关键的作用。如下,有几个关键的抽象:

MapperRegistry类的knownMappers属性保存着接口及其代理对象的关系。

// type = interface
knownMappers.put(type, new MapperProxyFactory(type));

MappedStatement类对应着<mapper>节点下的CRUD节点。

public void parseStatementNode() {
    String id = this.context.getStringAttribute("id");
    if (this.databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
        Integer fetchSize = this.context.getIntAttribute("fetchSize");
        Integer timeout = this.context.getIntAttribute("timeout");
        String parameterMap = this.context.getStringAttribute("parameterMap");
        String parameterType = this.context.getStringAttribute("parameterType");
        Class<?> parameterTypeClass = this.resolveClass(parameterType);
        String resultMap = this.context.getStringAttribute("resultMap");
        String resultType = this.context.getStringAttribute("resultType");
        // 解析其他属性...
        this.builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, (KeyGenerator)keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
    }
}

如上是抽象出来整个执行过程的简单流程。最后就是基于JDBC创建Statement对象,并执行execute方法。

与聊聊Mybatis的实现原理相似的内容:

聊聊Mybatis的实现原理

### 使用示例 平时我们使用的一般是集成了Spring或是Spring Boot的Mybatis,封装了一层,看源码不直接;如下,看看原生的Mybatis使用示例 ![image](https://img2023.cnblogs.com/blog/971683/202305/971683-2023

聊聊Mybatis集成Spring的原理

一般都是研究框架源码,我为什么要反过来研究集成原理呢? 在我自己看来,集成虽然比较简单,但要求的细节比较多,需要掌握根本性的东西才能做到集成。 Mybatis集成Spring用到了FactoryBean以及BeanDefinition注册的原理,从这两个维度来实现集成,而我们单独学习Spring时,

聊聊Mybatis框架原理

好久没有写博客了。最近工作中封装了一个类似ORM框架的东西。大概的原理就是将Excel数据初始化到本地sqlite数据库后,通过json配置文件,对数据库的数据做增删改查等操作。 其实大概的思考了下,就是半ORM框架mybatis的逻辑,只是我们自己封装的简陋蛮多。想想有现成的轮子没用,反而是自己写

第一次线上 OOM 事故,竟和 where 1 = 1 有关

这篇文章,聊聊一个大家经常使用的编程模式 :Mybatis +「where 1 = 1 」。 笔者人生第一次重大的线上事故 ,就是和使用了类似的编程模式 相关,所以印象极其深刻。 这几天在调试一段业务代码时,又遇到类似的问题,所以笔者觉得非常要必要和大家絮叨絮叨。 1 OOM 事故 笔者曾服务一家电

聊聊GLM-4-9B开源模型的微调loss计算

概述 Github官方地址:GLM-4 网上已经有很多关于微调的文章,介绍各种方式下的使用,这里不会赘述。我个人比较关心的是微调时的loss计算逻辑,这点在很多的文章都不会有相关的描述,因为大多数人都是关心如何使用之类的应用层,而不是其具体的底层逻辑,当然咱也说不清太底层的计算。 可了解其它loss

聊聊一个差点被放弃的项目以及近期的开源计划

前言 自从 StarBlog 和 SiteDirectory 之后,我还没写新的关于开源项目的系列,最近又积累了很多想法,正好写一篇博客来总结一下。 关于差点被放弃的项目,就是最近一直在做的单点认证(IdentityServerLite) IdentityServerLite 开发这个项目的起因,是

聊聊 JSON Web Token (JWT) 和 jwcrypto 的使用

哈喽大家好,我是咸鱼。 最近写的一个 Python 项目用到了 jwcrypto 这个库,这个库是专门用来处理 JWT 的,JWT 全称是 JSON Web Token ,JSON 格式的 Token。 今天就来简单入门一下 JWT。 官方介绍:https://jwt.io/introduction

聊聊MySQL是如何处理排序的

在MySQL的查询中常常会用到 order by 和 group by 这两个关键字,它们的相同点是都会对字段进行排序,那查询语句中的排序是如何实现的呢?

聊聊 Linux iowait

哈喽大家好,我是咸鱼。 我们在使用 top 命令来查看 Linux 系统整体 CPU 使用情况的时候,往往看的是下面这一列: %Cpu(s): 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 68.0 wa, 0.0 hi, 0.0 si, 0.0 st 其中,man 手册解释 w

聊聊Spring的工厂方法与FactoryBean

概述 工厂方法是比较常见,常用的一种设计模式。FactoryBean是Spring提供的一种Bean注入IOC容器的方式。 工厂方法 在做日常开发时,一般都会避免直接new对象,而且将new的操作丢给IOC容器,但对于第三方系统的集成,我们不太好直接丢给IOC容器,此时可以通过工厂模式, 提供一个工