MyBatis工作原理详解

文章目录

1. MyBatis简介

1.1 什么是MyBatis

MyBatis是一个优秀的持久层框架,它支持自定义SQL、存储过程以及高级映射。MyBatis几乎消除了JDBC代码和参数的手动设置以及结果集的检索。MyBatis使用简单的XML或注解进行配置,将接口和Java的POJO(Plain Old Java Objects,普通的Java对象)映射到数据库中的记录。

与其他ORM框架不同,MyBatis并没有将Java对象与数据库表关联起来,而是将方法与SQL语句关联。MyBatis让开发人员可以使用更自然的面向对象的方式来操作数据库。

1.2 MyBatis解决的问题

  1. 简化JDBC操作:MyBatis大大简化了JDBC操作,减少了手动编写JDBC代码的工作量。
  2. SQL与代码分离:MyBatis将SQL语句与Java代码分离,便于维护和管理。
  3. 参数映射:自动将Java对象映射到SQL语句的参数中。
  4. 结果映射:自动将SQL查询结果映射为Java对象。
  5. 支持动态SQL:可以根据不同的条件动态生成SQL语句。
  6. 缓存机制:提供一级缓存和二级缓存,提高查询效率。

1.3 与JDBC、Hibernate的对比

特性JDBCMyBatisHibernate
开发效率
灵活性
学习曲线陡峭平缓较陡峭
SQL控制完全手动自定义自动生成
性能依赖优化
映射关系SQL映射对象-关系映射

1.4 基本工作流程概览

MyBatis的基本工作流程如下:

  1. 应用程序调用MyBatis的API访问数据库
  2. MyBatis根据配置文件创建SqlSessionFactory
  3. 通过SqlSessionFactory获取SqlSession实例
  4. SqlSession完成数据库操作
  5. 业务逻辑处理完成后关闭SqlSession
// 基本工作流程示例代码
// 1. 读取配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 构建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 3. 打开SqlSession
SqlSession session = sqlSessionFactory.openSession();
try {
    // 4. 执行SQL语句
    User user = session.selectOne("org.mybatis.example.UserMapper.getUserById", 1);
    // 5. 处理结果
    System.out.println(user.getName());
} finally {
    // 6. 关闭SqlSession
    session.close();
}

2. MyBatis核心组件

MyBatis框架的核心组件构成了其工作原理的基础。每个组件都有其特定的功能和作用,它们协同工作,完成从Java对象到数据库操作的转换。

2.1 SqlSessionFactoryBuilder

SqlSessionFactoryBuilder是MyBatis的入口,用于构建SqlSessionFactory实例。它可以从XML配置文件或Configuration类构建SqlSessionFactory。

// 从XML文件创建SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

// 从Java配置类创建SqlSessionFactory
Configuration configuration = new Configuration();
// ... 配置属性和映射
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);

SqlSessionFactoryBuilder的作用就是读取配置信息并创建SqlSessionFactory对象。一旦创建了SqlSessionFactory,就不再需要SqlSessionFactoryBuilder了,它可以被回收或销毁。

2.2 SqlSessionFactory

SqlSessionFactory是MyBatis的核心接口,它用于创建SqlSession实例。SqlSessionFactory的生命周期应该与应用程序的生命周期相同,通常情况下,我们只需要一个SqlSessionFactory实例。

// 创建默认的SqlSession
SqlSession session = sqlSessionFactory.openSession();

// 创建自动提交的SqlSession
SqlSession autoCommitSession = sqlSessionFactory.openSession(true);

// 创建指定事务隔离级别的SqlSession
SqlSession isolatedSession = sqlSessionFactory.openSession(TransactionIsolationLevel.READ_COMMITTED);

// 创建指定执行器类型的SqlSession
SqlSession batchSession = sqlSessionFactory.openSession(ExecutorType.BATCH);

SqlSessionFactory是线程安全的,可以被多个线程共享。它通常在应用程序启动时创建,并在应用程序结束时销毁。

2.3 SqlSession

SqlSession是MyBatis的主要接口,通过它可以执行SQL命令、获取映射器(Mapper)、管理事务。SqlSession是线程不安全的,每个线程应该有自己的SqlSession实例。

SqlSession session = sqlSessionFactory.openSession();
try {
    // 执行SQL语句
    List<User> users = session.selectList("org.mybatis.example.UserMapper.getAllUsers");
    
    // 插入数据
    User newUser = new User("Tom", "tom@example.com");
    session.insert("org.mybatis.example.UserMapper.insertUser", newUser);
    
    // 更新数据
    User userToUpdate = session.selectOne("org.mybatis.example.UserMapper.getUserById", 1);
    userToUpdate.setName("Updated Name");
    session.update("org.mybatis.example.UserMapper.updateUser", userToUpdate);
    
    // 删除数据
    session.delete("org.mybatis.example.UserMapper.deleteUser", 2);
    
    // 提交事务
    session.commit();
} catch (Exception e) {
    // 回滚事务
    session.rollback();
    throw e;
} finally {
    // 关闭SqlSession
    session.close();
}

SqlSession的生命周期应该是请求作用域的,即一个请求一个SqlSession,请求结束后关闭SqlSession。

2.4 Mapper接口

Mapper接口是MyBatis中用于定义数据库操作的接口。MyBatis会为Mapper接口创建动态代理对象,通过代理对象执行SQL语句。

// 定义Mapper接口
public interface UserMapper {
    User getUserById(int id);
    List<User> getAllUsers();
    void insertUser(User user);
    void updateUser(User user);
    void deleteUser(int id);
}

// 使用Mapper接口
SqlSession session = sqlSessionFactory.openSession();
try {
    UserMapper userMapper = session.getMapper(UserMapper.class);
    
    // 查询
    User user = userMapper.getUserById(1);
    List<User> allUsers = userMapper.getAllUsers();
    
    // 插入
    User newUser = new User("Tom", "tom@example.com");
    userMapper.insertUser(newUser);
    
    // 更新
    user.setName("Updated Name");
    userMapper.updateUser(user);
    
    // 删除
    userMapper.deleteUser(2);
    
    session.commit();
} finally {
    session.close();
}

Mapper接口的好处是可以使用Java接口和方法,而不必直接使用字符串调用SQL语句,这样可以提供编译时类型检查,减少运行时错误。

2.5 Executor

Executor是MyBatis的核心接口之一,负责执行SQL语句。MyBatis有三种内置的Executor类型:

  1. SIMPLE:默认的Executor,每执行一次更新操作(update、insert、delete)就提交一次事务。
  2. REUSE:重用预处理语句(PreparedStatement)。
  3. BATCH:批量执行所有更新语句。
// 在配置文件中设置默认的Executor类型
<settings>
  <setting name="defaultExecutorType" value="REUSE" />
</settings>

// 在代码中指定Executor类型
SqlSession batchSession = sqlSessionFactory.openSession(ExecutorType.BATCH);

虽然Executor是MyBatis内部的接口,但了解它的作用有助于理解MyBatis的工作原理和优化查询性能。

2.6 StatementHandler

StatementHandler负责处理JDBC Statement的创建、设置参数、执行SQL语句以及获取结果。

// MyBatis内部的StatementHandler接口
public interface StatementHandler {
  Statement prepare(Connection connection, Integer transactionTimeout);
  void parameterize(Statement statement);
  void batch(Statement statement);
  int update(Statement statement);
  <E> List<E> query(Statement statement, ResultHandler resultHandler);
  BoundSql getBoundSql();
  ParameterHandler getParameterHandler();
}

StatementHandler将Java对象转换为JDBC可以执行的Statement对象,它处理了从Java参数到SQL参数的转换,以及从SQL结果到Java对象的转换。

2.7 ParameterHandler

ParameterHandler负责将用户传入的参数转换为JDBC Statement中需要的参数。

// MyBatis内部的ParameterHandler接口
public interface ParameterHandler {
  Object getParameterObject();
  void setParameters(PreparedStatement ps);
}

ParameterHandler处理SQL语句中的占位符(?)和实际参数值之间的映射关系,它将Java对象的属性值设置到JDBC PreparedStatement中。

2.8 ResultSetHandler

ResultSetHandler负责将JDBC返回的ResultSet结果集转换为Java对象。

