SayMeeveTime

解密 MyBatis 架构及其核心机制

avatar

Chester

在Java世界中,数据持久化是应用开发不可或缺的一环。传统的JDBC(Java Database Connectivity)方式虽然提供了与数据库交互的基础能力,但在实际开发中却暴露出不少问题:

  • 资源消耗与性能瓶颈:频繁的数据库连接创建与释放会消耗系统资源,影响性能(尽管连接池可以缓解)。
  • SQL硬编码与维护难题:SQL语句直接硬编码在Java代码中,使得代码难以维护,因为SQL的变化往往需要修改Java代码。
  • 参数设置与结果解析的繁琐:使用PreparedStatement设置参数时,如果WHERE条件不确定导致参数数量变化,修改SQL也需要修改Java代码,进一步降低了可维护性。此外,结果集的解析也存在硬编码问题,依赖于查询的列名,SQL变更同样会引发解析代码的变化。
  • 对象封装的缺失:如果能将数据库记录方便地封装成POJO(Plain Old Java Object)对象,将大大提高开发效率。

为了克服这些挑战,业界涌现了多种解决方案。MyBatis就是其中一个优秀的持久层框架,它起源于Apache的iBatis项目,并于2010年迁移到Google Code后更名为MyBatis。MyBatis对JDBC操作数据库的繁琐过程进行了封装,让开发者能够将精力集中在SQL本身,无需处理注册驱动、创建连接、创建Statement、手动设置参数、结果集检索等JDBC底层细节。

与传统的ORM(Object-Relational Mapping)框架不同,MyBatis并没有将Java对象与数据库表直接关联起来。相反,它将Java方法与SQL语句关联。这种设计允许用户充分利用数据库的各种功能,例如存储过程、视图、复杂查询以及特定数据库的专有特性。因此,对于操作遗留数据库、结构不规范的数据库,或者需要对SQL执行有完全控制权的场景,MyBatis是一个非常合适的选择。


MyBatis 架构核心剖析

要理解MyBatis如何工作,我们可以从其核心架构和工作流程入手。MyBatis的架构围绕着几个关键组件展开:

1. 配置文件 (Configuration Files)

  • mybatis-config.xml: 这是MyBatis的全局配置文件。它包含了MyBatis运行环境的各种配置信息,例如数据库连接环境 (environmentsenvironmentdataSource) 和事务管理器 (transactionManager)。Mapper映射文件也需要在此文件中被加载。
  • mapper.xml: 这是SQL映射文件。用于定义要执行的各种数据库操作的SQL语句。每个mapper.xml文件通常对应一个Mapper接口,并包含如<select><insert><update><delete>等标签定义的SQL语句。

2. SqlSessionFactoryBuilder

  • 这是一个工具类,它的主要职责是根据mybatis-config.xml等配置信息来构建SqlSessionFactory
  • 一旦SqlSessionFactory构建完成,SqlSessionFactoryBuilder的使命也就结束了。
  • 其最佳使用范围是方法体内的局部变量。

3. SqlSessionFactory

  • 这是MyBatis的会话工厂。它是创建SqlSession的关键。
  • SqlSessionFactory是一个接口,定义了多种openSession重载方法。
  • 它一旦创建就可以在整个应用运行期间重复使用,通常以单例模式进行管理。

4. SqlSession

  • 这是MyBatis的会话。它类似于JDBC中的一个连接 (Connection)。
  • 所有数据库操作都需要通过SqlSession来执行。它封装了对数据库的CRUD(创建、读取、更新、删除)操作。
  • SqlSession是通过SqlSessionFactory创建的。
  • SqlSession是面向用户的接口,默认实现类是DefaultSqlSession
  • 非常重要的一点是:每个线程都应该有它自己的SqlSession实例。SqlSession的实例不能共享使用,它也是线程不安全的。因此,其最佳使用范围是请求或方法范围。
  • 使用完毕后,SqlSession必须关闭,通常建议将其关闭操作放在finally块中,以确保每次都能执行。

