JPA / Hibernate

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 提供的扩展注解实现时间自动填充等高级功能。

注解

作用

示例

@Entity

声明这是一个实体类,对应数据库表

@Entity

@Table(name="user")

映射表名(可选)

@Table(name="user")

@Id

标记主键字段

@Id

@GeneratedValue(strategy=...)

主键生成策略

@GeneratedValue(strategy = GenerationType.IDENTITY)

策略

说明

IDENTITY

自增(MySQL 常用)

SEQUENCE

数据库序列(Oracle 常用)

TABLE

使用一张表生成主键

AUTO

自动选择适合的策略(默认)

注解

作用

示例

@Column(name="username")

映射字段名(可选)

@Column(nullable = false, length = 50)

@Transient

忽略此字段,不参与映射

不入库的字段

@Lob

大字段(如 text、blob)

文本/二进制

@Temporal

映射日期时间类型(Date)

@Temporal(TemporalType.DATE)

映射类型(Java Date)

TemporalType.DATE

只保存日期(yyyy-MM-dd)

TemporalType.TIME

只保存时间(HH:mm:ss)

TemporalType.TIMESTAMP

保存日期+时间

注解

关系类型

对应关系

@OneToOne

一对一

用户 - 身份证

@OneToMany

一对多

用户 - 订单

@ManyToOne

多对一

订单 - 用户

@ManyToMany

多对多

用户 - 角色

@JoinColumn

指定外键列名

单向关系中用

@JoinTable

中间表配置

多对多时使用

mappedBy

指定关系被维护方

用于双向关系

cascade

联级操作

PERSIST、REMOVE 等

fetch

加载策略

LAZY / EAGER

类型

含义说明

PERSIST

保存时级联保存(save()

MERGE

合并更新时级联更新(merge()

REMOVE

删除时级联删除(delete()

REFRESH

刷新实体状态时也刷新关联对象

DETACH

分离实体时也分离关联对象

ALL

包含以上所有操作(常用)

注解

作用

@PrePersist

插入前执行

@PostPersist

插入后执行

@PreUpdate

更新前执行

@PostUpdate

更新后执行

@PreRemove

删除前执行

@PostRemove

删除后执行

@PostLoad

加载后执行

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 {

    @PersistenceContext
    private EntityManager em;

    @Override
    public <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();
    }

    @Override
    public 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 管理实体关系,但在复杂查询场景中做了区分:

    • 对于一对多的单集合,我们使用 @EntityGraphJOIN 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)

实例刚创建,未关联到持久化上下文

new Entity()

;未调用 persist()

托管态(Managed / Persistent)

实体被 EntityManager 管理,处于持久化上下文中

调用 persist()

find()

查出对象

游离态(Detached)

实体曾被管理,但现在已脱离持久化上下文

调用 detach()

,事务结束,EntityManager 关闭

否(但数据库有记录)

删除态(Removed)

实体被标记为删除,等待同步到数据库

调用 remove()

是(删除操作)

当前状态

操作

结果状态

瞬时态

persist(entity)

托管态

托管态

remove(entity)

删除态

托管态

detach(entity)

游离态

游离态

merge(entity)

托管态

删除态

事务提交/刷新

数据库中删除

游离态/瞬时态

不受 EntityManager 管理

13. JPA 如何联表查询?

1. 基于实体关联映射的查询

JPA 支持通过实体间的关系(如 @OneToMany@ManyToOne@ManyToMany@OneToOne)来自动联表查询。示例:假设 OrderCustomer 关联,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 则适合原生复杂查询。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值