// MyBatis内部的ResultSetHandler接口
public interface ResultSetHandler {
  <E> List<E> handleResultSets(Statement stmt);
  <E> Cursor<E> handleCursorResultSets(Statement stmt);
  void handleOutputParameters(CallableStatement cs);
}

ResultSetHandler将数据库查询结果映射为Java对象,它处理了从数据库字段到Java对象属性的转换。

3. MyBatis工作流程详解

MyBatis的工作流程可以分为初始化阶段和执行阶段两部分。

3.1 初始化阶段

初始化阶段主要完成以下工作:

  1. 解析配置文件:MyBatis首先解析mybatis-config.xml配置文件,读取全局配置和映射文件位置。
  2. 解析映射文件:解析所有Mapper.xml映射文件,建立SQL语句和对应方法的映射关系。
  3. 创建会话工厂:创建SqlSessionFactory实例,准备生成SqlSession。
// 初始化阶段示例代码
// 1. 读取配置文件
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);

// 2. 创建SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();

// 3. 创建SqlSessionFactory对象
SqlSessionFactory factory = builder.build(inputStream);

// 至此,初始化阶段完成

在初始化阶段,MyBatis会解析所有XML配置文件,构建内部的Configuration对象,该对象包含了MyBatis运行所需的所有配置信息。

3.1.1 配置文件解析过程

MyBatis会按照以下顺序解析配置文件中的元素:

  1. properties:读取属性配置文件。
  2. settings:全局参数设置。
  3. typeAliases:类型别名。
  4. typeHandlers:类型处理器。
  5. objectFactory:对象工厂。
  6. plugins:插件。
  7. environments:环境配置(数据源、事务管理器)。
  8. databaseIdProvider:数据库厂商标识。
  9. mappers:映射器。
<!-- 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="org/mybatis/example/config.properties">
    <property name="username" value="dev_user"/>
    <property name="password" value="F2Fa3!33TYyg"/>
  </properties>
  
  <!-- 全局参数设置 -->
  <settings>
    <setting name="cacheEnabled" value="true"/>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="aggressiveLazyLoading" value="false"/>
  </settings>
  
  <!-- 类型别名 -->
  <typeAliases>
    <typeAlias alias="User" type="org.mybatis.example.User"/>
    <package name="org.mybatis.example"/>
  </typeAliases>
  
  <!-- 环境配置 -->
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  
  <!-- 映射器 -->
  <mappers>
    <mapper resource="org/mybatis/example/UserMapper.xml"/>
    <package name="org.mybatis.example"/>
  </mappers>
</configuration>
3.1.2 映射文件解析过程

映射文件(Mapper.xml)定义了SQL语句和参数映射、结果映射等。MyBatis会解析这些文件,构建内存中的映射关系。

<!-- UserMapper.xml示例 -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.UserMapper">
  <select id="getUserById" resultType="User">
    SELECT * FROM users WHERE id = #{id}
  </select>
  
  <select id="getAllUsers" resultType="User">
    SELECT * FROM users
  </select>
  
  <insert id="insertUser" parameterType="User">
    INSERT INTO users (name, email) VALUES (#{name}, #{email})
  </insert>
  
  <update id="updateUser" parameterType="User">
    UPDATE users SET name = #{name}, email = #{email} WHERE id = #{id}
  </update>
  
  <delete id="deleteUser" parameterType="int">
    DELETE FROM users WHERE id = #{id}
  </delete>
</mapper>

解析过程中,MyBatis会将每个SQL语句解析为一个MappedStatement对象,该对象包含了SQL语句的id、参数映射、结果映射、SQL语句类型等信息。

3.2 执行阶段

执行阶段主要完成以下工作:

  1. 创建会话:从SqlSessionFactory获取SqlSession。
  2. 绑定映射器:获取Mapper接口的代理对象。
  3. 执行SQL:调用Mapper方法,执行对应的SQL语句。
  4. 处理结果:将查询结果映射为Java对象。
  5. 释放资源:关闭SqlSession。
// 执行阶段示例代码
// 1. 从SqlSessionFactory获取SqlSession
SqlSession session = factory.openSession();

try {
    // 2. 获取Mapper接口代理对象
    UserMapper userMapper = session.getMapper(UserMapper.class);
    
    // 3. 执行SQL语句
    User user = userMapper.getUserById(1);
    
    // 4. 处理结果
    System.out.println("User found: " + user.getName());
    
    // 执行更新操作
    user.setName("New Name");
    userMapper.updateUser(user);
    
    // 提交事务
    session.commit();
} catch (Exception e) {
    // 回滚事务
    session.rollback();
} finally {
    // 5. 关闭SqlSession
    session.close();
}
3.2.1 Mapper代理对象的创建

当我们调用session.getMapper(UserMapper.class)时,MyBatis会为UserMapper接口创建一个动态代理对象。代理对象会拦截接口方法的调用,转而执行对应的SQL语句。

// MyBatis内部为Mapper创建代理对象的示意代码
public class MapperProxy<T> implements InvocationHandler {
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;
    
    // 构造方法等省略...
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 如果是Object类方法,直接调用
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        }
        
        // 从缓存中获取MapperMethod
        MapperMethod mapperMethod = methodCache.computeIfAbsent(method,
            k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
        
        // 执行SQL语句
        return mapperMethod.execute(sqlSession, args);
    }
}
3.2.2 SQL语句的执行流程

当调用Mapper接口的方法时,MyBatis会执行以下步骤:

  1. 获取MappedStatement:根据方法的完全限定名找到对应的MappedStatement。
  2. 创建Executor:根据配置创建Executor实例。
  3. 创建StatementHandler:根据SQL类型创建StatementHandler实例。
  4. 创建ParameterHandler:负责处理参数映射。
  5. 创建ResultSetHandler:负责处理结果集映射。
  6. 执行SQL:调用JDBC API执行SQL语句。
  7. 处理结果:将ResultSet映射为Java对象。
// 以查询为例,MyBatis内部执行SQL的简化流程

// 1. 从Configuration获取MappedStatement
MappedStatement ms = configuration.getMappedStatement("org.mybatis.example.UserMapper.getUserById");

// 2. 创建执行器
Executor executor = configuration.newExecutor(transaction, executorType);

// 3. 创建StatementHandler
StatementHandler handler = configuration.newStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);

// 4. 准备Statement
Statement stmt = handler.prepare(connection, transactionTimeout);

// 5. 设置参数
handler.parameterize(stmt);

// 6. 执行查询
List<User> users = handler.query(stmt, resultHandler);

// 7. 关闭Statement
stmt.close();
3.2.3 一次完整的查询示例分析

以下是一个完整的查询操作示例,从调用Mapper方法到返回结果的全过程:

// 应用代码
UserMapper userMapper = session.getMapper(UserMapper.class);
User user = userMapper.getUserById(1);

// 内部执行流程:
// 1. 获取MapperProxy代理对象
// 2. 调用MapperProxy.invoke方法
// 3. 获取MapperMethod对象
// 4. 调用MapperMethod.execute方法
// 5. 根据方法类型(SELECT)调用SqlSession.selectOne方法
// 6. SqlSession委托给Executor执行查询
// 7. Executor创建StatementHandler
// 8. StatementHandler创建Statement并设置参数
// 9. StatementHandler执行查询并返回结果集
// 10. ResultSetHandler将结果集映射为User对象
// 11. 返回User对象给应用代码

这个流程展示了MyBatis如何将一个简单的接口方法调用转换为复杂的数据库操作,同时隐藏了底层的JDBC细节。

4. 配置文件详解

MyBatis的配置文件是其工作的基础,主要包括主配置文件(mybatis-config.xml)和映射文件(Mapper.xml)。

4.1 主配置文件

mybatis-config.xml是MyBatis的全局配置文件,它定义了MyBatis的行为方式。

4.1.1 properties元素

properties元素用于定义属性,这些属性可以在整个配置文件中被引用。

<properties resource="org/mybatis/example/config.properties">
  <property name="username" value="dev_user"/>
  <property name="password" value="F2Fa3!33TYyg"/>
</properties>

<!-- 引用属性 -->
<dataSource type="POOLED">
  <property name="driver" value="${driver}"/>
  <property name="url" value="${url}"/>
  <property name="username" value="${username}"/>
  <property name="password" value="${password}"/>
</dataSource>

