Featured image of post Spring Data JPA 学习指南

Spring Data JPA 学习指南

什么是 Spring Data JPA?

Spring Data JPA 是 Spring Data 家族的一员,基于 JPA(Java Persistence API)标准实现,提供了一套简化的数据访问层解决方案。它对 Repository 层进行了抽象,让你只需要声明接口方法,无需编写实现代码。

JPA 与 Hibernate 的关系

  • JPA:Java 持久化 API,是 Java EE 制定的持久化标准接口
  • Hibernate:JPA 的一个实现(还有 EclipseLink、OpenJPA 等)

Spring Data JPA 并不是替代 Hibernate,而是对 Hibernate 进行了封装,让使用更加简便。

项目集成

Maven 依赖

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

基础配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: postgres
    password: secret
    driver-class-name: org.postgresql.Driver
  jpa:
    hibernate:
      ddl-auto: validate  # 生产环境用 validate,开发环境用 update
    show-sql: true        # 显示 SQL 语句
    properties:
      hibernate:
        format_sql: true  # 格式化 SQL
        dialect: org.hibernate.dialect.PostgreSQLDialect

快速开始

定义实体

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Entity
@Table(name = "knowledge_bases", indexes = {
    @Index(name = "idx_kb_hash", columnList = "fileHash", unique = true),
    @Index(name = "idx_kb_category", columnList = "category")
})
public class KnowledgeBaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true, length = 64)
    private String fileHash;

    @Column(nullable = false)
    private String name;

    @Enumerated(EnumType.STRING)
    @Column(length = 20)
    private VectorStatus vectorStatus = VectorStatus.PENDING;

    @PrePersist
    protected void onCreate() {
        uploadedAt = LocalDateTime.now();
    }
}

定义 Repository

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Repository
public interface KnowledgeRepository extends JpaRepository<KnowledgeBaseEntity, Long> {

    // 方法名查询 - 按向量化状态查找
    List<KnowledgeBaseEntity> findAllByVectorStatusOrderByUploadedAtDesc(VectorStatus status);

    // 方法名查询 - 按分类查找
    List<KnowledgeBaseEntity> findByCategoryOrderByUploadedAtDesc(String category);

    // 方法名查询 - 模糊搜索
    Optional<KnowledgeBaseEntity> findByFileHash(String fileHash);

    // JPQL 查询
    @Query("SELECT k FROM KnowledgeBaseEntity k WHERE LOWER(k.name) LIKE LOWER(CONCAT('%', :keyword, '%'))")
    List<KnowledgeBaseEntity> searchByKeyword(@Param("keyword") String keyword);

    // 聚合查询
    @Query("SELECT COALESCE(SUM(k.accessCount), 0) FROM KnowledgeBaseEntity k")
    long sumAccessCount();

    // 原生 SQL 查询
    @Query(value = "SELECT DISTINCT category FROM knowledge_bases WHERE category IS NOT NULL ORDER BY category", nativeQuery = true)
    List<String> findAllCategories();
}

方法命名规则

JPA 支持通过方法命名自动生成查询,方法名遵循特定规则:

常用关键字

关键字 示例 生成的 SQL
findBy findByName(String name) WHERE name = ?
findByNameContaining findByNameContaining(String keyword) WHERE name LIKE '%keyword%'
findByNameStartingWith findByNameStartingWith(String prefix) WHERE name LIKE 'prefix%'
findByAgeGreaterThan findByAgeGreaterThan(int age) WHERE age > ?
findByAgeBetween findByAgeBetween(int min, int max) WHERE age BETWEEN ? AND ?
findByNameOrAge findByNameOrAge(String name, int age) WHERE name = ? OR age = ?
findByNameOrderByAgeDesc findByNameOrderByAgeDesc(String name) WHERE name = ? ORDER BY age DESC
countBy countByName(String name) SELECT COUNT(*) WHERE name = ?
existsBy existsByName(String name) SELECT COUNT(*) > 0 WHERE name = ?
deleteBy deleteByName(String name) DELETE FROM ... WHERE name = ?

JPQL 与原生 SQL

JPQL(Java Persistence Query Language)

1
2
@Query("SELECT k FROM KnowledgeBaseEntity k WHERE LOWER(k.name) LIKE LOWER(CONCAT('%', :keyword, '%')) ORDER BY k.uploadedAt DESC")
List<KnowledgeBaseEntity> searchByKeyword(@Param("keyword") String keywords);

