Skip to content

ORM 从零到生产级掌握

ORM 不能只理解成“少写 SQL”。商业项目里真正要掌握的是:JDBC 为什么繁琐,ORM 帮你封装了什么,MyBatis 为什么 Mapper 接口能执行,动态 SQL 怎么变成最终 SQL,JPA 为什么修改实体不用手写 update,一级缓存和二级缓存为什么有一致性风险,事务为什么放 Service 层,批量写入和分页为什么会慢,线上 SQL 问题怎么定位。

一句话建立主线:

ORM 是把对象模型和关系型数据库之间的参数绑定、SQL 执行、结果映射、事务边界、缓存和扩展点封装起来;但最终性能和一致性仍然取决于 SQL、索引、事务和业务边界设计。

如果你想检查自己是否真的从零基础学懂到能面试、能落地、能排查,按 ORM 从零到精通验收清单 逐项验收。

学习目标

学完这一页,你要能做到:

  1. 解释 JDBC、ORM、MyBatis、Hibernate、JPA、Spring Data JPA、MyBatis-Plus 的关系。
  2. 解释一次 Mapper 方法调用到数据库返回对象的全过程。
  3. 解释 MyBatis 中 MappedStatementBoundSqlExecutorTypeHandlerResultMap 的作用。
  4. 解释 #{}${} 为什么安全性不同。
  5. 解释 MyBatis 一级缓存、二级缓存为什么可能读到旧数据。
  6. 解释 JPA 持久化上下文、托管态、脏检查、flush、commit。
  7. 解释 N+1 查询、懒加载异常、分页 count 慢、批量导入内存暴涨。
  8. 解释事务为什么放 Service 层,为什么同类自调用会让 Spring 事务失效。
  9. 能写 MyBatis XML、动态 SQL、批量写入、JPA Repository、事务 Demo。
  10. 能在订单、采集、资产查询、报表统计等商业场景中选择合适 ORM。
  11. 能排查慢 SQL、字段映射为空、事务未回滚、缓存不一致、分页变慢。

为什么 JDBC 还不够

JDBC 是 Java 访问关系型数据库的基础,但直接使用会很繁琐:

java
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(
    "select id, username, status from users where id = ?"
);
ps.setLong(1, id);
ResultSet rs = ps.executeQuery();
UserDO user = null;
if (rs.next()) {
    user = new UserDO();
    user.setId(rs.getLong("id"));
    user.setUsername(rs.getString("username"));
    user.setStatus(rs.getInt("status"));
}
rs.close();
ps.close();
conn.close();

这里有很多重复工作:

重复点风险
获取和关闭连接忘记关闭会连接泄漏
拼 SQL 和绑定参数拼接错误或 SQL 注入
遍历 ResultSet字段映射重复且容易写错
异常处理事务回滚和资源释放复杂
批量执行批大小、提交频率难统一

ORM 的价值就是把这些重复劳动变成框架能力,但不是替你消灭数据库知识。

ORM 家族怎么分工

mermaid
flowchart TD
    A["JDBC"] --> B["MyBatis"]
    A --> C["JPA 规范"]
    C --> D["Hibernate 实现"]
    D --> E["Spring Data JPA"]
    B --> F["MyBatis-Plus"]
技术定位适合场景风险
MyBatis半自动 ORM,SQL 可控复杂 SQL、报表、批量、性能敏感SQL 维护成本在开发者
MyBatis-PlusMyBatis 增强单表 CRUD、Wrapper 条件、代码生成复杂 SQL 仍要回到 MyBatis
JPA持久化规范规范化实体映射只是规范,不是具体实现
HibernateJPA 常见实现领域模型、CRUD、多数据库兼容最终 SQL 不可忽略
Spring Data JPARepository 封装快速 CRUD、方法名查询、Specification方法名过长、N+1、懒加载

选型原则:

  1. SQL 复杂、性能强控制:优先 MyBatis 或 MyBatis-Plus + 手写 SQL。
  2. 领域模型清晰、CRUD 多:可以 JPA/Hibernate。
  3. 团队 SQL 能力强:MyBatis 更透明。
  4. 团队不熟悉 Hibernate 原理:不要把核心交易系统直接交给全自动 ORM。

MyBatis 一次执行全过程

mermaid
flowchart TD
    A["业务调用 Mapper 接口"] --> B["MapperProxy 代理"]
    B --> C["根据接口方法找到 MappedStatement"]
    C --> D["动态 SQL 解析成 BoundSql"]
    D --> E["Executor 选择执行策略"]
    E --> F["StatementHandler 创建 PreparedStatement"]
    F --> G["ParameterHandler 绑定参数"]
    G --> H["数据库执行 SQL"]
    H --> I["ResultSetHandler 处理结果集"]
    I --> J["ResultMap 映射成 Java 对象"]

核心对象:

对象作用
MapperProxyMapper 接口的动态代理
MappedStatement一条 SQL 的完整描述
BoundSql动态 SQL 解析后的最终 SQL 和参数映射
Executor执行器,负责查询、更新、缓存
StatementHandler创建和执行 Statement
ParameterHandler把 Java 参数绑定到 SQL 占位符
ResultSetHandler把 ResultSet 映射为对象
TypeHandlerJava 类型和 JDBC 类型转换

这就是为什么 Mapper 接口没有实现类也能运行:运行时调用的是代理对象,代理再去找 XML 或注解中的 SQL。

MyBatis 最小 Demo

Mapper:

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

    List<OrderDO> selectByStatus(@Param("status") Integer status);
}

XML:

xml
<mapper namespace="com.example.mapper.OrderMapper">
    <resultMap id="OrderMap" type="com.example.domain.OrderDO">
        <id column="id" property="id"/>
        <result column="order_no" property="orderNo"/>
        <result column="status" property="status"/>
        <result column="amount" property="amount"/>
    </resultMap>

    <select id="selectById" resultMap="OrderMap">
        select id, order_no, status, amount
        from t_order
        where id = #{id}
    </select>

    <select id="selectByStatus" resultMap="OrderMap">
        select id, order_no, status, amount
        from t_order
        where status = #{status}
        order by id desc
    </select>
</mapper>

重点:

  1. namespace 要对应 Mapper 接口全限定名。
  2. select id 要对应接口方法名。
  3. 数据库列名和 Java 字段不一致时,用 resultMap 或别名。
  4. #{} 走预编译参数,不能被用户输入改变 SQL 结构。

#{}${} 的底层差异

mermaid
flowchart TD
    A["用户参数"] --> B{"使用方式"}
    B -- "#{ }" --> C["生成 ? 占位符"]
    C --> D["PreparedStatement 绑定参数"]
    D --> E["数据当作值"]
    B -- "${ }" --> F["直接拼进 SQL 字符串"]
    F --> G["数据可能改变 SQL 结构"]

示例:

xml
where username = #{username}

最终类似:

sql
where username = ?

而:

xml
order by ${sortField}

会直接拼接字段名。它不是绝对不能用,但必须做白名单:

java
private static final Set<String> ALLOW_SORT_FIELDS =
    new HashSet<String>(Arrays.asList("created_at", "amount", "id"));

public String checkSortField(String field) {
    if (!ALLOW_SORT_FIELDS.contains(field)) {
        throw new IllegalArgumentException("非法排序字段");
    }
    return field;
}

动态 SQL 怎么工作

动态 SQL 不是数据库执行 XML 标签,而是 MyBatis 在发送 SQL 前根据参数生成最终 SQL。

xml
<select id="selectPage" resultMap="OrderMap">
    select id, order_no, status, amount
    from t_order
    <where>
        <if test="status != null">
            and status = #{status}
        </if>
        <if test="orderNo != null and orderNo != ''">
            and order_no = #{orderNo}
        </if>
    </where>
    order by id desc
</select>

status=1orderNo=null 时,最终 SQL 只保留 status 条件。这样做的价值是既能灵活拼条件,又能保持参数预编译。

常见坑:

后果
<if> 判断漏空字符串条件异常或查不到数据
<foreach> 集合为空SQL 语法错误
动态表名直接 ${}SQL 注入
条件太多没有索引看似灵活,实际全表扫描

MyBatis 缓存为什么要谨慎

MyBatis 一级缓存是 SqlSession 级别,默认开启;二级缓存是 namespace 级别,需要配置。

mermaid
flowchart TD
    A["查询请求"] --> B{"一级缓存是否命中"}
    B -- "命中" --> C["返回缓存对象"]
    B -- "未命中" --> D["执行 SQL"]
    D --> E["写入一级缓存"]
    E --> F["返回结果"]

一级缓存的风险通常来自长 SqlSession 或同一事务中数据变化后的理解偏差。二级缓存风险更大:

  1. 多表关联查询时,更新 A 表不一定清理 B namespace 缓存。
  2. 分布式多实例本地缓存不共享。
  3. 业务绕过 MyBatis 更新数据库,缓存不知道。

生产建议:

  1. 一级缓存默认理解即可,不要长时间持有 SqlSession。
  2. 二级缓存谨慎开启,核心交易和复杂关联查询一般不建议依赖它。
  3. 缓存一致性优先用 Redis + 明确失效策略,而不是把 MyBatis 二级缓存当业务缓存。

JPA/Hibernate 怎么工作

JPA/Hibernate 的核心不是“自动生成 SQL”,而是持久化上下文。

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

Demo:

java
@Transactional
public void changeOrderAmount(Long id, BigDecimal amount) {
    Order order = entityManager.find(Order.class, id);
    order.setAmount(amount);
}

为什么没有 update 也会更新:

  1. find 出来的实体是托管态。
  2. Hibernate 在持久化上下文保存了实体快照。
  3. 事务提交前 flush。
  4. flush 时做脏检查。
  5. 发现字段变化,生成 SQL。

如果实体已经离开事务和 Session,再改字段就不会自动同步,这就是托管态和游离态的差异。

flush 和 commit

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