properties元素可以从外部文件加载属性,也可以直接在配置文件中定义属性。

4.1.2 settings元素

settings元素用于更改MyBatis的运行时行为。

<settings>
  <setting name="cacheEnabled" value="true"/>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="multipleResultSetsEnabled" value="true"/>
  <setting name="useColumnLabel" value="true"/>
  <setting name="useGeneratedKeys" value="false"/>
  <setting name="autoMappingBehavior" value="PARTIAL"/>
  <setting name="defaultExecutorType" value="SIMPLE"/>
  <setting name="defaultStatementTimeout" value="25"/>
  <setting name="defaultFetchSize" value="100"/>
  <setting name="safeRowBoundsEnabled" value="false"/>
  <setting name="mapUnderscoreToCamelCase" value="false"/>
  <setting name="localCacheScope" value="SESSION"/>
  <setting name="jdbcTypeForNull" value="OTHER"/>
  <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>

这些设置会改变MyBatis的行为,例如启用缓存、懒加载、自动映射等。

4.1.3 typeAliases元素

typeAliases元素用于为Java类型定义别名,简化XML配置。

<typeAliases>
  <typeAlias alias="User" type="org.mybatis.example.User"/>
  
  <!-- 批量扫描别名 -->
  <package name="org.mybatis.example"/>
</typeAliases>

使用typeAlias可以为单个类定义别名,而使用package可以为整个包中的类定义别名。

4.1.4 typeHandlers元素

typeHandlers元素用于定义类型处理器,它们负责Java类型和JDBC类型之间的转换。

<typeHandlers>
  <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
  
  <!-- 批量扫描类型处理器 -->
  <package name="org.mybatis.example"/>
</typeHandlers>

MyBatis已经为常见的Java类型提供了默认的类型处理器,但我们也可以自定义类型处理器来处理特殊类型。

4.1.5 objectFactory元素

objectFactory元素用于自定义对象工厂,它负责创建结果对象实例。

<objectFactory type="org.mybatis.example.ExampleObjectFactory">
  <property name="someProperty" value="100"/>
</objectFactory>

大多数情况下,我们不需要自定义对象工厂,使用默认的对象工厂就足够了。

4.1.6 plugins元素

plugins元素用于定义MyBatis插件,插件可以拦截MyBatis的某些方法调用,改变其行为。

<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

MyBatis允许我们拦截以下方法的调用:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)
4.1.7 environments元素

environments元素用于配置不同的环境,每个环境都需要一个事务管理器和一个数据源。

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC"/>
    <dataSource type="POOLED">
      <property name="driver" value="${driver}"/>
      <property name="url" value="${url}"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    </dataSource>
  </environment>
  
  <environment id="production">
    <transactionManager type="MANAGED"/>
    <dataSource type="JNDI">
      <property name="data_source" value="java:comp/env/jdbc/MyBatisDemoDS"/>
    </dataSource>
  </environment>
</environments>

environments元素可以配置多个环境,通过default属性指定默认环境。

4.1.8 databaseIdProvider元素

databaseIdProvider元素用于支持不同的数据库,使MyBatis可以根据不同的数据库执行不同的SQL语句。

<databaseIdProvider type="DB_VENDOR">
  <property name="SQL Server" value="sqlserver"/>
  <property name="DB2" value="db2"/>
  <property name="Oracle" value="oracle" />
  <property name="MySQL" value="mysql" />
</databaseIdProvider>

使用databaseIdProvider后,我们可以在映射文件中为不同的数据库编写不同的SQL语句。

4.1.9 mappers元素

mappers元素用于指定映射文件的位置,MyBatis会加载这些文件中的SQL映射。

<mappers>
  <!-- 使用相对路径 -->
  <mapper resource="org/mybatis/example/UserMapper.xml"/>
  
  <!-- 使用URL -->
  <mapper url="file:///var/mappers/UserMapper.xml"/>
  
  <!-- 使用映射器接口的完全限定类名 -->
  <mapper class="org.mybatis.example.UserMapper"/>
  
  <!-- 批量扫描映射器 -->
  <package name="org.mybatis.example"/>
</mappers>

mappers元素可以通过resource、url、class或package属性指定映射文件的位置。

4.2 映射文件

映射文件(Mapper.xml)用于定义SQL语句、参数映射和结果映射等。

4.2.1 mapper元素

mapper元素是映射文件的根元素,namespace属性指定了该映射文件对应的Mapper接口。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.UserMapper">
  <!-- SQL映射语句 -->
</mapper>

namespace属性必须指定为Mapper接口的完全限定名,这样MyBatis才能将SQL映射语句与Mapper接口的方法关联起来。

4.2.2 select元素

select元素用于定义查询语句,它有很多属性用于配置查询行为。

<select id="getUserById" parameterType="int" resultType="User">
  SELECT * FROM users WHERE id = #{id}
</select>

<select id="getUsersByName" parameterType="string" resultType="User">
  SELECT * FROM users WHERE name LIKE #{name}
</select>

<select id="getAllUsers" resultType="User">
  SELECT * FROM users
</select>

select元素的常用属性包括:

  • id:SQL语句的唯一标识,通常与Mapper接口的方法名相同。
  • parameterType:参数的类型。
  • resultType:结果的类型。
  • resultMap:结果映射的ID。
  • flushCache:是否刷新缓存。
  • useCache:是否使用缓存。
  • timeout:超时时间。
  • fetchSize:获取记录的数量。
  • statementType:语句类型(STATEMENT、PREPARED、CALLABLE)。
  • resultSetType:结果集类型(FORWARD_ONLY、SCROLL_INSENSITIVE、SCROLL_SENSITIVE)。
4.2.3 insert、update、delete元素

insert、update、delete元素用于定义插入、更新和删除语句。