5. Executor

  • 这是MyBatis底层自定义的数据库操作接口。
  • Executor接口有两个实现:一个是基本执行器,另一个是缓存执行器。它负责具体的SQL执行。

6. Mapped Statement

  • 这是MyBatis的底层封装对象。它包装了MyBatis的配置信息以及SQL映射信息。
  • mapper.xml文件中定义的每一个SQL语句(如<select><insert>等)都对应着一个Mapped Statement对象。SQL语句的id属性就是Mapped Statement的唯一标识符。
  • 输入参数映射: Mapped Statement定义了SQL执行的输入参数,这些参数可以是HashMap、基本类型或POJO。Executor在执行SQL之前,会通过Mapped Statement将输入的Java对象映射到SQL语句中。这相当于JDBC编程中对PreparedStatement设置参数的过程。参数引用通常使用#{参数名}(推荐,进行预编译)或${参数名}(字符串拼接,存在SQL注入风险,用于动态列名等场景)。
  • 输出结果映射: Mapped Statement定义了SQL执行后的输出结果,这些结果可以是HashMap、基本类型或POJO。Executor在SQL执行完毕后,会通过Mapped Statement将数据库返回的结果集映射到Java对象。这个过程相当于JDBC编程中对结果集的解析处理。映射可以通过简单的resultType(映射到基本类型、POJO、List、Map) 或更复杂的resultMap(自定义映射,处理字段名不匹配、一对一、一对多等复杂场景)来实现。

MyBatis 工作流程示意 (基于组件描述)

可以概念化地描述MyBatis的工作流程如下:

  1. 加载配置: 应用启动时,通过SqlSessionFactoryBuilder加载mybatis-config.xml全局配置文件和其中引用的mapper.xml映射文件。
  2. 构建会话工厂: SqlSessionFactoryBuilder解析配置文件,构建并初始化SqlSessionFactorySqlSessionFactory包含了解析后的所有配置信息(包括Mapped Statement)和运行环境信息。
  3. 创建会话: 当需要执行数据库操作时,应用通过SqlSessionFactory获取一个SqlSession
  4. 执行操作: 用户调用SqlSession提供的方法(如selectOne, insert, update, delete) 或通过Mapper接口调用对应方法。
  5. 定位 Mapped Statement: SqlSession根据调用信息(如Mapper接口方法名或XML中SQL的ID)找到对应的Mapped Statement对象。
  6. 参数映射: SqlSession将调用方法时传入的Java参数对象,通过Mapped Statement中定义的输入参数映射规则,转换成SQL语句所需的参数。
  7. 执行 SQL: SqlSession将映射好的参数和SQL语句交给底层的Executor执行器。Executor与数据库进行交互,执行SQL。
  8. 结果映射: Executor从数据库获取到结果集后,通过Mapped Statement中定义的输出结果映射规则 (resultTyperesultMap),将结果集映射成相应的Java对象。
  9. 返回结果: 映射后的Java对象被返回给SqlSession,再由SqlSession返回给用户代码。
  10. 关闭会话: 数据库操作完成后,必须显式或隐式地关闭SqlSession以释放数据库连接等资源。

通过上述架构和流程,MyBatis有效地解决了前面提到的JDBC问题:

  • 连接资源浪费: 在mybatis-config.xml中配置数据源,使用连接池来管理数据库连接。
  • SQL硬编码: 将SQL语句定义在独立的mapper.xml文件中,与Java代码分离。
  • 参数传递繁琐: MyBatis自动将Java对象映射到SQL语句,通过Mapped StatementparameterType等机制定义输入参数。
  • 结果集解析困难: MyBatis自动将SQL执行结果映射到Java对象,通过Mapped StatementresultTyperesultMap等机制定义输出结果类型。

Mapper 接口方式:模板代码的终结者

在早期或基本的MyBatis用法中,我们可能需要手动获取SqlSession,然后调用其方法来执行SQL,例如 sqlSession.selectOne("namespace.id", parameter)。这种方式虽然直接,但会导致大量重复的模板代码,如获取SqlSession、提交/回滚事务、关闭SqlSession等。

