1. JPA 和 Hibernate 有什么区别?
JPA 是 Java 官方提出的一套 ORM 规范,它只定义了实体映射、关系管理、查询接口等标准,不包含具体实现。Hibernate 是对 JPA 规范的最常用实现,提供了完整的 ORM 功能,并扩展了许多 JPA 没有的高级特性。而 Spring Data JPA 是 Spring 提供的对 JPA 的封装,它简化了 Repository 层的开发,支持方法名自动派生查询。在 Spring Boot 项目中,Spring Data JPA 默认使用 Hibernate 作为底层 JPA 实现。
2. JPA 常见注解作用?
JPA 通过 @Entity
将类标记为持久化实体,使用 @Id
和 @GeneratedValue
标识主键,使用 @Column
映射字段,并通过 @OneToMany
、@ManyToOne
等注解描述实体间关系。还可以通过 @Temporal
、@Transient
控制字段行为。实际项目中常结合 Hibernate 提供的扩展注解实现时间自动填充等高级功能。
注解 | 作用 | 示例 |
| 声明这是一个实体类,对应数据库表 |
|
| 映射表名(可选) |
|
| 标记主键字段 |
|
| 主键生成策略 |
|
策略 | 说明 |
| 自增(MySQL 常用) |
| 数据库序列(Oracle 常用) |
| 使用一张表生成主键 |
| 自动选择适合的策略(默认) |
注解 | 作用 | 示例 |
| 映射字段名(可选) |
|
| 忽略此字段,不参与映射 | 不入库的字段 |
| 大字段(如 text、blob) | 文本/二进制 |
| 映射日期时间类型(Date) |
|
值 | 映射类型(Java Date) |
| 只保存日期(yyyy-MM-dd) |
| 只保存时间(HH:mm:ss) |
| 保存日期+时间 |
注解 | 关系类型 | 对应关系 |
| 一对一 | 用户 - 身份证 |
| 一对多 | 用户 - 订单 |
| 多对一 | 订单 - 用户 |
| 多对多 | 用户 - 角色 |
| 指定外键列名 | 单向关系中用 |
| 中间表配置 | 多对多时使用 |
| 指定关系被维护方 | 用于双向关系 |
| 联级操作 | PERSIST、REMOVE 等 |
| 加载策略 | LAZY / EAGER |
类型 | 含义说明 |
| 保存时级联保存( ) |
| 合并更新时级联更新( ) |
| 删除时级联删除( ) |
| 刷新实体状态时也刷新关联对象 |
| 分离实体时也分离关联对象 |
| 包含以上所有操作(常用) |
注解 | 作用 |
| 插入前执行 |
| 插入后执行 |
| 更新前执行 |
| 更新后执行 |
| 删除前执行 |
| 删除后执行 |
| 加载后执行 |
3. Spring Data JPA 如何实现自定义 SQL?
Spring Data JPA 实现自定义 SQL 的常用方法是通过 @Query 注解编写 JPQL 或原生 SQL,并配合 @Modifying 实现更新删除操作。对于更复杂或动态查询,可以通过自定义 Repository 实现类,利用 EntityManager 执行原生或 JPQL 查询,或者使用 JPA Criteria API 结合 Specification 来动态构造查询条件。
1. 使用 @Query
注解写 JPQL 或原生 SQL
public interface UserRepository extends JpaRepository<User, Long> {// JPQL 查询(面向实体类和属性名)@Query("select u from User u where u.name = ?1")List<User> findByNameJPQL(String name);// 原生 SQL 查询,nativeQuery = true@Query(value = "select * from user where name = ?1", nativeQuery = true)List<User> findByNameNative(String name);
}
2. 使用命名参数(推荐,代码更易读)
@Query("select u from User u where u.name = :name and u.age > :age")
List<User> findByNameAndAge(@Param("name") String name, @Param("age") Integer age);
3. 定义更新或删除操作(需要加 @Modifying
)
@Modifying
@Query("update User u set u.status = :status where u.id = :id")
int updateStatus(@Param("status") Integer status, @Param("id") Long id);
注意:执行更新或删除时,方法上必须加 @Modifying
注解,并且调用前通常要加事务(@Transactional
)。
4.设计一个 SqlServiceImpl
作为 SQL 服务组件,内部封装基于 EntityManager
的通用查询和更新方法。
@Service
public class SqlServiceImpl implements SqlService {@PersistenceContextprivate EntityManager em;@Overridepublic <T> List<T> queryList(String sql, Map<String, Object> params, Class<T> resultClass) {Query query = em.createNativeQuery(sql, resultClass);setParams(query, params);return query.getResultList();}@Overridepublic int executeUpdate(String sql, Map<String, Object> params) {Query query = em.createNativeQuery(sql);setParams(query, params);return query.executeUpdate();}private void setParams(Query query, Map<String, Object> params) {if (params != null) {params.forEach(query::setParameter);}}
}
4. JPA 的缓存机制?MyBatis 的缓存区别在哪
JPA 默认支持一级缓存,绑定在 EntityManager 生命周期内,避免重复查询;可选开启二级缓存,在多个 EntityManager 之间共享实体,提高性能。常用的二级缓存实现包括 EhCache、Redis 等。对于跨事务的高频读取场景,通过二级缓存可以显著减少数据库压力。但需注意缓存一致性和适配分布式场景的问题。
JPA 的二级缓存和 MyBatis 的二级缓存在原理上类似,都是跨 Session 的共享缓存机制,目的是避免重复查询、提升性能。它们分别绑定在 EntityManager 和 SqlSession 上,区别在于 JPA 是基于实体对象的缓存,而 MyBatis 更偏向 SQL 结果级别。二者都要求结合缓存一致性策略合理使用,否则可能造成数据不一致。
JPA 的实体缓存由于基于实体主键,缓存粒度细,命中率高,因此访问效率通常优于 MyBatis 结果缓存。MyBatis 结果缓存依赖完全相同的 SQL 和参数,命中率相对较低,且缓存的结果需要反序列化成对象,性能略逊一筹。但 MyBatis 缓存更灵活,适合复杂查询场景。实际选择要结合业务特点。
MyBatis 在执行写操作后会直接清除当前 SqlSession 或 Mapper 对应的缓存区域,防止读取脏数据,但不会维护缓存一致性。而 JPA 则是基于实体生命周期管理缓存,CUD 操作后会自动更新缓存中对应实体的状态,确保缓存始终与数据库一致。JPA 缓存更新更智能,但控制相对复杂;MyBatis 缓存简单粗暴,控制更灵活。
5. JPA 如何实现分页?
在 JPA 中分页可以通过 JpaRepository
接口提供的 findAll(Pageable pageable)
方法快速实现,也可以结合自定义 @Query
和 Pageable 参数实现分页。对于更复杂的查询,可使用 EntityManager 执行原生 SQL 并手动设置 setFirstResult()
和 setMaxResults()
进行分页。整体上,JPA 分页底层是通过 limit offset
实现的,支持多种方式。
Page<T> findAll(Pageable pageable);Pageable pageable = PageRequest.of(0, 10, Sort.by("createTime").descending());
Page<User> userPage = userRepository.findAll(pageable);@Query("SELECT u FROM User u WHERE u.status = :status")
Page<User> findByStatus(@Param("status") Integer status, Pageable pageable);Page<User> page = userRepository.findByStatus(1, PageRequest.of(1, 20));Query query = entityManager.createNativeQuery("SELECT * FROM user WHERE status = ?", User.class);
query.setParameter(1, 1);
query.setFirstResult(0); // offset
query.setMaxResults(10); // page size
List<User> resultList = query.getResultList();
6. 什么是 N+1 查询问题?JPA 如何防止 N+1 查询问题?
N+1 查询问题是指:你执行了 1 条查询语句获取了主实体列表,但为了获取每个主实体的关联数据,又触发了 N 条额外的查询,共执行 N+1 次查询。
在实际项目中,我们采用 JPA 管理实体关系,但在复杂查询场景中做了区分:
- 对于一对多的单集合,我们使用
@EntityGraph
或JOIN FETCH
一次性加载 - 对于多个集合的场景,为了避免笛卡尔积,我们拆分为多条 SQL 查询并在服务层聚合
- 一对一可以直接 fetch,多对多则更建议拆分或优化结构处理
这种方式兼顾了查询性能与代码结构,是我们在 JPA 使用上的一套标准规范。
- 对于一对多的单集合,我们使用
EntityGraph 和 JOIN FETCH 能有效解决一对一和一对多的单集合场景。但当面对多个集合或多对多结构时,使用多个 JOIN FETCH 会导致笛卡尔积,带来严重性能问题。我们会采用分批拉取子表数据并按主键分组组装的方式替代,避免了真正的 N+1 查询问题,同时保持数据结构清晰和查询效率。
N+1 查询问题广泛存在于 ORM 框架中,虽然最常出现在懒加载的查询场景中,但更新(saveAll)、插入(saveAll)、删除操作中如果未启用批处理,同样也可能出现 N+1 的问题。因此我们通常会在 JPA 配置中启用 hibernate.jdbc.batch_size
等批处理参数,同时在逻辑层避免逐条操作,通过批处理语句或一次性提交操作来优化性能。而且saveAll()
和 JPA 的自动更新(dirty checking)本质上是针对每个实体单独生成一条 UPDATE
SQL,即使开启了 Hibernate 的 JDBC 批量功能,也只是把多条 SQL 放到同一个批次网络发送,数据库还是执行多条 UPDATE
,只不过减少了网络开销。
- 如果你想真正做到“一条 SQL 更新多条数据”,必须使用 JPQL 的批量更新语句或者原生 SQL,比如:
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.age > :age")
int bulkUpdate(@Param("status") Integer status, @Param("age") Integer age);
这条语句就是一条 SQL 在数据库执行,效率最高。
在 Spring Data JPA 中,我们通常使用 deleteByIdIn(List<Long> ids)
实现批量删除,这种方式底层执行的是一条 DELETE IN 语句,效率高且不加载实体,适合不需要触发实体事件的场景。相比手动遍历删除,它更适合大批量数据删除,尤其是在没有复杂业务逻辑时。
7. JPA/Hibernate 的懒加载和立即加载?
在 JPA 中,懒加载和立即加载控制着关联实体什么时候加载。懒加载可以在真正访问字段时再查询,提升查询效率,是默认行为;而立即加载则在主表查询时立即加载全部关联数据,使用方便但可能带来性能问题。我们通常对一对多、多对多使用懒加载,并结合 EntityGraph、Join Fetch 等方式显式控制加载时机,避免产生 N+1 查询或懒加载异常。
8. JPA 如何实现事务管理?
在 JPA 中事务由 Spring 统一管理,我们通常使用 @Transactional
来声明事务边界。Spring 会为事务方法创建代理,在方法执行前开启事务,方法正常结束提交事务,异常则自动回滚。默认情况下只对 RuntimeException 回滚,若需处理受检异常需要手动指定。JPA 操作必须在事务中执行,否则会抛出 TransactionRequiredException
,我们也可以通过事务传播机制控制方法嵌套的事务行为。
9. JPA 高性能分页与大数据量优化策略
默认 JPA 分页基于 LIMIT + OFFSET 实现,简单易用,但底层使用的是跳过(skip scan),越往后性能越差。性能线性下降!当数据量大、页码高时性能严重下降。为了解决这一问题,我们使用覆盖索引 + where id > ?
进行 游标分页(Keyset Pagination)实现高效翻页;此外还可通过投影查询减少字段,或结合数据库索引与分表策略优化分页性能。实际项目中会根据场景综合选择分页策略。
10. JPA + Redis 缓存实战方案
JPA 默认开启了一级缓存,基于 EntityManager 实例,在事务内缓存同一个实体对象,避免重复查询数据库。而二级缓存是 Hibernate 的扩展功能,用于不同 EntityManager 实例之间共享实体缓存,需要通过注解和配置文件显式启用。 常通过 EhCache、Redis 等作为缓存实现。但考虑到其复杂性和集群一致性问题,实际项目中建议优先使用 Spring Cache + Redis 替代原生方案。 为了实现分布式共享,我们通常结合 Redis 或 Caffeine 等缓存框架替代原生二级缓存,搭配 Spring Cache 更灵活。
实际项目中我们通常采用 Spring Cache 来处理接口层的缓存,比如常规的查询接口、字典表、配置项等,这样可以借助注解简化代码。而对于 Redis 的高级数据结构操作(如排行榜、限流、锁等),则通过 RedisTemplate 实现。在我们项目中,我们进一步封装了一个统一的缓存组件,底层既支持 Spring Cache,也支持 RedisTemplate,便于统一 TTL 策略、命名规则与缓存失效控制。
11. 为什么已经有了 JPA,还要用 MyBatis?
实际项目中我们既使用过 JPA,也使用过 MyBatis,一般来说业务模型结构清晰、查询简单的模块我们使用 Spring Data JPA 实现,利用它的自动查询机制提升开发效率;而对于多表关联复杂、SQL 调优要求高的模块,我们使用 MyBatis 或 MyBatis-Plus 来手动编写 SQL,更加灵活可控。在一些大型项目中,两者并存是非常常见的。
12. JPA Entity 生命周期
状态 | 说明 | 触发方式/操作 | 是否持久化到数据库 |
瞬时态(Transient) | 实例刚创建,未关联到持久化上下文 |
;未调用 | 否 |
托管态(Managed / Persistent) | 实体被 EntityManager 管理,处于持久化上下文中 | 调用 或 查出对象 | 是 |
游离态(Detached) | 实体曾被管理,但现在已脱离持久化上下文 | 调用 ,事务结束,EntityManager 关闭 | 否(但数据库有记录) |
删除态(Removed) | 实体被标记为删除,等待同步到数据库 | 调用 | 是(删除操作) |
当前状态 | 操作 | 结果状态 |
瞬时态 |
| 托管态 |
托管态 |
| 删除态 |
托管态 |
| 游离态 |
游离态 |
| 托管态 |
删除态 | 事务提交/刷新 | 数据库中删除 |
游离态/瞬时态 | 无 | 不受 EntityManager 管理 |
13. JPA 如何联表查询?
1. 基于实体关联映射的查询
JPA 支持通过实体间的关系(如 @OneToMany
、@ManyToOne
、@ManyToMany
、@OneToOne
)来自动联表查询。示例:假设 Order
和 Customer
关联,Order
有个 customer
属性:
@Entity
public class Order {@ManyToOne@JoinColumn(name = "customer_id")private Customer customer;// other fields...
}
查询所有订单及其客户:
List<Order> orders = entityManager.createQuery("SELECT o FROM Order o JOIN FETCH o.customer", Order.class).getResultList();
这里 JOIN FETCH
用于立即加载客户,避免 N+1 查询。
2. 使用 JPQL 显式写联表查询
你也可以写类似 SQL 的 JPQL 联表查询:
SELECT o, c FROM Order o JOIN o.customer c WHERE c.status = :status
也可以写普通 JOIN
或者 LEFT JOIN
。
14. 什么是全自动 ORM 与 半自动 ORM?
全自动 ORM 是指框架根据实体类和注解自动生成所有 SQL 语句,开发者无需手写 SQL,适合简单的 CRUD 和业务场景,代表技术如 JPA、Spring Data JPA。优点是开发效率高,但对复杂查询和性能调优支持有限。
半自动 ORM 则是开发者需要手写 SQL 或 XML 映射来完成复杂查询,框架负责对象映射和部分 CRUD 操作,代表技术如 MyBatis。它灵活度高,适合复杂业务,但开发和维护成本较大。
总结来说,全自动 ORM 适合快速开发和简单场景,半自动 ORM 更适合复杂查询和性能优化需求。
15. JPQL 和 SQL 的区别
JPQL 是 JPA 定义的面向对象查询语言,操作的是实体类和属性,不直接操作数据库表,语法类似 SQL,但用实体名和属性名替代表名和字段名。JPQL 具有数据库无关性,能方便地查询实体间的关系和继承结构。
而 SQL 是数据库的标准查询语言,直接操作数据库表和字段,语法依赖具体数据库方言,功能更强大,适合复杂查询和性能调优。
总结来说,JPQL 更适合 ORM 场景的对象查询,SQL 则适合原生复杂查询。