<insert id="insertUser" parameterType="User">
  INSERT INTO users (name, email)
  VALUES (#{name}, #{email})
  <selectKey keyProperty="id" resultType="int" order="AFTER">
    SELECT LAST_INSERT_ID()
  </selectKey>
</insert>

<update id="updateUser" parameterType="User">
  UPDATE users SET
    name = #{name},
    email = #{email}
  WHERE id = #{id}
</update>

<delete id="deleteUser" parameterType="int">
  DELETE FROM users WHERE id = #{id}
</delete>

这些元素的常用属性与select元素类似,此外,insert元素还有一些特殊属性:

  • useGeneratedKeys:是否使用JDBC的getGeneratedKeys方法获取主键。
  • keyProperty:指定主键属性。
  • keyColumn:指定主键列名。

insert元素中的selectKey子元素用于获取生成的主键值。

4.2.4 sql元素

sql元素用于定义可重用的SQL片段,可以被其他语句引用。

<sql id="userColumns">id, name, email</sql>

<select id="getUserById" parameterType="int" resultType="User">
  SELECT <include refid="userColumns"/>
  FROM users WHERE id = #{id}
</select>

<select id="getAllUsers" resultType="User">
  SELECT <include refid="userColumns"/>
  FROM users
</select>

使用sql元素可以减少重复代码,提高可维护性。

4.2.5 resultMap元素

resultMap元素用于定义结果映射,它可以将查询结果映射为复杂的对象图。

<resultMap id="userResultMap" type="User">
  <id property="id" column="user_id" />
  <result property="name" column="user_name"/>
  <result property="email" column="user_email"/>
</resultMap>

<select id="getUserById" parameterType="int" resultMap="userResultMap">
  SELECT
    user_id,
    user_name,
    user_email
  FROM users WHERE user_id = #{id}
</select>

resultMap元素的常用子元素包括:

  • id:映射主键列。
  • result:映射普通列。
  • association:映射一对一关联。
  • collection:映射一对多关联。
  • discriminator:根据条件映射不同的结果。
4.2.6 cache元素

cache元素用于配置该命名空间的缓存。

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

cache元素的属性包括:

  • eviction:缓存回收策略(LRU、FIFO、SOFT、WEAK)。
  • flushInterval:刷新间隔,单位毫秒。
  • size:缓存引用数量。
  • readOnly:是否只读。
  • blocking:是否阻塞。
  • type:自定义缓存实现类。

5. 参数处理机制

MyBatis的参数处理机制负责将Java对象的属性值设置到SQL语句的参数占位符中。

5.1 #{}与${}的区别

MyBatis支持两种参数占位符:#{}和${}。

  • #{}:预编译参数占位符,防止SQL注入。
  • ${}:直接字符串替换,有SQL注入风险。
<!-- 使用#{} -->
<select id="getUserById" parameterType="int" resultType="User">
  SELECT * FROM users WHERE id = #{id}
</select>
<!-- 预编译SQL:SELECT * FROM users WHERE id = ? -->

<!-- 使用${} -->
<select id="getUsersByTableName" parameterType="string" resultType="User">
  SELECT * FROM ${tableName}
</select>
<!-- 直接替换:SELECT * FROM users -->

一般情况下,应该优先使用#{},只有在需要动态改变表名、列名等情况下才使用${}。

5.2 简单参数

当Mapper方法接收单个参数时,可以直接使用参数名作为占位符名称。

// Mapper接口
public interface UserMapper {
    User getUserById(int id);
}

// XML映射
<select id="getUserById" parameterType="int" resultType="User">
  SELECT * FROM users WHERE id = #{id}
</select>

对于单个参数,MyBatis不关心参数名,可以使用任意名称,如#{id}、#{value}或#{anyName}。

5.3 多个参数

当Mapper方法接收多个参数时,可以使用@Param注解为参数命名。

// Mapper接口
public interface UserMapper {
    List<User> getUsersByNameAndEmail(@Param("name") String name, @Param("email") String email);
}

// XML映射
<select id="getUsersByNameAndEmail" resultType="User">
  SELECT * FROM users WHERE name = #{name} AND email = #{email}
</select>

如果不使用@Param注解,MyBatis会将参数命名为arg0、arg1…或param1、param2…。

// 不使用@Param注解
public interface UserMapper {
    List<User> getUsersByNameAndEmail(String name, String email);
}

// XML映射(使用arg名称)
<select id="getUsersByNameAndEmail" resultType="User">
  SELECT * FROM users WHERE name = #{arg0} AND email = #{arg1}
</select>

// 或者使用param名称
<select id="getUsersByNameAndEmail" resultType="User">
  SELECT * FROM users WHERE name = #{param1} AND email = #{param2}
</select>

为了提高可读性,建议使用@Param注解为参数命名。

5.4 JavaBean参数

当Mapper方法接收JavaBean对象作为参数时,可以使用属性名作为占位符名称。

// User类
public class User {
    private int id;
    private String name;
    private String email;
    // getter和setter方法
}

// Mapper接口
public interface UserMapper {
    void insertUser(User user);
}

// XML映射
<insert id="insertUser" parameterType="User">
  INSERT INTO users (name, email)
  VALUES (#{name}, #{email})
</insert>

MyBatis会使用JavaBean的getter方法获取属性值,设置到SQL参数中。

5.5 Map参数

当Mapper方法接收Map对象作为参数时,可以使用Map的键作为占位符名称。

// Mapper接口
public interface UserMapper {
    List<User> getUsersByCondition(Map<String, Object> condition);
}

// XML映射
<select id="getUsersByCondition" parameterType="map" resultType="User">
  SELECT * FROM users
  WHERE 1=1
  <if test="name != null">
    AND name = #{name}
  </if>
  <if test="email != null">
    AND email = #{email}
  </if>
</select>

// 调用方式
Map<String, Object> condition = new HashMap<>();
condition.put("name", "Tom");
condition.put("email", "tom@example.com");
List<User> users = userMapper.getUsersByCondition(condition);

使用Map参数可以灵活地传递不同的参数,但缺点是类型不安全,可读性不高。

5.6 数组和集合参数

当Mapper方法接收数组或集合作为参数时,可以使用特定的名称访问元素。

// Mapper接口
public interface UserMapper {
    List<User> getUsersByIds(List<Integer> ids);
}

// XML映射
<select id="getUsersByIds" resultType="User">
  SELECT * FROM users
  WHERE id IN
  <foreach collection="list" item="id" open="(" separator="," close=")">
    #{id}
  </foreach>
</select>

// 对于数组参数
<select id="getUsersByIds" resultType="User">
  SELECT * FROM users
  WHERE id IN
  <foreach collection="array" item="id" open="(" separator="," close=")">
    #{id}
  </foreach>
</select>

对于集合参数,MyBatis使用特定的名称:

  • List类型:collection=“list”
  • Array类型:collection=“array”
  • 如果使用@Param注解命名,则使用注解的名称:collection=“ids”

5.7 特殊参数处理

有时需要处理一些特殊类型的参数,如日期、枚举等。

// 处理日期参数
<insert id="insertUserWithBirthday">
  INSERT INTO users (name, birthday)
  VALUES (#{name}, #{birthday})
</insert>

// 处理枚举参数
<insert id="insertUserWithStatus">
  INSERT INTO users (name, status)
  VALUES (#{name}, #{status})
</insert>

MyBatis内置了很多TypeHandler来处理常见类型的转换,如日期、枚举等。我们也可以自定义TypeHandler来处理特殊类型。

// 自定义枚举TypeHandler
public class UserStatusTypeHandler extends BaseTypeHandler<UserStatus> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, UserStatus parameter, JdbcType jdbcType) throws SQLException {
        ps.setInt(i, parameter.getCode());
    }

    @Override
    public UserStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {
        int code = rs.getInt(columnName);
        return UserStatus.fromCode(code);
    }

    @Override
    public UserStatus getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        int code = rs.getInt(columnIndex);
        return UserStatus.fromCode(code);
    }

    @Override
    public UserStatus getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        int code = cs.getInt(columnIndex);
        return UserStatus.fromCode(code);
    }
}

// 注册TypeHandler
<typeHandlers>
  <typeHandler handler="org.mybatis.example.UserStatusTypeHandler" javaType="org.mybatis.example.UserStatus"/>
</typeHandlers>

6. 结果映射机制

MyBatis的结果映射机制负责将数据库查询结果转换为Java对象。

6.1 自动映射

MyBatis可以自动将查询结果映射为JavaBean对象,只要列名与属性名匹配(忽略大小写)。

// User类
public class User {
    private int id;
    private String name;
    private String email;
    // getter和setter方法
}

// XML映射
<select id="getUserById" parameterType="int" resultType="User">
  SELECT id, name, email FROM users WHERE id = #{id}
</select>

在这个例子中,MyBatis会自动将id列映射到id属性,name列映射到name属性,email列映射到email属性。

6.2 列名和属性名不匹配

当列名与属性名不匹配时,可以使用别名或resultMap解决。

<!-- 使用别名 -->
<select id="getUserById" parameterType="int" resultType="User">
  SELECT
    id AS id,
    username AS name,
    user_email AS email
  FROM users WHERE id = #{id}
</select>

<!-- 使用resultMap -->
<resultMap id="userResultMap" type="User">
  <id property="id" column="id" />
  <result property="name" column="username"/>
  <result property="email" column="user_email"/>
</resultMap>

<select id="getUserById" parameterType="int" resultMap="userResultMap">
  SELECT id, username, user_email
  FROM users WHERE id = #{id}
</select>

resultMap元素的常用子元素包括:

  • id:映射主键列。
  • result:映射普通列。
  • association:映射一对一关联。
  • collection:映射一对多关联。

6.3 一对一关联

当需要映射一对一关联关系时,可以使用association元素。

// User类
public class User {
    private int id;
    private String name;
    private String email;
    private Address address;
    // getter和setter方法
}

// Address类
public class Address {
    private int id;
    private String street;
    private String city;
    private String country;
    // getter和setter方法
}

// XML映射
<resultMap id="userWithAddressMap" type="User">
  <id property="id" column="user_id" />
  <result property="name" column="user_name"/>
  <result property="email" column="user_email"/>
  <association property="address" javaType="Address">
    <id property="id" column="addr_id"/>
    <result property="street" column="street"/>
    <result property="city" column="city"/>
    <result property="country" column="country"/>
  </association>
</resultMap>

<select id="getUserWithAddress" parameterType="int" resultMap="userWithAddressMap">
  SELECT
    u.id AS user_id,
    u.name AS user_name,
    u.email AS user_email,
    a.id AS addr_id,
    a.street,
    a.city,
    a.country
  FROM users u
  LEFT JOIN addresses a ON u.address_id = a.id
  WHERE u.id = #{id}
</select>