原生 SQL

1
2
@Query(value = "SELECT * FROM knowledge_bases WHERE file_hash = :hash", nativeQuery = true)
Optional<KnowledgeBaseEntity> findByFileHashNative(@Param("hash") String fileHash);

更新和删除

1
2
3
4
5
6
7
@Modifying
@Query("UPDATE KnowledgeBaseEntity k SET k.vectorStatus = :status WHERE k.id = :id")
int updateVectorStatus(@Param("id") Long id, @Param("status") VectorStatus status);

@Modifying
@Query("DELETE FROM KnowledgeBaseEntity k WHERE k.id = :id")
void deleteByIdCustom(@Param("id") Long id);

JPA 与 MyBatis 对比

特性 JPA MyBatis
学习曲线 较陡(概念多) 平缓(SQL 直观)
SQL 控制 自动生成,不可控 完全自己编写
灵活性 低(受限于框架) 高(完全掌控 SQL)
开发效率 高(方法名查询) 中(需写 SQL)
动态 SQL 支持但不够灵活 强大(OGNL 表达式)
性能调优 困难(黑盒) 容易(自己写 SQL)
多表关联 支持(但复杂) 更灵活
适用场景 简单 CRUD、领域驱动设计 复杂查询、报表、系统

JPA 优势

1
2
// 一行代码实现复杂查询
List<User> users = userRepository.findByNameContainingAndAgeGreaterThan("张", 25);

MyBatis 优势

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!-- MyBatis 可以精确控制每一条 SQL -->
<select id="complexSearch" resultType="User">
    SELECT u.*, COUNT(o.id) as order_count
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
    WHERE u.status = 'ACTIVE'
    <if test="name != null">
        AND u.name LIKE #{name}
    </if>
    GROUP BY u.id
    HAVING COUNT(o.id) > #{minOrders}
    ORDER BY order_count DESC
</select>

JPA 与 MyBatis-Plus 对比

特性 JPA MyBatis-Plus
底层框架 Hibernate MyBatis
SQL 生成 自动 自动 + 手写
方法名查询 支持 支持(更强大)
条件构造器 Criteria API(复杂) Lambda 表达式(简洁)
分页插件 Pageable(繁琐) IPage(简洁)
逻辑删除 需手动处理 内置支持
自动填充 不支持 支持(createTime, updateTime)
多租户 不支持 内置支持
乐观锁 @Version 注解 @Version 注解
热加载 需重启 无需重启

MyBatis-Plus Lambda 查询

1
2
3
4
5
6
7
// MyBatis-Plus 的 LambdaQueryWrapper 更简洁
List<User> users = userService.lambdaQuery()
    .like(User::getName, "张")
    .gt(User::getAge, 25)
    .eq(User::getStatus, "ACTIVE")
    .orderByDesc(User::getCreateTime)
    .list();

JPA Specification(复杂查询)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// JPA 处理复杂查询需要使用 Specification
Specification<User> spec = (root, query, cb) -> {
    List<Predicate> predicates = new ArrayList<>();

    if (name != null) {
        predicates.add(cb.like(root.get("name"), "%" + name + "%"));
    }
    if (age != null) {
        predicates.add(cb.greaterThan(root.get("age"), age));
    }

    return cb.and(predicates.toArray(new Predicate[0]));
};

List<User> users = userRepository.findAll(spec);

分页与排序

JPA 分页

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 使用 Pageable
Page<User> page = userRepository.findAll(PageRequest.of(page, size, Sort.by("createTime").descending()));

// 自定义分页查询
@Query("SELECT k FROM KnowledgeBaseEntity k ORDER BY k.uploadedAt DESC")
Page<KnowledgeBaseEntity> findAllOrderByUploadedAtDesc(Pageable pageable);

// 返回分页信息
public Page<KnowledgeBaseDTO> getUsers(int page, int size) {
    Pageable pageable = PageRequest.of(page, size);
    return userRepository.findAll(pageable).map(this::toDTO);
}

MyBatis-Plus 分页

1
2
3
// 使用 IPage
IPage<User> page = new Page<>(page, size);
userMapper.selectPage(page, null);

事务管理

JPA 的事务管理非常简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Service
@Transactional
public class KnowledgeBaseService {

