你学了整套 ORM 体系,换来了什么?
ORM 的宣传口径是:你只需要操作 Java 对象,框架自动帮你生成 SQL、管理关系、处理缓存。你不需要写 SQL,不需要关心数据库细节。
这套说辞听起来很美,但代价是什么?
下面这张表,列出了你为了“不用写 SQL”而需要学习的全部内容。
一、为了“把对象映射成表”,你学到的收益
| ORM 概念 | 它的作用 |
|---|---|
@Entity | 告诉框架这个类是实体 |
@Table | 指定对应的数据库表名 |
@Column | 指定字段映射(列名、长度、是否可空) |
@Id | 标记主键字段 |
@GeneratedValue | 指定主键生成策略(AUTO、IDENTITY、SEQUENCE、TABLE) |
@Transient | 标记非持久化字段 |
@Enumerated | 枚举类型映射(ORDINAL / STRING) |
@Temporal | 日期类型映射(DATE / TIME / TIMESTAMP) |
@Lob | 大字段映射(BLOB / CLOB) |
@Version | 乐观锁版本号字段 |
@Embeddable/@Embedded | 嵌入对象映射 |
@EmbeddedId | 复合主键 |
@MappedSuperclass | 父类字段继承 |
二、为了“关联其他表”,你学到的收益
| ORM 概念 | 它的作用 |
|---|---|
@OneToOne | 一对一关联 |
@OneToMany | 一对多关联 |
@ManyToOne | 多对一关联 |
@ManyToMany | 多对多关联 |
@JoinColumn | 指定外键列名 |
@JoinTable | 多对多中间表配置 |
@OrderBy | 关联集合的排序方式 |
mappedBy | 双向关联的对方字段 |
fetch = LAZY / EAGER | 关联加载时机 |
cascade = ALL / PERSIST / MERGE / REMOVE / REFRESH / DETACH | 级联操作类型 |
orphanRemoval = true / false | 孤儿对象删除策略 |
三、为了“操作数据”,你学到的收益
| ORM 概念 | 它的作用 |
|---|---|
EntityManager | JPA 核心操作接口 |
EntityManagerFactory | EntityManager 工厂 |
EntityTransaction | 事务管理 |
persist() | 保存实体 |
merge() | 合并实体(更新) |
remove() | 删除实体 |
find() | 按主键查询 |
getReference() | 懒加载引用查询 |
flush() | 立即同步到数据库 |
detach() | 从持久上下文分离 |
clear() | 清空持久上下文 |
contains() | 判断是否托管 |
四、为了“查询数据”,你学到的收益
HQL / JPQL(一门全新的查询语言)
| 概念 | 它的作用 |
|---|---|
SELECT ... FROM | 查询实体(注意:FROM 后面是实体名,不是表名) |
JOIN FETCH | 关联预加载(为了解决 N+1) |
WHERE+ 对象属性路径 | 条件查询(用u.name而不是u.name的 SQL 字段名) |
GROUP BY/HAVING | 分组聚合 |
ORDER BY | 排序 |
NEW构造器表达式 | 直接构造 VO 对象 |
| 子查询 | 支持但不完整 |
| 聚合函数 | 支持 COUNT、SUM、AVG、MAX、MIN,但有限制 |
| 窗口函数 | 不支持 |
| 递归 CTE | 不支持 |
| UNION | 不支持 |
数据库特有函数(DATE_FORMAT、EXTRACT、JSON_EXTRACT等) | 不支持,或需注册方言 |
Criteria API(一套更“类型安全”的查询构造器)
| 概念 | 它的作用 |
|---|---|
CriteriaBuilder | 查询构建器入口 |
CriteriaQuery<T> | 定义查询返回类型 |
Root<T> | 查询根节点 |
Join<Z, X> | 关联查询 |
Predicate | 条件组合(AND / OR / NOT) |
Path | 属性路径访问 |
Order | 排序条件 |
Expression | 表达式(算术运算、函数调用) |
Subquery | 子查询构造 |
Metamodel | 静态元模型(需要代码生成器) |
你学完这套 Criteria API,写出来的代码长这样:
CriteriaBuildercb=entityManager.getCriteriaBuilder();CriteriaQuery<User>query=cb.createQuery(User.class);Root<User>user=query.from(User.class);Join<User,Order>order=user.join("orders");Predicatepredicate=cb.and(cb.equal(user.get("name"),"张三"),cb.greaterThan(order.get("amount"),100));query.select(user).where(predicate).orderBy(cb.desc(user.get("createTime")));List<User>result=entityManager.createQuery(query).getResultList();五、为了“理解框架行为”,你学到的收益
| ORM 概念 | 它的作用 |
|---|---|
| 一级缓存(Persistence Context) | 同一个 EntityManager 内的对象缓存,自动管理 |
| 二级缓存 | 跨 EntityManager 的缓存,需额外配置 |
| 查询缓存 | 查询结果缓存,需配合二级缓存使用 |
| 脏检查机制 | 框架自动检测托管对象的变化并生成 UPDATE |
| 延迟加载 | 访问关联对象时才触发查询,否则是代理对象 |
| 关联加载策略 | LAZY vs EAGER,选错了就是 N+1 |
| 会话管理 | 什么时候打开 EntityManager,什么时候关闭 |
| 托管状态 / 游离状态 / 分离状态 / 删除状态 | 四种状态的区分,以及状态转换规则 |
| 乐观锁实现机制 | @Version注解 + 版本号检查 |
| 事务传播行为 | 需要配合 Spring 事务管理(@Transactional的各种属性) |
| 方言(Dialect) | 不同数据库的 SQL 方言差异,需要选择正确的方言类 |
六、为了“处理复杂场景”,你学到的收益
| ORM 概念 | 它的作用 |
|---|---|
@NamedQuery | 预定义 JPQL 查询,启动时解析 |
@NamedNativeQuery | 预定义原生 SQL 查询 |
@SqlResultSetMapping | 原生 SQL 结果映射到实体 |
@ConstructorResult | 原生 SQL 结果映射到 VO |
@EntityGraph | 动态控制关联加载图 |
@NamedEntityGraph | 预定义关联加载图 |
@Query(Spring Data JPA) | 在 Repository 接口上写 JPQL 或原生 SQL |
@Modifying | 标记 UPDATE / DELETE 操作 |
@Transactional | 事务边界控制(其实跟 JPA 无关,是 Spring 的能力) |
PersistenceUnitUtil | 工具类,判断实体状态 |
EntityManager.createNativeQuery() | 你终于退回了原生 SQL |
这些“收益”的最终结果是什么?
你学完了:
- 20+ 个注解
- 10+ 个关联关系配置
- 一套完整的查询语言(JPQL/HQL)
- 一套复杂的 Criteria API
- N+1 问题的识别与解决方案
- 四种实体状态及转换规则
- 一级缓存 / 二级缓存 / 查询缓存的区别与用法
- 方言配置
你以为换来了“不用写 SQL”。
但事实是:
- 复杂联表查询你写不出 JPQL → 退回到
createNativeQuery() - 窗口函数、递归 CTE、UNION → JPA 不支持 → 退回到
createNativeQuery() - 报表统计 > 3 张表 JOIN → JPQL 写起来比 SQL 还难懂 → 退回到
createNativeQuery() - 需要调优 SQL 时,框架生成的那坨 SQL 你自己都看不懂 → 退回到
createNativeQuery()
你学了一大套 ORM 体系,最终在复杂场景下,它只是你退回原生 SQL 路上的一个减速带。
破:MyBatis 至少还让你写 SQL,JPA 连让你写 SQL 都要绕一圈
MyBatis 的缺点是 XML 太啰嗦,但它至少让你直接写 SQL。你的 SQL 能力是完整的。
JPA 的问题是:你学了一套框架,但它主动阉割了你的 SQL 能力。你本来能写 SQL,但框架告诉你“用 JPQL”,你用了之后发现 JPQL 只有 SQL 1/3 的能力。剩下 2/3 的能力,你只能退回原生 SQL——但这时你发现,你学的那套 JPQL 语法在复杂场景下毫无用处。
你学了一套专有语法,结果它在你最需要的时候用不上。那学它的“收益”在哪?
立:SimpleDAO
SimpleDAO 不做对象关系映射,不管理对象状态,不制造缓存,不生成 SQL。
- 你要查数据?写 SQL,调用
list() - 你要联表?写 SQL,调用
page() - 你要复杂报表?写 SQL,调用
list() - 你要调优?复制 SQL 到数据库直接跑
你不需要学 JPQL、Criteria API、EntityManager、缓存策略、懒加载配置、四种状态转换、N+1 解决方案、方言配置、EntityGraph、二级缓存配置……
你只需要会 SQL,就会用 SimpleDAO。
因为它的核心能力只有一条:执行你写的 SQL,把结果映射成 Java 对象。
就这么简单。没有任何隐藏的“收益”,也没有任何隐藏的成本。
开源地址
- 核心框架:https://gitee.com/gao_zhenzhong/simple-dao
- 系统底座:https://gitee.com/gao_zhenzhong/simple-dao-starter
- 代码生成器:https://gitee.com/gao_zhenzhong/simple-dao-coder
- 实战案例:https://gitee.com/gao_zhenzhong/simple-dao-demo
写在最后
MyBatis 让你学了几十个标签、上百个属性、十几个注解、31 个异常,最终换来的是“在 XML 里拼一个字符串”。
JPA 让你学了 20+ 个注解、10+ 个关联关系、一套 JPQL 语法、一套 Criteria API、四种状态转换、三种缓存策略、一套方言体系……最终换来的是“写了复杂查询时,还得退回到原生 SQL”。
但是,我们也要承认一个事实:单表对象化确实是一个真实的收益。
在单表 CRUD 的场景下,JPA 和 Hibernate 确实能帮你省掉写 INSERT、UPDATE、DELETE 和按主键 SELECT 的 SQL。你不用写 INSERT INTO user (name, age) VALUES (?, ?),直接 user.setName(“张三”) 然后 entityManager.persist(user) 就行。
这个收益是真实的,不是带引号的。
问题是:单表 CRUD 只是企业持久层开发的一小部分——远不到 10%。
剩下的 90%(多表联查、子查询、聚合报表、批量更新、跨表条件筛选……),才是你真正要面对的业务场景。而这些场景下,JPA 帮不了你,你只能退回到原生 SQL。
所以,单表对象化是一个真实的收益,但它覆盖的只是业务中很小的一部分。用一个覆盖 10% 场景的收益,换取 90% 场景下的复杂度和限制,这笔账怎么算,你自己权衡。
两个方向,殊途同归。
SimpleDAO 让你什么都不用多学,你只需要会 SQL,就会用它。
它把“你不用学什么”当作核心收益。
哪一个收益是真的,你自己算账。