association元素定义了User对象与Address对象之间的一对一关联关系。

6.4 一对多关联

当需要映射一对多关联关系时,可以使用collection元素。

// User类
public class User {
    private int id;
    private String name;
    private String email;
    private List<Order> orders;
    // getter和setter方法
}

// Order类
public class Order {
    private int id;
    private Date createTime;
    private BigDecimal amount;
    // getter和setter方法
}

// XML映射
<resultMap id="userWithOrdersMap" type="User">
  <id property="id" column="user_id" />
  <result property="name" column="user_name"/>
  <result property="email" column="user_email"/>
  <collection property="orders" ofType="Order">
    <id property="id" column="order_id"/>
    <result property="createTime" column="create_time"/>
    <result property="amount" column="amount"/>
  </collection>
</resultMap>

<select id="getUserWithOrders" parameterType="int" resultMap="userWithOrdersMap">
  SELECT
    u.id AS user_id,
    u.name AS user_name,
    u.email AS user_email,
    o.id AS order_id,
    o.create_time,
    o.amount
  FROM users u
  LEFT JOIN orders o ON u.id = o.user_id
  WHERE u.id = #{id}
</select>

collection元素定义了User对象与Order对象之间的一对多关联关系。

6.5 鉴别器映射

当需要根据某个列的值进行不同的映射时,可以使用discriminator元素。

// Vehicle类(抽象类)
public abstract class Vehicle {
    private int id;
    private String brand;
    // getter和setter方法
}

// Car类(继承Vehicle)
public class Car extends Vehicle {
    private int doors;
    // getter和setter方法
}

// Truck类(继承Vehicle)
public class Truck extends Vehicle {
    private double cargoCapacity;
    // getter和setter方法
}

// XML映射
<resultMap id="vehicleResultMap" type="Vehicle">
  <id property="id" column="id" />
  <result property="brand" column="brand"/>
  <discriminator javaType="int" column="vehicle_type">
    <case value="1" resultType="Car">
      <result property="doors" column="doors"/>
    </case>
    <case value="2" resultType="Truck">
      <result property="cargoCapacity" column="cargo_capacity"/>
    </case>
  </discriminator>
</resultMap>

<select id="getVehicleById" parameterType="int" resultMap="vehicleResultMap">
  SELECT
    id,
    brand,
    vehicle_type,
    doors,
    cargo_capacity
  FROM vehicles
  WHERE id = #{id}
</select>

discriminator元素根据vehicle_type列的值,决定是映射为Car对象还是Truck对象。

6.6 延迟加载

MyBatis支持延迟加载(懒加载)关联对象,即只有在真正使用关联对象时才会去查询数据库。

<!-- 开启延迟加载 -->
<settings>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="aggressiveLazyLoading" value="false"/>
</settings>

<!-- 一对一关联的延迟加载 -->
<resultMap id="userResultMap" type="User">
  <id property="id" column="id" />
  <result property="name" column="name"/>
  <result property="email" column="email"/>
  <association property="address" javaType="Address" 
               select="getAddressById" column="address_id"/>
</resultMap>

<select id="getUserById" parameterType="int" resultMap="userResultMap">
  SELECT id, name, email, address_id
  FROM users WHERE id = #{id}
</select>

<select id="getAddressById" parameterType="int" resultType="Address">
  SELECT id, street, city, country
  FROM addresses WHERE id = #{id}
</select>

<!-- 一对多关联的延迟加载 -->
<resultMap id="userResultMap" type="User">
  <id property="id" column="id" />
  <result property="name" column="name"/>
  <result property="email" column="email"/>
  <collection property="orders" ofType="Order" 
              select="getOrdersByUserId" column="id"/>
</resultMap>

<select id="getUserById" parameterType="int" resultMap="userResultMap">
  SELECT id, name, email
  FROM users WHERE id = #{id}
</select>

<select id="getOrdersByUserId" parameterType="int" resultType="Order">
  SELECT id, create_time, amount
  FROM orders WHERE user_id = #{id}
</select>

在这个例子中,当查询User对象时,不会立即查询Address对象或Order列表,只有在代码中实际访问这些属性时才会触发查询。

7. 动态SQL

MyBatis的动态SQL功能允许我们根据不同的条件生成不同的SQL语句。

7.1 if元素

if元素用于根据条件判断是否包含某部分SQL。

<select id="getUsersByCondition" parameterType="User" resultType="User">
  SELECT * FROM users
  WHERE 1=1
  <if test="name != null">
    AND name = #{name}
  </if>
  <if test="email != null">
    AND email = #{email}
  </if>
</select>

if元素的test属性支持OGNL表达式,可以使用逻辑运算符(&&, ||, !)、比较运算符(==, !=, <, >, <=, >=)等。

7.2 choose, when, otherwise元素

choose元素类似于Java中的switch语句,提供了多个条件中选择一个的能力。

<select id="getUsersByCondition" parameterType="User" resultType="User">
  SELECT * FROM users
  <where>
    <choose>
      <when test="id != null">
        id = #{id}
      </when>
      <when test="name != null">
        name = #{name}
      </when>
      <otherwise>
        1=1
      </otherwise>
    </choose>
  </where>
</select>

在这个例子中,如果id不为null,则使用id作为条件;如果id为null但name不为null,则使用name作为条件;如果id和name都为null,则使用1=1作为条件。

7.3 where元素

where元素用于动态生成WHERE子句,它会自动处理WHERE关键字以及AND/OR前缀。

<select id="getUsersByCondition" parameterType="User" resultType="User">
  SELECT * FROM users
  <where>
    <if test="name != null">
      AND name = #{name}
    </if>
    <if test="email != null">
      AND email = #{email}
    </if>
  </where>
</select>

where元素会自动去除开头的AND/OR关键字,如果所有条件都不满足,则不会生成WHERE子句。

7.4 set元素

set元素用于动态生成SET子句,它会自动处理SET关键字以及逗号后缀。

<update id="updateUser" parameterType="User">
  UPDATE users
  <set>
    <if test="name != null">
      name = #{name},
    </if>
    <if test="email != null">
      email = #{email},
    </if>
  </set>
  WHERE id = #{id}
</update>

set元素会自动去除结尾的逗号,如果所有条件都不满足,则不会生成SET子句。

7.5 foreach元素

foreach元素用于遍历集合,生成重复的SQL片段。

<select id="getUsersByIds" parameterType="list" resultType="User">
  SELECT * FROM users
  WHERE id IN
  <foreach collection="list" item="id" open="(" separator="," close=")">
    #{id}
  </foreach>
</select>

foreach元素的属性:

  • collection:要遍历的集合,可以是array、list、map等。
  • item:集合中的元素。
  • index:集合的索引。
  • open:开始字符。
  • separator:分隔符。
  • close:结束字符。

7.6 bind元素

bind元素用于创建一个变量,将OGNL表达式的值绑定到上下文中。

<select id="getUsersByName" parameterType="string" resultType="User">
  <bind name="pattern" value="'%' + name + '%'" />
  SELECT * FROM users
  WHERE name LIKE #{pattern}
</select>

bind元素创建了一个名为pattern的变量,值为’%’ + name + ‘%’,这样就可以在SQL中使用这个变量。

7.7 trim元素

trim元素用于自定义SQL片段的前缀和后缀。

<select id="getUsersByCondition" parameterType="User" resultType="User">
  SELECT * FROM users
  <trim prefix="WHERE" prefixOverrides="AND|OR">
    <if test="name != null">
      AND name = #{name}
    </if>
    <if test="email != null">
      AND email = #{email}
    </if>
  </trim>
</select>

trim元素的属性:

  • prefix:要添加的前缀。
  • prefixOverrides:要去除的前缀。
  • suffix:要添加的后缀。
  • suffixOverrides:要去除的后缀。

7.8 sql元素和include元素

sql元素用于定义可重用的SQL片段,include元素用于引用这些片段。

<sql id="userColumns">id, name, email</sql>

<select id="getUserById" parameterType="int" resultType="User">
  SELECT <include refid="userColumns"/>
  FROM users WHERE id = #{id}
</select>

<select id="getAllUsers" resultType="User">
  SELECT <include refid="userColumns"/>
  FROM users
</select>

使用sql元素和include元素可以避免重复编写SQL片段,提高代码的可维护性。

