Skip to content

ORM 从零到精通验收清单

这页用来验收你是否真的把 ORM 学到了能做项目、能排查、能面试、能解释原理的程度。ORM 不是“会写 Mapper”或“会用 Repository”,而是要能从 JDBC 痛点一路讲到 SQL 执行、结果映射、缓存、事务、批量写入、JPA 持久化上下文、N+1、慢 SQL 和线上排查。

学完后你应该能说清楚:

  1. JDBC 为什么繁琐,ORM 到底封装了哪些重复工作。
  2. MyBatis Mapper 接口没有实现类为什么能执行。
  3. XML 动态 SQL 如何变成数据库真正执行的 SQL。
  4. #{}${}TypeHandlerResultMap 分别解决什么问题。
  5. MyBatis 一级缓存、二级缓存为什么可能带来一致性问题。
  6. MyBatis 插件为什么能做分页、数据权限、慢 SQL 统计。
  7. MyBatis-Plus Wrapper、分页、逻辑删除、自动填充的本质是什么。
  8. JPA/Hibernate 为什么不用手写 update 也会更新数据库。
  9. 持久化上下文、脏检查、flush、commit、懒加载、N+1 的完整原理。
  10. 事务为什么应该放 Service 层,哪些情况会失效。
  11. 批量导入、分页查询、复杂报表、数据权限在商业项目里怎么做。
  12. ORM 出现慢 SQL、字段为空、事务没回滚、缓存旧数据时怎么排查。

总学习路线

mermaid
flowchart TD
    A["阶段1:JDBC 痛点"] --> B["阶段2:ORM 封装边界"]
    B --> C["阶段3:MyBatis Mapper 代理和 SQL 执行"]
    C --> D["阶段4:动态 SQL、参数绑定、结果映射"]
    D --> E["阶段5:缓存、插件、批量写入"]
    E --> F["阶段6:MyBatis-Plus 增强机制"]
    F --> G["阶段7:JPA/Hibernate 持久化上下文"]
    G --> H["阶段8:Spring Data JPA Repository"]
    H --> I["阶段9:事务边界和一致性"]
    I --> J["阶段10:性能优化和生产排查"]

这条路线的核心思想是:先理解数据库访问为什么复杂,再理解框架帮你封装了什么,最后必须回到 SQL、事务和执行计划。ORM 能提高效率,但不能替你理解数据库。

阶段1:JDBC 为什么繁琐

直接使用 JDBC 查询一条订单:

java
public OrderDO findById(Long id) throws SQLException {
    Connection connection = dataSource.getConnection();
    PreparedStatement ps = connection.prepareStatement(
            "select id, order_no, status, amount from t_order where id = ?"
    );
    ps.setLong(1, id);
    ResultSet rs = ps.executeQuery();

    OrderDO order = null;
    if (rs.next()) {
        order = new OrderDO();
        order.setId(rs.getLong("id"));
        order.setOrderNo(rs.getString("order_no"));
        order.setStatus(rs.getInt("status"));
        order.setAmount(rs.getBigDecimal("amount"));
    }

    rs.close();
    ps.close();
    connection.close();
    return order;
}

这段代码暴露出数据库访问的重复工作:

重复工作不封装会怎样
获取连接、释放连接容易连接泄漏,连接池被打满
创建 SQL拼接错误、维护困难
绑定参数类型转换重复,可能 SQL 注入
处理结果集字段映射重复,列名写错后字段为空
异常和事务回滚、提交、资源释放复杂
批量操作批大小、提交频率、失败定位难统一

ORM 的第一层价值是把这些重复动作封装起来。

mermaid
flowchart TD
    A["Java 方法参数"] --> B["ORM 参数绑定"]
    B --> C["PreparedStatement"]
    C --> D["数据库执行 SQL"]
    D --> E["ResultSet"]
    E --> F["ORM 结果映射"]
    F --> G["Java 对象"]

但注意:ORM 封装的是访问过程,不是消灭 SQL。数据库最终执行的仍然是 SQL。

阶段2:ORM 家族怎么分工

mermaid
flowchart TD
    A["JDBC"] --> B["MyBatis"]
    A --> C["JPA 规范"]
    C --> D["Hibernate 实现"]
    D --> E["Spring Data JPA"]
    B --> F["MyBatis-Plus"]