为了解决这个问题,MyBatis引入了Mapper接口方式。开发者只需要定义一个Java接口(如UserMapper),并在相应的mapper.xml文件中定义好SQL语句,MyBatis可以通过动态代理自动生成该接口的实现类。然后,通过sqlSession.getMapper(UserMapper.class)即可获取到这个代理对象,直接调用接口方法就能完成数据库操作,极大地简化了开发。

使用Mapper接口方式时,接口方法的名字通常与mapper.xml文件中对应的SQL语句的id一致。方法的参数会自动映射到SQL中的参数,方法的返回值类型则对应SQL的resultTyperesultMap配置。

为了让MyBatis能够找到并注册Mapper接口及其对应的XML文件,需要在mybatis-config.xml中的<mappers>节点进行配置。常见的配置方式是使用<package name="...">扫描指定包下的所有Mapper接口。需要注意的是,为了让MyBatis正确地找到XML文件,Mapper接口和对应的mapper.xml文件通常建议放在同一个包下,并且文件名与接口名对应。


全局配置与高级特性

除了核心架构组件和Mapper接口,MyBatis还提供了丰富的全局配置和高级特性,以满足各种复杂的持久化需求。

全局配置 (mybatis-config.xml)

全局配置文件 (mybatis-config.xml) 包含了多个重要的配置节点:

  • <properties>: 用于引入外部的属性配置文件(如数据库连接配置),使得配置信息更加灵活和易于管理。
  • <settings>: 包含了MyBatis运行时行为的各种全局设置,例如是否启用二级缓存 (cacheEnabled)、是否启用延迟加载 (lazyLoadingEnabled)、延迟加载行为 (aggressiveLazyLoading)、默认的执行器类型 (defaultExecutorType)、驼峰命名自动映射 (mapUnderscoreToCamelCase) 等。
  • <typeAliases>: 用于为Java类型定义短名称别名,避免在Mapper文件中书写完整的类路径。MyBatis内置了一些常用类型的别名。开发者也可以自定义别名,或通过包扫描的方式批量为指定包下的类定义别名(默认别名为类名首字母小写)。
  • <typeHandlers>: 用于处理Java类型和JDBC类型之间的映射。MyBatis内置了许多默认的类型处理器。对于特殊的类型映射需求(例如将Java中的List<String>映射到数据库的VARCHAR字段),可以自定义类型处理器。自定义的TypeHandler需要实现TypeHandler接口,并可能需要使用@MappedJdbcTypes@MappedTypes注解来指定它处理的JDBC类型和Java类型。自定义TypeHandler可以在单个SQL参数/结果中局部引用,或在全局配置中注册。
  • <objectFactory>: 用于自定义MyBatis创建结果对象的方式。
  • <plugins>: 用于拦截MyBatis的方法调用,实现自定义逻辑,例如分页插件等。
  • <environments> / <environment>: 配置数据库运行环境,可以定义多个环境(如开发、测试、生产),并通过default属性指定当前使用的环境。每个环境包含事务管理器 (transactionManager) 和数据源 (dataSource) 的配置。
  • <transactionManager>: 配置事务管理器,MyBatis支持JDBC事务和Managed事务。
  • <dataSource>: 配置数据源,MyBatis支持POOLED(连接池)和UNPOOLED(非连接池)类型,也可以配置第三方数据源。
  • <mappers>: 用于注册Mapper文件或Mapper接口。支持多种方式:按相对类路径资源 (resource)、按绝对URL (url)、按Mapper接口类 (class)、扫描包 (package)。package方式在实际项目中常用。

Mapper 映射文件 (mapper.xml)