8. 缓存机制

MyBatis提供了一级缓存和二级缓存,用于提高查询性能。

8.1 一级缓存

一级缓存是SqlSession级别的缓存,即在同一个SqlSession中,相同的查询只会执行一次,后续查询会直接从缓存中获取结果。

// 使用一级缓存
SqlSession session = sqlSessionFactory.openSession();
try {
    // 第一次查询,会查询数据库
    User user1 = session.selectOne("org.mybatis.example.UserMapper.getUserById", 1);
    
    // 第二次查询相同的语句和参数,会直接从缓存返回结果
    User user2 = session.selectOne("org.mybatis.example.UserMapper.getUserById", 1);
    
    // user1和user2是同一个对象
    System.out.println(user1 == user2); // 输出true
} finally {
    session.close();
}

一级缓存的生命周期与SqlSession相同,当SqlSession关闭、提交或回滚事务、执行更新语句时,缓存会被清空。

8.2 一级缓存的配置

<settings>
  <setting name="localCacheScope" value="SESSION"/>
</settings>

当localCacheScope设置为STATEMENT时,每次查询结束后缓存都会被清空,相当于禁用了一级缓存。

8.3 二级缓存

二级缓存是命名空间级别的缓存,即在同一个命名空间(Mapper)中,不同的SqlSession可以共享缓存。

<!-- 在映射文件中开启二级缓存 -->
<mapper namespace="org.mybatis.example.UserMapper">
  <cache/>
  
  <!-- 查询语句 -->
  <select id="getUserById" parameterType="int" resultType="User">
    SELECT * FROM users WHERE id = #{id}
  </select>
</mapper>

要使用二级缓存,还需要在配置文件中启用缓存,并确保结果对象是可序列化的。

<settings>
  <setting name="cacheEnabled" value="true"/>
</settings>
// User类需要实现Serializable接口
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private int id;
    private String name;
    private String email;
    // getter和setter方法
}

8.4 二级缓存的配置

可以通过cache元素的属性配置二级缓存的行为。

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

cache元素的属性:

  • eviction:缓存回收策略(LRU、FIFO、SOFT、WEAK)。
  • flushInterval:刷新间隔,单位毫秒。
  • size:缓存引用数量。
  • readOnly:是否只读。
  • blocking:是否阻塞。
  • type:自定义缓存实现类。

8.5 自定义缓存

MyBatis允许我们使用自定义的缓存实现。

<cache type="org.mybatis.example.CustomCache">
  <property name="host" value="localhost"/>
  <property name="port" value="6379"/>
</cache>

自定义缓存类需要实现Cache接口。

public class CustomCache implements Cache {
    private final String id;
    private String host;
    private int port;
    
    public CustomCache(String id) {
        this.id = id;
    }
    
    @Override
    public String getId() {
        return id;
    }
    
    // 其他Cache接口方法实现
    // ...
    
    // 属性设置方法
    public void setHost(String host) {
        this.host = host;
    }
    
    public void setPort(int port) {
        this.port = port;
    }
}

8.6 缓存的使用场景与注意事项

缓存适用于以下场景:

  • 查询频率高,变更频率低的数据。
  • 不需要实时数据的查询。
  • 重复查询相同数据的场景。

缓存使用注意事项:

  • 在多表查询时要谨慎使用缓存,因为任何一个表的更新都会导致缓存失效。
  • 在分布式环境下,要确保缓存的一致性,可能需要使用分布式缓存。
  • 对于敏感数据,应该禁用缓存或使用加密缓存。

9. 插件机制

MyBatis的插件机制允许我们拦截MyBatis的执行,修改或增强其行为。

9.1 拦截器接口

MyBatis的插件需要实现Interceptor接口。

public interface Interceptor {
    Object intercept(Invocation invocation) throws Throwable;
    Object plugin(Object target);
    void setProperties(Properties properties);
}
  • intercept方法:拦截方法,在目标方法执行时被调用。
  • plugin方法:生成代理对象,决定是否拦截目标对象。
  • setProperties方法:设置属性,在初始化插件时被调用。

9.2 @Intercepts注解

@Intercepts注解用于指定要拦截的方法。

@Intercepts({
    @Signature(
        type = Executor.class,
        method = "update",
        args = {MappedStatement.class, Object.class}
    ),
    @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
    )
})
public class ExamplePlugin implements Interceptor {
    // 插件实现
}

@Intercepts注解包含一个或多个@Signature注解,每个@Signature注解指定要拦截的方法签名,包括类型(type)、方法名(method)和参数类型(args)。

9.3 可拦截的方法

MyBatis允许拦截以下方法:

  • Executor: update, query, flushStatements, commit, rollback, getTransaction, close, isClosed
  • ParameterHandler: getParameterObject, setParameters
  • ResultSetHandler: handleResultSets, handleOutputParameters
  • StatementHandler: prepare, parameterize, batch, update, query
// 拦截Executor.update方法的示例
@Intercepts({
    @Signature(
        type = Executor.class,
        method = "update",
        args = {MappedStatement.class, Object.class}
    )
})
public class ExecutorUpdateInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取目标对象
        Executor executor = (Executor) invocation.getTarget();
        // 获取方法参数
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        
        // 在执行前处理
        System.out.println("Before update: " + ms.getId());
        
        // 执行原方法
        Object result = invocation.proceed();
        
        // 在执行后处理
        System.out.println("After update: " + ms.getId());
        
        return result;
    }
    
    @Override
    public Object plugin(Object target) {
        // 判断目标对象是否是要拦截的类型
        if (target instanceof Executor) {
            // 返回代理对象
            return Plugin.wrap(target, this);
        }
        // 返回原对象
        return target;
    }
    
    @Override
    public void setProperties(Properties properties) {
        // 设置属性
    }
}

9.4 插件的配置

插件需要在MyBatis配置文件中注册。

<plugins>
  <plugin interceptor="org.mybatis.example.ExecutorUpdateInterceptor">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

插件的配置包括拦截器类名和插件属性。

9.5 插件的应用场景

插件的常见应用场景包括:

  • 性能监控:记录SQL执行时间,检测慢查询。
  • 分页功能:自动处理分页参数。
  • 数据加密:自动加密敏感数据。
  • 审计日志:记录数据变更操作。
  • 动态数据源:根据不同条件切换数据源。
// 性能监控插件示例
@Intercepts({
    @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
    )
})
public class PerformanceInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        Object result = invocation.proceed();
        
        long endTime = System.currentTimeMillis();
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        System.out.println("SQL执行时间: " + (endTime - startTime) + "ms, ID: " + ms.getId());
        
        return result;
    }
    
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    
    @Override
    public void setProperties(Properties properties) {
    }
}

10. 与Spring集成

MyBatis可以与Spring框架集成,简化配置和使用。

10.1 依赖配置

要使用MyBatis-Spring,需要添加相关依赖。

<!-- Maven依赖 -->
<dependencies>
    <!-- Spring核心依赖 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.9</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.3.9</version>
    </dependency>
    
    <!-- MyBatis依赖 -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.7</version>
    </dependency>
    
    <!-- MyBatis-Spring依赖 -->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>2.0.6</version>
    </dependency>
</dependencies>

10.2 Spring配置

配置MyBatis-Spring主要包括数据源、SqlSessionFactory、Mapper扫描等。

<!-- Spring配置文件 -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd
           http://mybatis.org/schema/mybatis-spring
           http://mybatis.org/schema/mybatis-spring.xsd">
    
    <!-- 扫描组件 -->
    <context:component-scan base-package="org.mybatis.example"/>
    
    <!-- 引入属性文件 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    
    <!-- 配置数据源 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    
    <!-- 配置SqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <property name="mapperLocations" value="classpath:org/mybatis/example/mapper/*.xml"/>
    </bean>
    
    <!-- 配置MapperScannerConfigurer -->
    <mybatis:scan base-package="org.mybatis.example.mapper"/>
</beans>

10.3 Java配置

如果使用Java配置(无XML),可以使用@Configuration注解配置MyBatis-Spring。