技术定位优点风险
JDBCJava 数据库访问基础最直接、最可控重复代码多
MyBatis半自动 ORMSQL 可控,适合复杂查询SQL 质量依赖开发者
MyBatis-PlusMyBatis 增强单表 CRUD 效率高复杂 SQL 不能硬套 Wrapper
JPA持久化规范统一实体映射标准规范本身不执行 SQL
HibernateJPA 常见实现持久化上下文、脏检查、懒加载N+1、懒加载、生成 SQL 要看懂
Spring Data JPARepository 封装快速 CRUD 和查询方法方法名查询过长、count 慢

选型不能按“哪个高级”决定,而要按业务决定:

场景推荐
订单、支付、库存等核心链路MyBatis,SQL 可控
医疗数据采集批量入库MyBatis 批量或 JDBC Batch
后台管理单表 CRUDMyBatis-Plus 或 Spring Data JPA
复杂报表统计MyBatis 手写 SQL
领域模型清晰的普通业务JPA/Hibernate
需要跨数据库兼容JPA/Hibernate 可以考虑

如果团队不看 SQL、不看执行计划,任何 ORM 都可能把问题藏到线上。

阶段3:MyBatis Mapper 为什么能执行

Mapper 接口没有实现类,却能调用:

java
public interface OrderMapper {
    OrderDO selectById(Long id);
}

原因是 MyBatis 启动时会解析 XML 或注解,运行时为接口创建代理对象。

mermaid
flowchart TD
    A["启动扫描 Mapper 接口"] --> B["解析 XML 或注解 SQL"]
    B --> C["创建 MappedStatement"]
    C --> D["注册到 Configuration"]
    D --> E["为 Mapper 接口创建代理"]
    E --> F["业务调用 Mapper 方法"]
    F --> G["MapperProxy 找到 MappedStatement"]
    G --> H["执行 SQL 并映射结果"]

一次查询全过程:

mermaid
flowchart TD
    A["OrderMapper.selectById"] --> B["MapperProxy.invoke"]
    B --> C["MapperMethod.execute"]
    C --> D["SqlSession.selectOne"]
    D --> E["Executor.query"]
    E --> F["StatementHandler 创建 PreparedStatement"]
    F --> G["ParameterHandler 绑定参数"]
    G --> H["数据库执行 SQL"]
    H --> I["ResultSetHandler 处理结果"]
    I --> J["ResultMap 映射 OrderDO"]

核心对象:

对象作用
ConfigurationMyBatis 全局配置和运行时注册中心
MappedStatement一条 Mapper SQL 的完整描述
SqlSourceSQL 来源,静态 SQL 或动态 SQL
BoundSql最终要执行的 SQL 和参数映射
ExecutorSQL 执行器,处理查询、更新、缓存
StatementHandler创建和操作 JDBC Statement
ParameterHandler绑定 Java 参数到 ?
ResultSetHandler把 ResultSet 转成对象
TypeHandlerJava 类型和 JDBC 类型转换

面试说法:Mapper 接口能执行不是魔法,而是动态代理加 MappedStatement

阶段4:MyBatis 最小 Demo

Mapper:

java
public interface AssetMapper {
    AssetDO selectByCode(@Param("assetCode") String assetCode);

    List<AssetDO> selectPage(@Param("tenantId") Long tenantId,
                             @Param("status") Integer status,
                             @Param("keyword") String keyword);
}

XML:

xml
<mapper namespace="com.example.asset.AssetMapper">
    <resultMap id="AssetMap" type="com.example.asset.AssetDO">
        <id column="id" property="id"/>
        <result column="asset_code" property="assetCode"/>
        <result column="asset_name" property="assetName"/>
        <result column="tenant_id" property="tenantId"/>
        <result column="status" property="status"/>
    </resultMap>

    <select id="selectByCode" resultMap="AssetMap">
        select id, asset_code, asset_name, tenant_id, status
        from data_asset
        where asset_code = #{assetCode}
          and deleted = 0
    </select>

    <select id="selectPage" resultMap="AssetMap">
        select id, asset_code, asset_name, tenant_id, status
        from data_asset
        <where>
            tenant_id = #{tenantId}
            and deleted = 0
            <if test="status != null">
                and status = #{status}
            </if>
            <if test="keyword != null and keyword != ''">
                and (asset_code like concat('%', #{keyword}, '%')
                     or asset_name like concat('%', #{keyword}, '%'))
            </if>
        </where>
        order by id desc
    </select>
</mapper>

你必须理解这些对应关系:

XML 元素对应含义
namespaceMapper 接口全限定名
select idMapper 方法名
resultMap列和 Java 字段的映射规则
#{assetCode}预编译参数绑定
<if>MyBatis 生成 SQL 前的条件判断

如果 namespace、方法名、参数名、列名、属性名任何一个不匹配,就会出现找不到语句、参数为空、字段映射为空等问题。

阶段5:#{}${} 的本质差异

mermaid
flowchart TD
    A["用户输入"] --> B{"MyBatis 使用方式"}
    B -- "#{ }" --> C["SQL 中生成 ?"]
    C --> D["PreparedStatement 参数绑定"]
    D --> E["输入只作为值"]
    B -- "${ }" --> F["直接字符串替换"]
    F --> G["输入可能改变 SQL 结构"]

#{} 示例:

xml
where username = #{username}

最终类似:

sql
where username = ?

数据库把用户输入当成值,不会改变 SQL 结构。

${} 示例:

xml
order by ${sortField}

如果前端传入:

text
id desc; delete from user

就有 SQL 注入风险。动态字段、排序、表名确实有时需要 ${},但必须白名单:

java
private static final Map<String, String> SORT_FIELD_MAP = Map.of(
        "createdAt", "created_at",
        "amount", "amount",
        "id", "id"
);

public String safeSortField(String input) {
    String field = SORT_FIELD_MAP.get(input);
    if (field == null) {
        throw new IllegalArgumentException("非法排序字段");
    }
    return field;
}

原则:用户输入不能直接改变 SQL 结构。

阶段6:动态 SQL 如何变成最终 SQL

MyBatis 动态 SQL 标签不是交给数据库执行,而是在 Java 侧生成最终 SQL。

mermaid
flowchart TD
    A["XML SQL 模板"] --> B["参数对象"]
    B --> C["OGNL 判断 if/foreach/where"]
    C --> D["生成最终 SQL 字符串"]
    D --> E["生成参数映射列表"]
    E --> F["BoundSql"]
    F --> G["PreparedStatement 执行"]

foreach 示例:

xml
<select id="selectByIds" resultMap="AssetMap">
    select id, asset_code, asset_name
    from data_asset
    where id in
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

如果 ids[1,2,3],最终 SQL 类似:

sql
select id, asset_code, asset_name
from data_asset
where id in (?, ?, ?)

常见坑:

后果正确做法
ids 为空in () 语法错误Java 层提前返回空结果
foreach 数量过大SQL 太长、参数太多分批或临时表
动态条件太多优化器难选索引设计常用查询组合
动态排序直接 ${}SQL 注入白名单映射

排查动态 SQL 时,不要只看 XML,要看最终 SQL 和参数,也就是 BoundSql

阶段7:结果映射和 TypeHandler

数据库返回的是 ResultSet,Java 需要对象。映射失败的高频原因是列名和属性名不一致。

sql
select asset_code from data_asset

Java 字段:

java
private String assetCode;

解决方式:

  1. 开启下划线转驼峰。
  2. SQL 使用别名:asset_code as assetCode
  3. 使用 resultMap 显式映射。

自定义 TypeHandler 适合处理枚举、JSON 字段、加密字段。

java
@MappedTypes(AssetStatus.class)
public class AssetStatusTypeHandler extends BaseTypeHandler<AssetStatus> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i,
                                    AssetStatus parameter, JdbcType jdbcType)
            throws SQLException {
        ps.setInt(i, parameter.getCode());
    }

    @Override
    public AssetStatus getNullableResult(ResultSet rs, String columnName)
            throws SQLException {
        return AssetStatus.of(rs.getInt(columnName));
    }
}

不要把复杂业务逻辑塞进 TypeHandler。它应该只做类型转换。

阶段8:MyBatis 缓存

MyBatis 一级缓存是 SqlSession 级别,默认开启。

mermaid
flowchart TD
    A["同一 SqlSession 查询"] --> B{"本地缓存是否有结果"}
    B -- "有" --> C["直接返回"]
    B -- "没有" --> D["执行 SQL"]
    D --> E["结果放入一级缓存"]

一级缓存命中条件大致与 MappedStatement、SQL、参数、分页等有关。执行 insert/update/deletecommitrollbackclose 通常会清空缓存。

二级缓存是 Mapper namespace 级别,风险更大:

mermaid
flowchart TD
    A["Mapper A 查询订单和用户"] --> B["写入 A namespace 缓存"]
    C["Mapper B 更新用户"] --> D["只清 B namespace 缓存"]
    D --> E["A namespace 可能仍是旧数据"]

为什么生产谨慎使用二级缓存:

风险原因
多表关联旧数据更新某个表不一定清理所有相关 namespace
多实例不一致本地缓存不天然跨 JVM 同步
外部系统修改数据库MyBatis 缓存不知道
强一致业务风险高订单、支付、库存不能读旧数据

建议:业务缓存用 Redis + 明确 key、TTL、失效策略;MyBatis 二级缓存不要当核心业务缓存。

阶段9:MyBatis 插件原理

MyBatis 插件能拦截四类核心对象:

拦截对象常见用途
ExecutorSQL 统计、审计、缓存控制
StatementHandler分页、SQL 改写、数据权限
ParameterHandler参数处理、加密字段
ResultSetHandler结果脱敏、字段转换

插件原理是动态代理:

mermaid
flowchart TD
    A["原始 Executor"] --> B["插件包装成代理对象"]
    B --> C["业务执行查询"]
    C --> D["代理先进入 Interceptor"]
    D --> E["执行增强逻辑"]
    E --> F["调用原始对象方法"]

分页插件本质:

text
原 SQL:
select id, name from data_asset where tenant_id = ?

改写后:
select id, name from data_asset where tenant_id = ? limit ?, ?

插件不能解决所有性能问题。分页插件只能改写 SQL,深分页慢、count 慢、索引不合理仍然需要数据库优化。

阶段10:MyBatis-Plus 的本质

MyBatis-Plus 不是另一个数据库访问框架,而是在 MyBatis 之上注入通用 SQL 和增强能力。

mermaid
flowchart TD
    A["BaseMapper.selectById"] --> B["MyBatis-Plus 注入通用 MappedStatement"]
    B --> C["根据实体表名、主键、字段生成 SQL"]
    C --> D["交给 MyBatis Executor"]
    D --> E["JDBC 执行"]

Wrapper 的本质是构造 SQL 条件片段:

java
LambdaQueryWrapper<AssetDO> wrapper = Wrappers.lambdaQuery();
wrapper.eq(AssetDO::getTenantId, tenantId)
       .eq(AssetDO::getDeleted, 0)
       .like(StringUtils.hasText(keyword), AssetDO::getAssetName, keyword)
       .orderByDesc(AssetDO::getId);

List<AssetDO> list = assetMapper.selectList(wrapper);

它最终仍会变成 SQL。Wrapper 只是让简单条件更容易写,不代表 SQL 自动合理。

高频坑:

后果正确做法
or() 不分组破坏租户、权限、逻辑删除条件and(w -> ...) 包裹
last() 拼用户输入SQL 注入只允许固定常量
逻辑删除没设计唯一索引删除后同名新增冲突唯一索引考虑删除标记
分页 count 很慢大表复杂 join 统计慢自定义 count 或 Slice/游标
Wrapper 写复杂报表可读性差,SQL 不可控回到 XML 手写 SQL

阶段11:JPA/Hibernate 核心原理

JPA/Hibernate 的核心是持久化上下文,不是“自动 SQL”四个字。

mermaid
flowchart TD
    A["EntityManager.find"] --> B["实体进入持久化上下文"]
    B --> C["保存实体快照"]
    C --> D["业务修改实体字段"]
    D --> E["flush 时脏检查"]
    E --> F{"快照是否变化"}
    F -- "变化" --> G["生成 update SQL"]
    F -- "未变化" --> H["不执行 update"]
    G --> I["事务提交或回滚"]

Demo:

java
@Transactional
public void changeAssetName(Long id, String newName) {
    Asset asset = entityManager.find(Asset.class, id);
    asset.changeName(newName);
}

为什么没有调用 save 也会更新:

  1. find 返回的实体是托管态。
  2. Hibernate 在持久化上下文中保存快照。
  3. 事务提交前触发 flush。
  4. flush 做脏检查。
  5. 字段变化后生成 update

实体状态:

状态含义
临时态new 出来,还没被持久化上下文管理
托管态被 EntityManager 管理,变化会被脏检查
游离态曾经托管,但现在离开上下文
删除态标记为删除,flush 时执行 delete

flushcommit 区别:

概念含义
flush把上下文变更同步成 SQL 发给数据库
commit提交事务,让数据库变更真正生效

flush 之后仍然可以回滚,所以不要把 flush 当成 commit。

阶段12:懒加载和 N+1

懒加载依赖代理对象和持久化上下文。

java
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;

如果事务结束后再访问:

java
order.getItems().size();

可能出现 LazyInitializationException,因为 EntityManager 已经关闭,代理无法再查数据库。

N+1 查询:

mermaid
flowchart TD
    A["查询 100 个订单"] --> B["执行 1 条订单 SQL"]
    B --> C["循环访问每个订单的明细"]
    C --> D["每个订单再查 1 条明细 SQL"]
    D --> E["总计 101 条 SQL"]

解决方向:

  1. 列表页优先 DTO 投影,只查需要字段。
  2. 明确需要关联时用 fetch join。
  3. 使用 EntityGraph。
  4. 批量查询关联数据后在内存组装。
  5. 不要在 Controller 返回 Entity,让 JSON 序列化触发懒加载。

DTO 投影示例:

java
public record AssetListItem(Long id, String assetCode, String assetName) {
}

@Query("""
       select new com.example.AssetListItem(a.id, a.assetCode, a.assetName)
       from Asset a
       where a.tenantId = :tenantId
       order by a.id desc
       """)
List<AssetListItem> findAssetList(Long tenantId);

阶段13:Spring Data JPA Repository 原理

Repository 接口也没有实现类:

java
public interface AssetRepository extends JpaRepository<Asset, Long> {
    List<Asset> findByTenantIdAndStatus(Long tenantId, Integer status);
}

它能执行是因为 Spring Data JPA 创建了代理对象。

mermaid
flowchart TD
    A["启动扫描 Repository"] --> B["识别实体类型和主键类型"]
    B --> C["创建 Repository 代理"]
    C --> D["解析方法调用"]
    D --> E{"方法类型"}
    E -- "内置 CRUD" --> F["SimpleJpaRepository"]
    E -- "方法名查询" --> G["解析方法名生成查询"]
    E -- "@Query" --> H["执行 JPQL 或 SQL"]
    F --> I["EntityManager"]
    G --> I
    H --> I

方法名查询适合简单条件,不适合无限拉长:

java
findByTenantIdAndStatusAndAssetNameContainingAndDeletedOrderByIdDesc

这种方法名可读性差,复杂条件应改用 @Query、Specification、Querydsl 或 MyBatis。

PageSlice

类型特点
Page查数据 + 查 count,能返回总页数
Slice通常不查 count,只判断是否有下一页

大表列表、日志流、采集记录滚动加载,不一定需要总数,Slice 或游标分页更合适。

阶段14:事务为什么放 Service 层

事务应该覆盖一个完整业务动作,而不是一条 Mapper/Repository 调用。

下单可能包含:

  1. 插入订单。
  2. 插入订单明细。
  3. 扣库存。
  4. 写本地消息表。
  5. 写审计日志。

这些要么一起提交,要么一起回滚。

java
@Service
public class OrderService {
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(CreateOrderCommand command) {
        orderMapper.insert(command.toOrder());
        orderItemMapper.batchInsert(command.toItems());
        stockMapper.decrease(command.skuId(), command.count());
        localMessageMapper.insert(command.toMessage());
    }
}

Spring 事务本质是 AOP 代理 + 线程绑定连接:

mermaid
flowchart TD
    A["调用 Service 代理方法"] --> B["事务拦截器开启事务"]
    B --> C["从连接池获取 Connection"]
    C --> D["绑定到当前线程"]
    D --> E["Mapper/JPA 使用同一连接"]
    E --> F{"方法是否异常"}
    F -- "正常" --> G["commit"]
    F -- "异常" --> H["rollback"]
    G --> I["释放连接"]
    H --> I

事务失效高频原因:

原因为什么
同类自调用没经过 Spring 代理
异常被 catch 吞掉事务拦截器认为成功
受检异常未配置 rollbackFor默认只回滚运行时异常
方法不是 public代理可能无法拦截
多数据源事务管理器选错操作不在同一事务管理器
事务中调用远程接口太久锁和连接占用时间过长

阶段15:批量导入怎么设计

不要十万条一个事务,也不要简单循环单条 insert。

错误示例:

java
@Transactional
public void importAll(List<AssetImportRow> rows) {
    for (AssetImportRow row : rows) {
        assetMapper.insert(row.toAsset());
    }
}

问题:

  1. SQL 次数太多。
  2. 单事务太大,锁和 undo 压力大。
  3. 一行失败,全量回滚,定位困难。
  4. 连接长时间占用。

商业设计:

mermaid
flowchart TD
    A["上传文件"] --> B["创建导入任务"]
    B --> C["解析和基础校验"]
    C --> D["按批次 500/1000 切分"]
    D --> E["每批独立事务写入"]
    E --> F{"批次是否失败"}
    F -- "成功" --> G["记录成功数"]
    F -- "失败" --> H["降级小批或单条定位"]
    H --> I["记录失败行和原因"]
    G --> J["更新任务进度"]
    I --> J

MyBatis 批量:

xml
<insert id="batchInsert">
    insert into data_asset(asset_code, asset_name, tenant_id, created_at)
    values
    <foreach collection="list" item="item" separator=",">
        (#{item.assetCode}, #{item.assetName}, #{item.tenantId}, now())
    </foreach>
</insert>

JPA 批量:

java
@Transactional
public void batchPersist(List<Asset> assets) {
    for (int i = 0; i < assets.size(); i++) {
        entityManager.persist(assets.get(i));
        if (i > 0 && i % 500 == 0) {
            entityManager.flush();
            entityManager.clear();
        }
    }
}

JPA 批量必须定期 flushclear,否则持久化上下文会堆积大量实体,导致内存上涨。

阶段16:商业场景怎么选 ORM

医疗数据采集入库

特点:字段多、批量大、失败要定位、需要幂等。

建议:

  1. MyBatis 批量写入。
  2. 按任务批次提交。
  3. 唯一索引或业务流水号保证幂等。
  4. 失败行记录行号和原因。
  5. 采集原始数据和标准化数据分表保存。

数据资产列表查询

特点:条件多、租户和医院数据权限、分页、排序。

建议:

  1. MyBatis 或 MyBatis-Plus。
  2. 权限条件必须进入 SQL。
  3. 排序字段白名单。
  4. 大表避免深分页。
  5. 列表页只查展示字段,详情页再查完整信息。

后台基础字典 CRUD

特点:单表为主、逻辑简单。

建议:

  1. MyBatis-Plus 或 Spring Data JPA。
  2. 逻辑删除、自动填充、乐观锁可以使用框架能力。
  3. 不要为了简单 CRUD 写大量重复 XML。

报表统计

特点:join、group by、聚合、时间范围、性能敏感。

建议:

  1. MyBatis 手写 SQL。
  2. 复杂统计考虑汇总表。
  3. 定时任务预聚合。
  4. 配合 EXPLAIN 和索引优化。

阶段17:生产排查

字段映射为空

mermaid
flowchart TD
    A["对象字段为空"] --> B["SQL 是否查出该列"]
    B --> C["列名是否和属性匹配"]
    C --> D["是否开启下划线转驼峰"]
    D --> E["resultMap column/property 是否写反"]
    E --> F["多表 join 是否有重复列名"]
    F --> G["TypeHandler 是否转换失败"]

事务没有回滚

mermaid
flowchart TD
    A["事务未回滚"] --> B["方法是否经过 Spring 代理"]
    B --> C["是否同类自调用"]
    C --> D["异常是否被 catch 吞掉"]
    D --> E["异常类型是否需要 rollbackFor"]
    E --> F["事务管理器是否正确"]
    F --> G["是否存在异步线程或新连接"]

SQL 慢

mermaid
flowchart TD
    A["SQL 慢"] --> B["打印最终 SQL 和参数"]
    B --> C["执行 EXPLAIN"]
    C --> D["看索引、扫描行数、排序、回表"]
    D --> E["看分页和 count"]
    E --> F["看锁等待和连接池"]
    F --> G["优化 SQL、索引或业务查询方式"]

JPA N+1

mermaid
flowchart TD
    A["接口 SQL 数量异常"] --> B["开启 SQL 日志"]
    B --> C["是否先查列表再循环查关联"]
    C --> D["是否 JSON 序列化触发懒加载"]
    D --> E["改 DTO 投影或 fetch join"]
    E --> F["回归验证 SQL 数量"]

批量导入内存上涨

mermaid
flowchart TD
    A["批量导入内存上涨"] --> B["批大小是否过大"]
    B --> C["JPA 是否未 clear 持久化上下文"]
    C --> D["是否一次性读完整文件"]
    D --> E["失败重试是否重复堆积"]
    E --> F["分片读取、分批事务、及时释放"]

阶段18:常见坑和后果

后果正确做法
只看 Mapper 不看 SQL线上慢查询难定位打印最终 SQL 和参数
用户输入直接 ${}SQL 注入白名单映射
二级缓存随便开读到旧数据明确一致性边界
Entity 直接返回前端懒加载、循环引用、字段泄露返回 DTO
JPA 批量不 clear内存上涨定期 flush/clear
事务放 Mapper 层多表业务无法整体回滚放 Service 层
catch 异常不抛事务不回滚抛出异常或手动标记回滚
远程调用放长事务里锁和连接占用事务内只做数据库关键操作
Wrapper 写复杂 SQL逻辑不可读,索引难优化复杂查询手写 SQL
分页插件解决深分页仍然慢游标分页、索引、业务改造

阶段19:面试标准回答

问:ORM 是什么?

标准回答:

ORM 是对象关系映射,负责把 Java 对象和关系型数据库之间的参数绑定、SQL 执行、结果映射、事务和缓存等重复工作封装起来。它能提高开发效率,但不等于不用理解 SQL。最终性能仍然取决于表结构、索引、执行计划和事务边界。

问:MyBatis Mapper 接口为什么能执行?

标准回答:

MyBatis 启动时会扫描 Mapper 接口,解析 XML 或注解形成 MappedStatement 并注册到 Configuration。运行时为 Mapper 接口创建动态代理。调用接口方法时,代理根据 namespace 和方法名找到 MappedStatement,动态 SQL 生成 BoundSql,再通过 ExecutorStatementHandlerParameterHandlerResultSetHandler 完成 SQL 执行和结果映射。

问:MyBatis 和 Hibernate 有什么区别?

标准回答:

MyBatis 是半自动 ORM,SQL 主要由开发者编写和控制,适合复杂 SQL、报表和性能敏感场景;Hibernate/JPA 更强调实体映射、持久化上下文、脏检查和自动 SQL,CRUD 效率高,但必须关注 N+1、懒加载、flush、最终生成 SQL 和事务边界。

问:JPA 为什么修改实体不用手写 update?

标准回答:

事务内查询出来的实体是托管态,Hibernate 会在持久化上下文里保存实体快照。业务修改字段后,事务提交前触发 flush,Hibernate 做脏检查,发现当前实体和快照不同,就生成 update SQL。flush 只是同步 SQL,commit 才是真正提交事务。

问:事务为什么放 Service 层?

标准回答:

事务应该覆盖一个完整业务动作,而不是单条 SQL。Service 层通常会调用多个 Mapper 或 Repository,例如订单、明细、库存、本地消息表要一起成功或一起回滚。事务放 Mapper 层只能保证单次数据访问,无法保证业务一致性。

最终验收题

如果下面问题答不清楚,说明还没真正掌握 ORM:

  1. JDBC 访问数据库有哪些重复动作,ORM 分别封装了什么?
  2. MyBatis 的 MappedStatementBoundSql 有什么区别?
  3. #{} 为什么能防 SQL 注入,${} 为什么危险?
  4. 动态 SQL 最终怎么变成数据库执行的 SQL?
  5. MyBatis 一级缓存什么时候命中,什么时候清空?
  6. 二级缓存为什么在多表和分布式场景里容易不一致?
  7. MyBatis 插件拦截哪些对象?分页插件为什么不能解决深分页慢?
  8. MyBatis-Plus Wrapper 的 orlast 有什么风险?
  9. JPA 持久化上下文、实体状态、脏检查、flush、commit 怎么串起来?
  10. N+1 查询怎么产生,怎么发现,怎么解决?
  11. Spring Data JPA Repository 接口为什么能执行?
  12. Page 和 Slice 为什么在大表分页里选择不同?
  13. 事务失效有哪些原因?怎么排查?
  14. 批量导入如何控制批大小、事务、失败定位和幂等?
  15. ORM 线上慢 SQL 应该按什么证据链排查?

关联知识点跳转

本章小结

ORM 的核心不是“少写 SQL”,而是把数据库访问链路工程化:参数绑定、SQL 执行、结果映射、缓存、事务、批量和扩展点。真正掌握 ORM,必须能解释框架如何把方法调用变成 SQL,能看懂最终 SQL 和执行计划,能识别缓存和事务边界,能在商业项目里选择合适框架,并能在生产故障中拿证据排查。