mapper.xml 文件是MyBatis的核心,它定义了SQL语句和结果映射规则。

  • <select>, <insert>, <update>, <delete>: 定义了基本的CRUD操作的SQL语句。每个语句都有一个唯一的id和一个可选的parameterType(输入参数类型)。
  • 参数处理 (parameterType): 定义输入参数的类型。参数在SQL中通过#{paramName}${paramName}引用。@Param注解可以用于为多个简单类型参数指定名称,以便在XML中引用。对象参数可以直接引用属性名,如果使用了@Param,则需要加上前缀(#{paramName.propertyName})。
  • 结果处理 (resultType, resultMap): 定义SQL执行结果如何映射到Java对象。
    • resultType: 用于简单的结果映射,直接指定返回的Java类型,MyBatis会自动将列名与属性名匹配。
    • resultMap: 用于复杂的结果映射,需要手动定义列到属性的映射规则。通过<resultMap id="..." type="...">定义,内部使用<id>映射主键列,<result>映射普通列。支持使用<constructor>指定构造方法进行对象创建。resultMap支持继承 (extends) 以复用映射规则。

动态 SQL (Dynamic SQL)

动态SQL是MyBatis的强大特性之一,它允许根据条件构建灵活变化的SQL语句。主要节点包括:

  • <if>: 根据条件判断是否包含某个SQL片段。
  • <where>: 用于包含多个if条件的查询语句,如果存在条件,会自动加上WHERE关键字,并处理多余的ANDOR
  • <set>: 用于包含多个if条件的更新语句,如果存在条件,会自动加上SET关键字,并处理多余的逗号。
  • <foreach>: 用于迭代集合或数组,构建IN条件、批量插入等。属性包括collection(集合/数组名称)、opencloseitem(当前元素别名)、separator(元素分隔符)。
  • <sql> / <include>: 定义可重用的SQL片段 (<sql>),并在其他SQL语句中引用 (<include refid="...">),避免重复。

关联查询映射

MyBatis通过<resultMap>支持处理复杂的关系映射,包括一对一和一对多查询。

  • 一对一 (<association>): 在父对象的resultMap中使用<association>标签来映射关联的单个对象。可以通过嵌套结果映射(在<association>中定义列到属性的映射) 或通过懒加载 (select, column, fetchType="lazy") 的方式实现。
  • 一对多 (<collection>): 在父对象的resultMap中使用<collection>标签来映射关联的集合。ofType属性指定集合元素的类型。同样支持嵌套结果映射或懒加载。

查询缓存 (Query Cache)

MyBatis提供两级缓存机制来提高查询性能:

  • 一级缓存: SqlSession级别的缓存。默认开启。在同一个SqlSession中,对同一条SQL语句的重复查询会直接从缓存获取数据,不再访问数据库。当SqlSession关闭后,一级缓存失效。不同SqlSession之间不共享一级缓存。
  • 二级缓存: Mapper namespace级别的缓存。需要显式配置开启。在同一个namespace下,不同SqlSession执行相同的SQL语句(且参数相同)时,第一次执行后会将数据写入缓存,后续查询会从缓存读取。二级缓存可以跨SqlSession共享,其作用范围是一个Mapper的整个命名空间。

逆向工程 (Reverse Engineering)

为了减少为每个数据库表手动创建实体类、Mapper接口和Mapper XML文件的重复工作,MyBatis提供了逆向工程工具。这些工具可以根据数据库表结构自动生成相应的Java代码和XML文件,提高开发效率。例如,mybatis-generator-core工具就是常用的逆向工程工具。


总结

MyBatis作为一个优秀的持久层框架,通过其独特的设计理念——将Java方法与SQL语句关联,并在XML或注解中配置SQL——成功地解决了传统JDBC编程中的痛点。其核心架构围绕着SqlSessionFactoryBuilderSqlSessionFactorySqlSessionExecutorMapped Statement等组件构建。借助Mapper接口方式,开发者可以专注于业务逻辑,大大减少模板代码。同时,MyBatis提供了强大的全局配置、动态SQL、丰富的参数和结果映射能力(特别是resultMap用于处理一对一和一对多关联),以及两级缓存机制,使其成为一个灵活、高效且功能强大的数据持久化解决方案。对于需要精细控制SQL,或处理复杂数据库结构的场景,MyBatis是一个非常值得考虑的选择。


2024056667
powered by SayMeeveTime