@Configuration
@MapperScan("org.mybatis.example.mapper")
public class MyBatisConfig {
    
    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/mybatis");
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        return dataSource;
    }
    
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource());
        factoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:org/mybatis/example/mapper/*.xml"));
        return factoryBean.getObject();
    }
    
    @Bean
    public SqlSessionTemplate sqlSessionTemplate() throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory());
    }
}

10.4 事务管理

MyBatis-Spring使用Spring的事务管理器管理事务。

<!-- Spring配置文件中的事务管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

<!-- 启用基于注解的事务管理 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
// 使用@Transactional注解管理事务
@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Transactional
    public void updateUser(User user) {
        userMapper.updateUser(user);
        // 如果发生异常,事务会自动回滚
        // ...
    }
}

10.5 Mapper注入

MyBatis-Spring支持将Mapper注入到Spring管理的Bean中。

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    public User getUserById(int id) {
        return userMapper.getUserById(id);
    }
    
    public List<User> getAllUsers() {
        return userMapper.getAllUsers();
    }
    
    public void insertUser(User user) {
        userMapper.insertUser(user);
    }
    
    public void updateUser(User user) {
        userMapper.updateUser(user);
    }
    
    public void deleteUser(int id) {
        userMapper.deleteUser(id);
    }
}

在Service层注入Mapper接口,无需手动创建SqlSession,MyBatis-Spring会自动处理。

11. 深入理解SqlSession

SqlSession是MyBatis的核心接口,深入理解它的工作原理有助于我们更好地使用MyBatis。

11.1 SqlSession的创建

SqlSession的创建过程如下:

// 1. 读取配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");

// 2. 创建SqlSessionFactoryBuilder
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();

// 3. 创建SqlSessionFactory
SqlSessionFactory factory = builder.build(inputStream);

// 4. 创建SqlSession
SqlSession session = factory.openSession();

在内部,SqlSessionFactory会创建一个Transaction对象,然后基于Transaction和配置的ExecutorType创建一个Executor对象,最后基于Executor创建SqlSession。

11.2 SqlSession的方法

SqlSession接口提供了许多方法,可以分为几类:

查询方法
// 查询单个对象
User user = session.selectOne("org.mybatis.example.UserMapper.getUserById", 1);

// 查询列表
List<User> users = session.selectList("org.mybatis.example.UserMapper.getAllUsers");

// 查询映射
Map<Integer, User> userMap = session.selectMap("org.mybatis.example.UserMapper.getAllUsers", "id");

// 查询游标
Cursor<User> userCursor = session.selectCursor("org.mybatis.example.UserMapper.getAllUsers");
更新方法
// 插入
int insertCount = session.insert("org.mybatis.example.UserMapper.insertUser", user);

// 更新
int updateCount = session.update("org.mybatis.example.UserMapper.updateUser", user);

// 删除
int deleteCount = session.delete("org.mybatis.example.UserMapper.deleteUser", 1);
事务方法
// 提交事务
session.commit();

// 回滚事务
session.rollback();

// 关闭会话
session.close();
Mapper方法
// 获取Mapper接口的代理对象
UserMapper userMapper = session.getMapper(UserMapper.class);

11.3 SqlSession的生命周期

SqlSession的生命周期应该是请求作用域的,即一个请求一个SqlSession,请求结束后关闭SqlSession。

SqlSession session = sqlSessionFactory.openSession();
try {
    // 使用SqlSession
    // ...
    
    // 提交事务
    session.commit();
} catch (Exception e) {
    // 回滚事务
    session.rollback();
    throw e;
} finally {
    // 关闭SqlSession
    session.close();
}

在Spring环境中,SqlSession的生命周期由Spring管理,无需手动创建和关闭。

11.4 SqlSession与线程安全

SqlSession不是线程安全的,不能在多个线程间共享同一个SqlSession实例。每个线程应该有自己的SqlSession实例。

// 错误示例:多个线程共享同一个SqlSession
SqlSession session = sqlSessionFactory.openSession();
// 线程1使用session
// 线程2也使用session
// 可能导致不可预期的问题

// 正确示例:每个线程有自己的SqlSession
// 线程1
SqlSession session1 = sqlSessionFactory.openSession();
try {
    // 使用session1
} finally {
    session1.close();
}

// 线程2
SqlSession session2 = sqlSessionFactory.openSession();
try {
    // 使用session2
} finally {
    session2.close();
}

在Spring环境中,SqlSession是绑定到当前线程的,因此是线程安全的。

12. 常见问题与解决方案

这一节我们将介绍MyBatis使用过程中的常见问题和解决方案。

12.1 参数绑定问题

问题:单个参数时无法识别参数名
// Mapper接口
User getUserById(int userId);

// XML映射
<select id="getUserById" parameterType="int" resultType="User">
  SELECT * FROM users WHERE id = #{userId} <!-- 这里无法识别userId -->
</select>

解决方案:

  • 使用任意名称,如#{value}、#{id}或#{任意名称}
  • 使用@Param注解指定参数名
// 使用@Param注解
User getUserById(@Param("userId") int userId);

// XML映射
<select id="getUserById" parameterType="int" resultType="User">
  SELECT * FROM users WHERE id = #{userId} <!-- 这里可以识别userId -->
</select>
问题:多个参数时无法识别参数名
// Mapper接口
List<User> getUsersByNameAndEmail(String name, String email);

// XML映射
<select id="getUsersByNameAndEmail" resultType="User">
  SELECT * FROM users WHERE name = #{name} AND email = #{email} <!-- 这里无法识别name和email -->
</select>

解决方案:

  • 使用@Param注解指定参数名
  • 使用Map传递多个参数
  • 使用JavaBean对象传递多个参数
// 使用@Param注解
List<User> getUsersByNameAndEmail(@Param("name") String name, @Param("email") String email);

// 使用Map传递多个参数
List<User> getUsersByCondition(Map<String, Object> condition);

// 使用JavaBean对象传递多个参数
List<User> getUsersByCondition(User user);

12.2 结果映射问题

问题:列名与属性名不一致
// User类
public class User {
    private int id;
    private String userName; // 与数据库列名不一致
    private String userEmail; // 与数据库列名不一致
    // getter和setter方法
}

// 数据库表
// users表:id, name, email

解决方案:

  • 使用别名
  • 使用resultMap
  • 启用驼峰命名自动映射
<!-- 使用别名 -->
<select id="getUserById" resultType="User">
  SELECT
    id AS id,
    name AS userName,
    email AS userEmail
  FROM users WHERE id = #{id}
</select>

<!-- 使用resultMap -->
<resultMap id="userResultMap" type="User">
  <id property="id" column="id"/>
  <result property="userName" column="name"/>
  <result property="userEmail" column="email"/>
</resultMap>

<select id="getUserById" resultMap="userResultMap">
  SELECT id, name, email
  FROM users WHERE id = #{id}
</select>

<!-- 启用驼峰命名自动映射 -->
<settings>
  <setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

12.3 性能问题

问题:查询结果集过大

解决方案:

  • 分页查询
  • 使用游标
  • 优化SQL查询条件
// 分页查询
List<User> getUsersByPage(@Param("offset") int offset, @Param("limit") int limit);

// 使用游标
Cursor<User> getAllUsersCursor();
<!-- 分页查询 -->
<select id="getUsersByPage" resultType="User">
  SELECT * FROM users LIMIT #{offset}, #{limit}
</select>
// 使用游标示例
try (Cursor<User> cursor = userMapper.getAllUsersCursor()) {
    for (User user : cursor) {
        // 处理用户
    }
}
问题:频繁创建SqlSession

解决方案:

  • 使用SqlSessionTemplate(Spring环境)
  • 使用ThreadLocal管理SqlSession
// 使用SqlSessionTemplate
@Bean
public SqlSessionTemplate sqlSessionTemplate() throws Exception {
    return new SqlSessionTemplate(sqlSessionFactory());
}

// 注入并使用SqlSessionTemplate
@Autowired
private SqlSessionTemplate sqlSessionTemplate;

public User getUserById(int id) {
    return sqlSessionTemplate.selectOne("org.mybatis.example.UserMapper.getUserById", id);
}

12.4 配置问题

问题:配置文件路径错误

解决方案:

  • 检查配置文件路径
  • 确保配置文件在classpath中
  • 使用绝对路径
// 使用相对路径
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");

// 使用绝对路径
InputStream inputStream = new FileInputStream("/path/to/mybatis-config.xml");
问题:映射文件未注册

解决方案:

  • 在配置文件中注册映射文件
  • 使用包扫描注册映射文件
  • 检查映射文件的namespace是否正确
<!-- 注册单个映射文件 -->
<mappers>
  <mapper resource="org/mybatis/example/UserMapper.xml"/>
</mappers>

<!-- 使用包扫描注册映射文件 -->
<mappers>
  <package name="org.mybatis.example"/>
</mappers>

12.5 动态SQL问题

问题:动态SQL条件不生效

解决方案:

  • 检查变量名是否正确
  • 检查OGNL表达式是否正确
  • 使用where元素替代where 1=1
<!-- 正确示例 -->
<select id="getUsersByCondition" parameterType="User" resultType="User">
  SELECT * FROM users
  <where>
    <if test="name != null">
      AND name = #{name}
    </if>
    <if test="email != null">
      AND email = #{email}
    </if>
  </where>
</select>
问题:foreach元素使用错误

解决方案:

  • 检查collection属性是否正确
  • 检查item属性是否正确
  • 对于单个参数使用@Param注解
<!-- 正确示例 -->
<select id="getUsersByIds" resultType="User">
  SELECT * FROM users
  WHERE id IN
  <foreach collection="list" item="id" open="(" separator="," close=")">
    #{id}
  </foreach>
</select>
// Mapper接口
List<User> getUsersByIds(@Param("list") List<Integer> ids);

13. 最佳实践

最后一节,我们总结一些MyBatis的最佳实践,帮助大家更好地使用MyBatis。

13.1 项目结构

推荐的项目结构:

src/main/java
  ├── org.example.domain        // 领域对象(实体类)
  ├── org.example.mapper        // Mapper接口
  ├── org.example.service       // 服务层
  └── org.example.controller    // 控制器层
src/main/resources
  ├── org.example.mapper        // Mapper XML文件
  ├── mybatis-config.xml        // MyBatis配置文件
  └── application.properties    // 应用配置文件

13.2 命名规范

  • 表名:使用下划线分隔单词,如user_info。
  • 列名:使用下划线分隔单词,如user_name。
  • Java类名:使用驼峰命名法,如UserInfo。
  • 属性名:使用驼峰命名法,如userName。
  • Mapper接口:使用XxxMapper命名,如UserMapper。
  • XML文件:与Mapper接口同名,如UserMapper.xml。
  • 方法名:使用动词+名词,如getUserById。

13.3 使用接口绑定

推荐使用接口绑定方式,而不是使用字符串调用SQL语句。

// 推荐:使用接口绑定
UserMapper userMapper = session.getMapper(UserMapper.class);
User user = userMapper.getUserById(1);

// 不推荐:使用字符串调用
User user = session.selectOne("org.example.mapper.UserMapper.getUserById", 1);

接口绑定的优点:

  • 类型安全
  • 编译时检查
  • 代码可读性更好
  • IDE支持更好(代码补全、重构等)

13.4 参数处理

  • 单个参数:可以不使用@Param注解
  • 多个参数:使用@Param注解为参数命名
  • 复杂对象:使用JavaBean传递多个相关参数
// 单个参数
User getUserById(int id);

// 多个参数
List<User> getUsersByNameAndEmail(@Param("name") String name, @Param("email") String email);

// 复杂对象
void insertUser(User user);

13.5 结果映射

  • 简单映射:使用resultType
  • 复杂映射:使用resultMap
  • 启用驼峰命名自动映射
<!-- 简单映射 -->
<select id="getUserById" resultType="User">
  SELECT * FROM users WHERE id = #{id}
</select>

<!-- 复杂映射 -->
<resultMap id="userWithOrdersMap" type="User">
  <id property="id" column="user_id" />
  <result property="name" column="user_name"/>
  <result property="email" column="user_email"/>
  <collection property="orders" ofType="Order">
    <id property="id" column="order_id"/>
    <result property="amount" column="amount"/>
  </collection>
</resultMap>

<!-- 启用驼峰命名自动映射 -->
<settings>
  <setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

13.6 动态SQL

  • 使用if元素进行条件判断
  • 使用where元素替代where 1=1
  • 使用set元素处理更新语句
  • 使用foreach元素处理集合
  • 使用sql元素重用SQL片段
<!-- 动态查询 -->
<select id="getUsersByCondition" parameterType="User" resultType="User">
  SELECT * FROM users
  <where>
    <if test="name != null">
      AND name = #{name}
    </if>
    <if test="email != null">
      AND email = #{email}
    </if>
  </where>
</select>

<!-- 动态更新 -->
<update id="updateUser" parameterType="User">
  UPDATE users
  <set>
    <if test="name != null">
      name = #{name},
    </if>
    <if test="email != null">
      email = #{email},
    </if>
  </set>
  WHERE id = #{id}
</update>

13.7 缓存使用

  • 慎用二级缓存,特别是在多表查询时
  • 对于读多写少的数据,可以使用缓存
  • 对于实时性要求高的数据,不要使用缓存
<!-- 配置二级缓存 -->
<cache
  eviction="LRU"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

13.8 分页查询

  • 使用RowBounds进行内存分页(小数据量)
  • 使用LIMIT进行物理分页(大数据量)
  • 使用分页插件
// 使用RowBounds进行内存分页
RowBounds rowBounds = new RowBounds(offset, limit);
List<User> users = userMapper.getAllUsers(rowBounds);

// 使用LIMIT进行物理分页
List<User> users = userMapper.getUsersByPage(offset, limit);
<!-- 使用LIMIT进行物理分页 -->
<select id="getUsersByPage" resultType="User">
  SELECT * FROM users LIMIT #{offset}, #{limit}
</select>

13.9 异常处理

  • 使用try-finally确保SqlSession正确关闭
  • 在Spring环境中,让Spring管理SqlSession
  • 使用日志记录SQL异常
// 使用try-finally确保SqlSession正确关闭
SqlSession session = sqlSessionFactory.openSession();
try {
    // 使用SqlSession
    // ...
    
    // 提交事务
    session.commit();
} catch (Exception e) {
    // 回滚事务
    session.rollback();
    // 记录异常
    logger.error("SQL执行异常", e);
    throw e;
} finally {
    // 关闭SqlSession
    session.close();
}

13.10 性能优化

  • 合理使用缓存
  • 优化SQL语句
  • 使用批量操作
  • 延迟加载
  • 使用游标处理大结果集
<!-- 批量插入 -->
<insert id="batchInsertUsers" parameterType="list">
  INSERT INTO users (name, email) VALUES
  <foreach collection="list" item="user" separator=",">
    (#{user.name}, #{user.email})
  </foreach>
</insert>

<!-- 启用延迟加载 -->
<settings>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="aggressiveLazyLoading" value="false"/>
</settings>

13.11 与Spring集成

  • 使用@Mapper注解标记Mapper接口
  • 使用@MapperScan注解扫描Mapper接口
  • 使用@Transactional注解管理事务
  • 使用SqlSessionTemplate替代SqlSession
// 使用@Mapper注解
@Mapper
public interface UserMapper {
    User getUserById(int id);
    // 其他方法
}

// 使用@MapperScan注解
@Configuration
@MapperScan("org.example.mapper")
public class MyBatisConfig {
    // 配置
}

// 使用@Transactional注解
@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Transactional
    public void updateUser(User user) {
        userMapper.updateUser(user);
    }
}

13.12 测试

  • 使用MyBatis-Spring-Test进行集成测试
  • 使用H2等内存数据库进行单元测试
  • 使用DbUnit准备测试数据
// 使用MyBatis-Spring-Test进行测试
@RunWith(SpringRunner.class)
@MybatisTest
public class UserMapperTest {
    
    @Autowired
    private UserMapper userMapper;
    
    @Test
    public void testGetUserById() {
        User user = userMapper.getUserById(1);
        assertNotNull(user);
        assertEquals("Tom", user.getName());
    }
}

总结

本文详细介绍了MyBatis的工作原理,包括核心组件、工作流程、配置文件、参数处理、结果映射、动态SQL、缓存机制、插件机制、与Spring集成等方面。

MyBatis是一个优秀的持久层框架,它提供了灵活的SQL映射和丰富的特性,使数据库操作变得简单而高效。通过深入理解MyBatis的工作原理,我们可以更好地使用这个框架,编写出高质量的数据访问层代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈凯哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值