MyBatis 从入门到精通:一篇就够的实战指南(Java)

MyBatis 从入门到精通:一篇就够的实战指南(Java)

目标读者:Java 初/中/高级开发、准备面试的同学、正在从 JPA 切换到 MyBatis 的团队。

文章结构:概念 → 快速上手 → 核心原理 → 高级特性 → 性能调优 → 常见问题 → 最佳实践 → 面试题。


目录

  • [1. 什么是 MyBatis?为什么选择它](#1. 什么是 MyBatis?为什么选择它)
  • [2. 快速开始(Spring Boot 版)](#2. 快速开始(Spring Boot 版))
  • [3. 核心概念与运行机制](#3. 核心概念与运行机制)
  • [4. 映射语法与动态 SQL](#4. 映射语法与动态 SQL)
  • [5. 类型处理器 TypeHandler(含自定义 JSON/枚举示例)](#5. 类型处理器 TypeHandler(含自定义 JSON/枚举示例))
  • [6. Spring 事务与多数据源整合](#6. Spring 事务与多数据源整合)
  • [7. 分页:LIMIT、RowBounds 与 PageHelper](#7. 分页:LIMIT、RowBounds 与 PageHelper)
  • [8. 缓存机制:一级/二级缓存](#8. 缓存机制:一级/二级缓存)
  • [9. 性能优化与批处理](#9. 性能优化与批处理)
  • [10. 插件(拦截器)机制](#10. 插件(拦截器)机制)
  • [11. 代码生成:MyBatis Generator 与替代方案](#11. 代码生成:MyBatis Generator 与替代方案)
  • [12. 常见问题排查(Troubleshooting)](#12. 常见问题排查(Troubleshooting))
  • [13. 工程落地与包结构建议](#13. 工程落地与包结构建议)
  • [14. 面试高频题与答题要点](#14. 面试高频题与答题要点)
  • [15. 纯 MyBatis(非 Spring)最小可运行示例](#15. 纯 MyBatis(非 Spring)最小可运行示例)
  • [16. 参考资料与学习路径](#16. 参考资料与学习路径)

1. 什么是 MyBatis?为什么选择它

MyBatis 是一个轻量级持久层框架,核心价值是:

  • SQL 自由:你自己写 SQL,掌控查询、索引、复杂联表、性能。
  • ORM 适度:相比 JPA/Hibernate,MyBatis 不做全自动映射,避免"黑盒",排查更直观。
  • 可扩展:插件拦截器、TypeHandler、动态 SQL、二级缓存,足够灵活。

什么时候优先选 MyBatis?

  • 报表/复杂查询/存储过程多、对 SQL 可控性要求强。
  • 性能极致场景:需要自己精细化调优 SQL。
  • 需要多租户/审计/数据权限等"跨切面"能力(拦截器很好用)。

什么时候考虑 JPA?

  • 以 CRUD 为主,领域模型聚合清晰,希望更少 SQL(但也要接受性能可预期性降低)。

2. 快速开始(Spring Boot 版)

目标:5 分钟跑通一个 User 的增删改查。

2.1 数据表

sql 复制代码
CREATE TABLE `user` (
  `id`          BIGINT PRIMARY KEY AUTO_INCREMENT,
  `username`    VARCHAR(50) NOT NULL,
  `email`       VARCHAR(100) NOT NULL,
  `status`      TINYINT NOT NULL DEFAULT 1,   -- 1:启用 0:停用
  `created_at`  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at`  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  UNIQUE KEY uk_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2.2 依赖(Maven)

xml 复制代码
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.3</version>
  </dependency>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
  </dependency>
</dependencies>

2.3 application.yml

yaml 复制代码
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: root
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
mybatis:
  mapper-locations: classpath:mapper/**/*.xml
  type-aliases-package: com.example.demo.domain
  configuration:
    map-underscore-to-camel-case: true
    default-fetch-size: 100
    default-statement-timeout: 10

2.4 实体类

java 复制代码
package com.example.demo.domain;

import lombok.Data;
import java.time.LocalDateTime;

@Data
public class User {
  private Long id;
  private String username;
  private String email;
  private Integer status;
  private LocalDateTime createdAt;
  private LocalDateTime updatedAt;
}

2.5 Mapper 接口

java 复制代码
package com.example.demo.mapper;

import com.example.demo.domain.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;

@Mapper
public interface UserMapper {
  int insert(User user);
  int updateById(User user);
  int deleteById(@Param("id") Long id);
  User selectById(@Param("id") Long id);
  List<User> selectByUsernameLike(@Param("keyword") String keyword);
}

2.6 Mapper XML(resources/mapper/UserMapper.xml)

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="com.example.demo.mapper.UserMapper">

  <resultMap id="BaseResultMap" type="com.example.demo.domain.User">
    <id column="id" property="id"/>
    <result column="username" property="username"/>
    <result column="email" property="email"/>
    <result column="status" property="status"/>
    <result column="created_at" property="createdAt"/>
    <result column="updated_at" property="updatedAt"/>
  </resultMap>

  <sql id="Base_Column_List">
    id, username, email, status, created_at, updated_at
  </sql>

  <insert id="insert" parameterType="com.example.demo.domain.User" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO user (username, email, status)
    VALUES (#{username}, #{email}, #{status})
  </insert>

  <update id="updateById" parameterType="com.example.demo.domain.User">
    UPDATE user
    <set>
      <if test="username != null">username = #{username},</if>
      <if test="email != null">email = #{email},</if>
      <if test="status != null">status = #{status},</if>
    </set>
    WHERE id = #{id}
  </update>

  <delete id="deleteById" parameterType="long">
    DELETE FROM user WHERE id = #{id}
  </delete>

  <select id="selectById" parameterType="long" resultMap="BaseResultMap">
    SELECT <include refid="Base_Column_List"/>
    FROM user WHERE id = #{id}
  </select>

  <select id="selectByUsernameLike" parameterType="string" resultMap="BaseResultMap">
    SELECT <include refid="Base_Column_List"/>
    FROM user
    <where>
      <if test="keyword != null and keyword != ''">
        AND username LIKE CONCAT('%', #{keyword}, '%')
      </if>
    </where>
    ORDER BY id DESC
    LIMIT 100
  </select>

</mapper>

2.7 Service & Controller(示例)

java 复制代码
package com.example.demo.service;

import com.example.demo.domain.User;
import com.example.demo.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
public class UserService {
  private final UserMapper userMapper;

  @Transactional
  public Long create(User user) {
    userMapper.insert(user);
    return user.getId();
  }

  @Transactional
  public void update(User user) {
    userMapper.updateById(user);
  }

  public User get(Long id) {
    return userMapper.selectById(id);
  }

  public List<User> search(String keyword) {
    return userMapper.selectByUsernameLike(keyword);
  }
}

3. 核心概念与运行机制

MyBatis 运行时关键对象

  • SqlSessionFactory :会话工厂(线程安全),由配置构建,创建 SqlSession

  • SqlSession:一次数据库会话(非线程安全),管理 Statement/事务。

  • Mapper 接口与代理 :MyBatis 使用 MapperProxy 动态代理接口方法 → MappedStatement → 执行器(Executor)。

  • 执行器 Executor :负责缓存、SQL 执行,类型有 SIMPLEREUSEBATCH

  • 处理器

    • ParameterHandler(参数预处理),
    • ResultSetHandler(结果映射),
    • TypeHandler(Java ↔ JDBC 类型转换)。

执行流程(简化)

  1. Mapper 方法被调用 → 动态代理找到 MappedStatement(由 XML/注解解析)。
  2. ParameterHandler 绑定 #{} 参数,生成 PreparedStatement
  3. Executor 执行,命中缓存则返回,否则访问数据库。
  4. ResultSetHandlerResultSet 映射为对象(resultMap/自动映射)。

4. 映射语法与动态 SQL

4.1 参数绑定:#{} vs ${}

  • #{}:预编译占位符,安全(防注入),自动做类型转换。
  • ${}:字符串拼接,谨慎使用(表名/列名动态时),注意 SQL 注入风险。

4.2 resultType vs resultMap

  • resultType:直接映射到类/基本类型,适合简单查询。
  • resultMap:可定义 <id/><result/><association/>(一对一)、<collection/>(一对多)、<discriminator/>(鉴别器),适合复杂对象图。

4.3 关联映射:一对一 / 一对多

xml 复制代码
<!-- 一对一:User 包含 Profile -->
<resultMap id="UserWithProfile" type="User">
  <id column="u_id" property="id"/>
  <result column="username" property="username"/>
  <association property="profile" javaType="Profile" columnPrefix="p_">
    <id column="p_id" property="id"/>
    <result column="p_phone" property="phone"/>
  </association>
</resultMap>

<select id="selectUserWithProfile" resultMap="UserWithProfile">
  SELECT u.id AS u_id, u.username,
         p.id AS p_id, p.phone AS p_phone
  FROM user u LEFT JOIN profile p ON u.id = p.user_id
  WHERE u.id = #{id}
</select>
xml 复制代码
<!-- 一对多:User 包含 roles 列表(通过嵌套查询或嵌套结果) -->
<resultMap id="UserWithRoles" type="User">
  <id column="u_id" property="id"/>
  <result column="username" property="username"/>
  <collection property="roles" ofType="Role">
    <id column="r_id" property="id"/>
    <result column="r_name" property="name"/>
  </collection>
</resultMap>

<select id="selectUserWithRoles" resultMap="UserWithRoles">
  SELECT u.id AS u_id, u.username,
         r.id AS r_id, r.name AS r_name
  FROM user u LEFT JOIN user_role ur ON u.id=ur.user_id
  LEFT JOIN role r ON ur.role_id=r.id
  WHERE u.id=#{id}
</select>

建议:能一次性 JOIN 出来就不要 N+1(嵌套查询),必要时才懒加载。

4.4 动态 SQL 常用标签

  • <if> / <choose-when-otherwise> 条件拼接。
  • <where> 自动处理多余 AND/OR。
  • <set> 动态更新字段(自动处理逗号)。
  • <trim prefix="(" suffix=")" suffixOverrides=","/> 高级裁剪。
  • <foreach> 循环(IN 查询、批量插入)。
  • <bind> 绑定变量(如 LIKE 模糊匹配)。

综合示例:复杂查询

xml 复制代码
<select id="queryUsers" resultMap="BaseResultMap">
  SELECT <include refid="Base_Column_List"/>
  FROM user
  <where>
    <if test="username != null and username != ''">
      AND username LIKE CONCAT('%', #{username}, '%')
    </if>
    <if test="emails != null and emails.size > 0">
      AND email IN
      <foreach collection="emails" item="e" open="(" close=")" separator=",">
        #{e}
      </foreach>
    </if>
    <if test="statusList != null and statusList.size>0">
      AND status IN
      <foreach collection="statusList" item="s" open="(" close=")" separator=",">
        #{s}
      </foreach>
    </if>
  </where>
  ORDER BY id DESC
  <if test="limit != null">LIMIT #{limit}</if>
  <if test="offset != null"> OFFSET #{offset}</if>
</select>

5. 类型处理器 TypeHandler(含自定义 JSON/枚举示例)

5.1 内置与枚举处理

  • 内置 Date, LocalDateTime, 基本类型 等常见映射开箱即用。
  • 枚举:使用 EnumTypeHandler(存字符串)或 EnumOrdinalTypeHandler(存序号)。不建议用序号,改名/顺序调整有风险。

枚举示例

java 复制代码
public enum UserStatus {
  DISABLED(0), ENABLED(1);
  private final int code;
  UserStatus(int code){this.code=code;}
  public int getCode(){return code;}
}
java 复制代码
// 自定义枚举 TypeHandler:用 code 入库
@MappedJdbcTypes(JdbcType.TINYINT)
@MappedTypes(UserStatus.class)
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 code==1?UserStatus.ENABLED:UserStatus.DISABLED;
  }
  @Override
  public UserStatus getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    int code = rs.getInt(columnIndex);
    return code==1?UserStatus.ENABLED:UserStatus.DISABLED;
  }
  @Override
  public UserStatus getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    int code = cs.getInt(columnIndex);
    return code==1?UserStatus.ENABLED:UserStatus.DISABLED;
  }
}

在 Mapper XML 中声明或通过全局配置扫描

xml 复制代码
<result column="status" property="status" typeHandler="com.example.demo.type.UserStatusTypeHandler"/>

5.2 JSON 字段映射(以 Jackson 为例)

java 复制代码
public class Profile {
  private String phone;
  private List<String> tags;
}
java 复制代码
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(Profile.class)
public class JsonProfileTypeHandler extends BaseTypeHandler<Profile> {
  private static final ObjectMapper MAPPER = new ObjectMapper();
  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Profile parameter, JdbcType jdbcType) throws SQLException {
    try { ps.setString(i, MAPPER.writeValueAsString(parameter)); }
    catch (JsonProcessingException e) { throw new SQLException(e); }
  }
  @Override
  public Profile getNullableResult(ResultSet rs, String columnName) throws SQLException {
    String json = rs.getString(columnName);
    if (json == null) return null;
    try { return MAPPER.readValue(json, Profile.class); }
    catch (IOException e) { throw new SQLException(e); }
  }
  @Override
  public Profile getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    String json = rs.getString(columnIndex);
    if (json == null) return null;
    try { return MAPPER.readValue(json, Profile.class); }
    catch (IOException e) { throw new SQLException(e); }
  }
  @Override
  public Profile getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    String json = cs.getString(columnIndex);
    if (json == null) return null;
    try { return MAPPER.readValue(json, Profile.class); }
    catch (IOException e) { throw new SQLException(e); }
  }
}

6. Spring 事务与多数据源整合

6.1 事务

  • 使用 @Transactional(默认运行时异常回滚)。
  • MyBatis 与 Spring 事务整合由 SqlSessionTemplate 完成,无需手工 commit/rollback

注意 :同类内部方法调用不会触发 AOP 事务,可用 @Transactional 放到 public 对外方法,或通过 self-injection 解决。

6.2 多数据源(读写分离示例)

思路:定义两个 DataSource,两个 SqlSessionFactory,分别 @MapperScan 指向不同包。

java 复制代码
@Configuration
@MapperScan(basePackages = "com.example.demo.mapper.read", sqlSessionFactoryRef = "readSqlSessionFactory")
public class ReadDataSourceConfig {
  @Bean
  public DataSource readDataSource() { /* 配置 HikariDataSource */ }
  @Bean
  public SqlSessionFactory readSqlSessionFactory(@Qualifier("readDataSource") DataSource ds) throws Exception {
    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    bean.setDataSource(ds);
    return bean.getObject();
  }
}

也可使用路由数据源(AbstractRoutingDataSource)+ 拦截器/注解在运行时切换。


7. 分页:LIMIT、RowBounds 与 PageHelper

  • 直接 LIMIT/OFFSET:最可控,适合大多数场景。
  • RowBounds :会把所有结果查出后在内存截断,大表慎用
  • PageHelper 插件:拦截 SQL 自动改写为分页语句,并统计总数,开发效率高。

PageHelper 示例

xml 复制代码
<!-- Maven -->
<dependency>
  <groupId>com.github.pagehelper</groupId>
  <artifactId>pagehelper-spring-boot-starter</artifactId>
  <version>1.4.7</version>
</dependency>
java 复制代码
Page<User> page = PageHelper.startPage(1, 20)
  .doSelectPage(() -> userMapper.selectByUsernameLike("tom"));
return new PageResult<>(page.getResult(), page.getTotal());

8. 缓存机制:一级/二级缓存

  • 一级缓存(SqlSession 级别) :同一会话内相同查询直接命中;会在 commit/close 后失效。
  • 二级缓存(Mapper 命名空间级别) :多个会话共享;需开启 <cache/>@CacheNamespace;更新/插入/删除默认会清空本命名空间缓存。

二级缓存开启示例

xml 复制代码
<mapper namespace="com.example.demo.mapper.UserMapper">
  <cache eviction="LRU" flushInterval="600000" size="1024" readOnly="false"/>
  <!-- 其余 SQL ... -->
</mapper>

注意 :跨表更新导致数据不一致时,使用 cache-ref 引用同一缓存区域,或干脆关闭二级缓存,改用业务层缓存(如 Redis)。


9. 性能优化与批处理

9.1 批处理

  • ExecutorType.BATCH:减少网络往返,成批提交。
java 复制代码
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
  UserMapper mapper = session.getMapper(UserMapper.class);
  for (User u: users) { mapper.insert(u); }
  session.commit();
}
  • 批量插入 SQL(更快):
xml 复制代码
<insert id="batchInsert">
  INSERT INTO user (username, email, status) VALUES
  <foreach collection="list" item="u" separator=",">
    (#{u.username}, #{u.email}, #{u.status})
  </foreach>
</insert>

9.2 其他优化要点

  • 尽量命中 覆盖索引,避免回表。
  • 合理选择 JOIN vs. 子查询 ,规避 N+1
  • <sql> 片段重用列清单,保持一致性。
  • 大列表分页:使用 游标/seek 方法where id > ? limit ?)替代传统 offset,提高性能。

10. 插件(拦截器)机制

MyBatis 允许对 4 大接口进行拦截:ExecutorStatementHandlerParameterHandlerResultSetHandler

示例:SQL 耗时打印 + 租户注入

java 复制代码
@Intercepts({
  @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SqlCostInterceptor implements Interceptor {
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    StatementHandler handler = (StatementHandler) Plugin.getTarget(invocation.getTarget());
    BoundSql boundSql = handler.getBoundSql();
    String sql = boundSql.getSql();
    long t1 = System.nanoTime();
    try { return invocation.proceed(); }
    finally {
      long t2 = System.nanoTime();
      System.out.println("SQL耗时(ms): " + (t2 - t1)/1_000_000 + " => " + sql);
    }
  }
  @Override
  public Object plugin(Object target) { return Plugin.wrap(target, this); }
  @Override
  public void setProperties(Properties properties) {}
}

注意:拦截器容易引入隐形开销与副作用,审慎上线,可灰度测试。


11. 代码生成:MyBatis Generator 与替代方案

11.1 MyBatis Generator(MBG)

优点:稳健、官方、能根据表结构生成 model/mapper/xml

缺点:生成代码风格偏老,需要二次改造;复杂 SQL 仍需手写。

MBG 配置(片段)

xml 复制代码
<generatorConfiguration>
  <context id="MySqlContext" targetRuntime="MyBatis3Simple">
    <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                    connectionURL="jdbc:mysql://localhost:3306/demo"
                    userId="root" password="root"/>
    <javaModelGenerator targetPackage="com.example.demo.domain" targetProject="src/main/java"/>
    <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"/>
    <javaClientGenerator targetPackage="com.example.demo.mapper" targetProject="src/main/java" type="XMLMAPPER"/>
    <table tableName="user" domainObjectName="User"/>
  </context>
</generatorConfiguration>

11.2 替代方案

  • MyBatis-Plus:增强 CRUD、代码生成、分页、Wrapper 条件构造器(学习/上手快)。
  • 自建代码生成器:基于模板(Freemarker/Beetl),按团队规范定制输出。

12. 常见问题排查(Troubleshooting)

  1. Parameter 'xxx' not found :方法参数未加 @Param,或 XML 使用的名字与实际不一致。
  2. Invalid bound statement (not found)namespace + id 未匹配;mapper-locations 扫描路径不对;包路径大小写错误。
  3. TooManyResultsExceptionselectOne 返回多行;请改 limit 1 或使用 selectList
  4. 驼峰映射不生效 :未开启 map-underscore-to-camel-case 或列别名未对齐。
  5. javaType/jdbcType 冲突jdbcTypeNULL 可避免某些数据库驱动因 null 无法推断类型而报错。
  6. 二级缓存脏读 :跨命名空间更新;使用 cache-ref 或关闭二级缓存。
  7. SQL 注入 :使用 ${} 拼接外部输入(尤其是 order by、表名)造成风险,优先 #{} 或白名单校验。
  8. 事务不生效 :同类内部调用;数据源未配置到 DataSourceTransactionManager;异常被吞。
  9. RowBounds 内存炸裂 :大数据量分页必须改 SQL → LIMIT 或使用插件。
  10. 批处理返回主键useGeneratedKeys + keyProperty;批量插入需注意驱动/数据库是否支持批量返回主键。

13. 工程落地与包结构建议

复制代码
com.example.demo
├── common        // 公共组件(拦截器、枚举、异常、工具)
├── config        // 数据源、MyBatis、事务、分页插件配置
├── domain        // 领域模型(DO)
├── dto|vo        // 入参/出参对象
├── mapper        // Mapper 接口 + XML(resources/mapper)
├── repository    // 组合多个 Mapper 的仓储实现(可选)
├── service       // 业务服务层(含 @Transactional)
├── web           // 控制器
└── starter       // 启动类

建议

  • 基础列片段统一 <sql id="Base_Column_List"/>,减少遗漏。
  • Mapper 拆分:BaseMapper(通用) + XxxMapper(个性化)。
  • 复杂 SQL 封装成视图/存储过程时,务必评估可维护性与迁移成本。

14. 面试高频题与答题要点

  1. MyBatis 与 Hibernate 区别? ------ SQL 自由 vs. 自动 ORM;可控性/性能 vs. 生产力。
  2. #{} 与 ${} 区别? ------ 预编译占位符(安全) vs. 字符串拼接(有注入风险)。
  3. 一级/二级缓存工作原理? ------ 会话级/命名空间级;更新语句清缓存;cache-ref
  4. 动态 SQL 标签 ------ if/choose/where/set/trim/foreach/bind 场景与常见坑。
  5. 插件能拦截哪些点? ------ Executor/StatementHandler/ParameterHandler/ResultSetHandler;谨慎使用。
  6. 分页实现方案? ------ LIMIT、RowBounds(慎用)、PageHelper(优点/缺点)。
  7. 批处理如何做? ------ ExecutorType.BATCH vs. 多值 INSERT;差异与回滚策略。
  8. 如何避免 N+1? ------ JOIN 一次查全;必要时懒加载但注意一致性与事务边界。
  9. 事务为什么没生效? ------ AOP 代理、同类调用、异常类型、事务管理器配置。
  10. TypeHandler 的作用与自定义示例 ------ 枚举/JSON 映射。

15. 纯 MyBatis(非 Spring)最小可运行示例

15.1 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>
  <environments default="dev">
    <environment id="dev">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/demo"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="mapper/UserMapper.xml"/>
  </mappers>
</configuration>

15.2 启动代码

java 复制代码
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession session = sqlSessionFactory.openSession()) {
  UserMapper mapper = session.getMapper(UserMapper.class);
  User u = mapper.selectById(1L);
  System.out.println(u);
}

16. 参考资料与学习路径

  • 官方文档:熟悉 XML 标签、配置项、缓存与插件机制。
  • 深入源码:MapperProxyExecutorConfigurationMappedStatementBoundSql
  • 实战演练:把线上一个复杂查询用 MyBatis 重写,做 Explain 分析 + 索引优化 + 压测。
  • 扩展阅读:MyBatis-Plus、ShardingSphere(分库分表)、多租户/数据权限设计。

最后

MyBatis 的真正价值在于"可控性 + 可观测性 + 可维护性"。

掌握本文的路线与示例,配合你所在业务的真实 SQL 场景,基本可以实现从入门 → 熟练 → 精通。祝使用愉快!🚀