    @Transactional
    public void deleteKnowledgeBase(Long id) {
        // 所有操作在一个事务中
        knowledgeRepository.deleteById(id);
        vectorRepository.deleteByKnowledgeBaseId(id);
        // 如果抛出异常,自动回滚
    }

    // 读取-only 事务,提升性能
    @Transactional(readOnly = true)
    public List<KnowledgeBaseEntity> findAll() {
        return knowledgeRepository.findAll();
    }
}

常见问题处理

1. N+1 查询问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 问题:访问关联对象时触发额外查询
@Entity
public class ResumeAnalysisEntity {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "resume_id")
    private ResumeEntity resume;
}

// 解决:使用 @EntityGraph 或 JOIN FETCH
@Query("SELECT a FROM ResumeAnalysisEntity a JOIN FETCH a.resume WHERE a.id = :id")
Optional<ResumeAnalysisEntity> findByIdWithResume(@Param("id") Long id);

2. 实体状态转换

1
2
3
4
5
6
7
8
// 游离对象(detached)不能直接修改
// 解决方案:从数据库重新查询
@Transactional
public void updateUser(User user) {
    User managed = userRepository.findById(user.getId()).orElseThrow();
    managed.setName(user.getName());  // 修改托管对象
    // 事务提交时自动 flush
}

3. JSON 字段映射

1
2
3
4
// PostgreSQL 的 JSONB 字段
@JdbcTypeCode(SQLTypes.JSON)
@Column(columnDefinition = "jsonb")
private Map<String, Object> metadata;

最佳实践

1. 合理使用 @Transactional

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 读操作不加事务,减少数据库压力
@Transactional(readOnly = true)
public List<User> findAll() { ... }

// 写操作必须加事务
@Transactional
public User save(User user) { ... }

// 删除操作
@Transactional
public void delete(Long id) { ... }

2. 使用 Projection 减少数据传输

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 接口投影
public interface UserNameOnly {
    String getName();
    String getEmail();
}

// 方法投影
List<UserNameOnly> findUserNames();

// DTO 投影
@Query("SELECT new com.example.UserDTO(u.id, u.name) FROM User u")
List<UserDTO> findAllUserDTO();

3. 使用 Example 进行动态查询

1
2
3
4
5
6
7
ExampleMatcher matcher = ExampleMatcher.matching()
    .withIgnorePaths("id")
    .withIncludeNullValues()
    .withMatcher("name", ExampleMatcher.GenericPropertyMatchers.contains());

Example<User> example = Example.of(user, matcher);
List<User> users = repository.findAll(example);

4. 审计字段自动填充

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;

@LastModifiedDate
private LocalDateTime updatedAt;

// 启用审计
@EntityScan
@EnableJpaAuditing

项目中的实际使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@Repository
public interface KnowledgeRepository extends JpaRepository<KnowledgeBaseEntity, Long> {

    // 方法名查询 - 按分类和上传时间查找
    List<KnowledgeBaseEntity> findByCategoryOrderByUploadedAtDesc(String category);

    // 方法名查询 - 统计状态数量
    long countByVectorStatus(VectorStatus vectorStatus);

    // JPQL 查询 - 模糊搜索
    @Query("SELECT k FROM KnowledgeBaseEntity k WHERE LOWER(k.name) LIKE LOWER(CONCAT('%', :keyword, '%')) OR LOWER(k.originalFilename) LIKE LOWER(CONCAT('%', :keyword, '%')) ORDER BY k.uploadedAt DESC")
    List<KnowledgeBaseEntity> searchByKeyword(String keywords);

    // 原生 SQL - 聚合统计
    @Query("SELECT COALESCE(SUM(k.accessCount), 0) FROM KnowledgeBaseEntity k")
    long sumAccessCount();
}

总结

场景 推荐方案
简单 CRUD JPA / MyBatis-Plus
复杂报表 MyBatis / MyBatis-Plus
领域驱动设计 JPA
快速开发 MyBatis-Plus
需要精确控制 SQL MyBatis

Spring Data JPA 适合:

  • 业务模型相对稳定
  • 以领域驱动设计为主
  • 需要快速开发

MyBatis 适合:

  • 复杂查询多
  • 需要精确优化 SQL
  • 历史项目迁移
使用 Hugo 构建
主题 StackJimmy 设计

发布了 32 篇文章 | 共 75016 字