flush 后 SQL 已经执行,但事务还没提交,仍然可以回滚。不要把 flush 理解成提交。

N+1 查询

N+1 是 ORM 里最常见的性能坑。

mermaid
flowchart TD
    A["查询 100 个订单"] --> B["1 条 SQL"]
    B --> C["循环访问 order.items"]
    C --> D["每个订单再查一次明细"]
    D --> E["总共 1 + 100 条 SQL"]

解决方式:

  1. 需要什么字段就写 DTO 查询。
  2. 用 fetch join 一次加载必要关联。
  3. 使用批量抓取配置。
  4. 不要在序列化 JSON 时隐式触发懒加载。
  5. 接口层不要直接返回 Entity。

事务为什么放 Service 层

Mapper/Repository 是数据访问动作,Service 是完整业务动作。一个下单动作可能包含:

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

这些要么一起成功,要么一起失败,所以事务应该包在 Service 层。

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.getSkuId(), command.getCount());
        localMessageMapper.insert(buildMessage(command));
    }
}

Spring 事务失效高频原因:

原因为什么失效
同类自调用没经过 Spring 代理
异常被 catch 不再抛出事务拦截器不知道失败
默认不回滚受检异常需要 rollbackFor
方法不是 public代理可能无法拦截
多数据源事务管理器选错连接不在同一个事务里
手动 JDBC 连接绕过 Spring 管理连接

批量写入为什么容易出问题

批量导入十万条数据,不能简单循环单条 insert:

java
for (OrderDO order : orders) {
    orderMapper.insert(order);
}

问题:

  1. SQL 次数太多,网络往返高。
  2. 一个超大事务占用锁和 undo。
  3. 失败回滚成本高。
  4. JPA 持久化上下文可能堆积大量对象。

MyBatis 批量思路:

java
for (List<OrderDO> batch : split(orders, 500)) {
    orderMapper.batchInsert(batch);
}

JPA 批量思路:

java
for (int i = 0; i < orders.size(); i++) {
    entityManager.persist(orders.get(i));
    if (i % 500 == 0) {
        entityManager.flush();
        entityManager.clear();
    }
}

商业场景选型

场景推荐理由
订单核心链路MyBatisSQL 可控,便于事务和性能排查
采集数据批量入库MyBatis 批量控制批大小、字段映射和写入性能
后台单表 CRUDMyBatis-Plus 或 Spring Data JPA开发效率高
复杂报表统计MyBatis 手写 SQL可控 join、聚合、索引
领域模型 CRUDJPA/Hibernate实体状态和关系映射方便
大表分页查询MyBatis + 游标/延迟关联避免深分页和 count 慢

线上排查流程

mermaid
flowchart TD
    A["ORM 线上问题"] --> B{"表现"}
    B -- "SQL 慢" --> C["查慢 SQL 和执行计划"]
    B -- "字段为空" --> D["查列名、别名、resultMap、驼峰"]
    B -- "事务没回滚" --> E["查代理、异常、rollbackFor、事务管理器"]
    B -- "数据不一致" --> F["查缓存、事务边界、消息发送"]
    B -- "内存上涨" --> G["查批量大小、JPA 持久化上下文"]
    C --> H["索引、扫描行数、排序、回表、锁等待"]

排查证据:

证据看什么
SQL 日志最终 SQL 和参数
EXPLAIN索引、扫描行数、排序、回表
事务日志方法是否经过代理,异常是否抛出
数据库连接池是否连接泄漏或等待
GC 日志批量导入是否对象堆积
业务日志同一个 traceId 下执行了哪些 SQL

面试标准回答

ORM 是什么

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

MyBatis 和 Hibernate 区别

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

Mapper 接口为什么能执行

text
MyBatis 启动时会解析 XML 或注解形成 MappedStatement,运行时为 Mapper 接口创建代理。调用接口方法时,代理根据 namespace 和方法名找到对应 MappedStatement,生成 BoundSql,通过 Executor、StatementHandler、ParameterHandler 执行 SQL,再由 ResultSetHandler 映射结果。

事务为什么放 Service 层

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

关联知识点

知识点继续学习
从零到精通验收ORM 从零到精通验收清单
商业场景训练营ORM 商业场景训练营
MyBatis 总览MyBatis
MyBatis 核心原理MyBatis核心全过程原理
动态 SQLMyBatis动态SQL
缓存机制MyBatis缓存机制
批量操作MyBatis批量操作
MyBatis 插件MyBatis拦截器
MyBatis-PlusMyBatis-Plus核心全过程原理
Hibernate/JPAHibernate/JPA核心全过程原理
Spring Data JPASpring Data JPA核心全过程原理
ORM 事务事务与一致性
ORM 面试ORM面试题

本章小结

ORM 学到生产级,关键是把 JDBC、SQL、参数绑定、结果映射、Mapper 代理、动态 SQL、缓存、持久化上下文、脏检查、事务边界、批量写入和慢 SQL 排查串起来。会用框架只是第一步,能解释最终 SQL 为什么这样执行、为什么慢、为什么事务没生效,才算真正掌握。