Spring Data JPA

技术栈
后端框架
javaspringJPAHibernateORM数据访问

概览

Spring Data JPA

Spring Data JPA 是 Spring Data 生态中基于 JPA(Java Persistence API)规范的数据访问层框架。

是什么

在 JPA 规范之上提供 Repository 抽象,封装 Hibernate 等 JPA 实现,极大简化数据库操作代码。通过接口命名约定和方法名解析,开发者无需手写 SQL 即可实现大多数 CRUD 和查询操作。

解决什么问题

  • 消除样板代码:无需编写 DAO 实现类
  • 方法命名查询findByNameAndAge 自动生成 SQL
  • 分页与排序:开箱即用的 Pageable/Sort 支持
  • 审计功能:自动填充创建时间、修改时间

关键特性

  • Repository 接口抽象:CrudRepository / JpaRepository / PagingAndSortingRepository
  • 方法查询:根据命名约定自动生成 JPQL
  • @Query 注解:自定义 JPQL / 原生 SQL
  • Specification:动态查询条件组装
  • 审计注解:@CreatedDate / @LastModifiedDate
  • 乐观锁:@Version 支持

安装

Spring Data JPA 安装指南

1. 环境准备

前置条件

  • JDK 8+(Spring Data JPA 3.x 需 JDK 17+)
  • 已配置数据库(MySQL 8.0+ / PostgreSQL 14+ / H2 等)

构建工具

  • Maven 3.6+ 或 Gradle 7+

2. 安装步骤

Maven

<dependencies>
    <!-- Spring Data JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- 数据库驱动(以 MySQL 为例) -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- H2 内存数据库(测试用) -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.mysql:mysql-connector-j'
    testImplementation 'com.h2database:h2'
}

数据源配置

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    hibernate:
      ddl-auto: update   # none / validate / update / create / create-drop
    show-sql: false
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQLDialect
        format_sql: true

3. 常见安装问题

Q: ddl-auto 选哪个值?

  • 开发环境:update(自动更新表结构)
  • 测试环境:create-drop(每次测试重建)
  • 生产环境:validate(仅验证,不变更结构)或 none

Q: Hibernate Dialect 报错?

确保数据库方言与数据库版本匹配。Spring Boot 3.x 通常自动检测,不需要手动设置。

Q: N+1 查询问题?

使用 @EntityGraphJOIN FETCH 解决:

@Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :id")
Optional<User> findByIdWithOrders(@Param("id") Long id);

示例

Spring Data JPA CRUD 与动态查询示例

目标

演示 Spring Data JPA 的基本 CRUD、命名查询、@Query 自定义查询、Specification 动态查询。

完整代码

1. 实体定义

package com.example.entity;

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "products")
public class Product {

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

    @Column(nullable = false, length = 100)
    private String name;

    @Column(precision = 10, scale = 2)
    private Double price;

    private Integer stock;

    @Enumerated(EnumType.STRING)
    private Status status;

    private LocalDateTime createdAt;

    public enum Status { ACTIVE, INACTIVE, DISCONTINUED }

    // 无参构造(JPA 必需)
    public Product() {}

    public Product(String name, Double price, Integer stock) {
        this.name = name;
        this.price = price;
        this.stock = stock;
        this.status = Status.ACTIVE;
        this.createdAt = LocalDateTime.now();
    }

    // getters & setters ...
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Double getPrice() { return price; }
    public void setPrice(Double price) { this.price = price; }
    public Integer getStock() { return stock; }
    public void setStock(Integer stock) { this.stock = stock; }
    public Status getStatus() { return status; }
    public void setStatus(Status status) { this.status = status; }
    public LocalDateTime getCreatedAt() { return createdAt; }
}

2. Repository 接口

package com.example.repository;

import com.example.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface ProductRepository extends
        JpaRepository<Product, Long>,
        JpaSpecificationExecutor<Product> {

    // 方法命名查询
    List<Product> findByNameContaining(String keyword);

    List<Product> findByPriceBetween(Double min, Double max);

    Optional<Product> findByName(String name);

    List<Product> findByStatusOrderByCreatedAtDesc(Product.Status status);

    boolean existsByName(String name);

    long countByStatus(Product.Status status);

    // JPQL 自定义查询
    @Query("SELECT p FROM Product p WHERE p.price >= :minPrice AND p.stock > 0")
    List<Product> findAvailableProducts(@Param("minPrice") Double minPrice);

    // 原生 SQL
    @Query(value = "SELECT * FROM products WHERE stock < :threshold",
           nativeQuery = true)
    List<Product> findLowStock(@Param("threshold") Integer threshold);

    // 更新操作
    @Modifying
    @Query("UPDATE Product p SET p.stock = p.stock - :quantity " +
           "WHERE p.id = :id AND p.stock >= :quantity")
    int deductStock(@Param("id") Long id, @Param("quantity") Integer quantity);
}

3. Specification 动态查询

package com.example.specification;

import com.example.entity.Product;
import org.springframework.data.jpa.domain.Specification;
import jakarta.persistence.criteria.*;
import java.util.ArrayList;
import java.util.List;

public class ProductSpecification {

    public static Specification<Product> filterBy(
            String nameLike,
            Double minPrice,
            Double maxPrice,
            Product.Status status) {

        return (Root<Product> root, CriteriaQuery<?> query,
                CriteriaBuilder cb) -> {
            List<Predicate> predicates = new ArrayList<>();

            if (nameLike != null && !nameLike.isEmpty()) {
                predicates.add(
                    cb.like(root.get("name"), "%" + nameLike + "%"));
            }
            if (minPrice != null) {
                predicates.add(
                    cb.greaterThanOrEqualTo(root.get("price"), minPrice));
            }
            if (maxPrice != null) {
                predicates.add(
                    cb.lessThanOrEqualTo(root.get("price"), maxPrice));
            }
            if (status != null) {
                predicates.add(cb.equal(root.get("status"), status));
            }

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

4. 使用示例

package com.example;

import com.example.entity.Product;
import com.example.entity.Product.Status;
import com.example.repository.ProductRepository;
import com.example.specification.ProductSpecification;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class ProductService {

    @Autowired
    private ProductRepository productRepository;

    // 基本 CRUD
    public Product create(String name, Double price, Integer stock) {
        return productRepository.save(new Product(name, price, stock));
    }

    public Product findById(Long id) {
        return productRepository.findById(id)
            .orElseThrow(() -> new RuntimeException("Product not found"));
    }

    // 分页查询
    public Page<Product> searchWithPagination(String name, int page, int size) {
        return productRepository.findByNameContaining(name,
            PageRequest.of(page, size, Sort.by("price").ascending()));
    }

    // 动态查询
    public List<Product> dynamicSearch(
            String name, Double minPrice, Double maxPrice, Status status) {
        return productRepository.findAll(
            ProductSpecification.filterBy(name, minPrice, maxPrice, status));
    }

    // 扣减库存(事务保证)
    public boolean purchase(Long id, int quantity) {
        int rows = productRepository.deductStock(id, quantity);
        return rows > 0;
    }
}

运行测试

@SpringBootTest
class ProductServiceTest {
    @Autowired
    private ProductService productService;

    @Test
    void testCrud() {
        Product p = productService.create("MacBook Pro", 12999.00, 50);
        assertNotNull(p.getId());
        Product found = productService.findById(p.getId());
        assertEquals("MacBook Pro", found.getName());
    }
}

教程

Spring Data JPA 深入与性能优化教程

第一章:实体映射详解

1.1 关联关系

@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)  // 默认 LAZY,建议显式声明
    @JoinColumn(name = "user_id")
    private User user;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "payment_id", unique = true)
    private Payment payment;
}

@Entity
public class OrderItem {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    private Product product;

    private Integer quantity;
}

1.2 FetchType 策略

FetchType 行为 风险
LAZY(推荐) 使用时才加载 需事务内访问,否则 LazyInitializationException
EAGER 立即加载 可能导致 N+1 或 Cartesian 积

1.3 Cascade 级联操作

@OneToMany(mappedBy = "order",
    cascade = {CascadeType.PERSIST, CascadeType.MERGE},
    orphanRemoval = true)
private List<OrderItem> items;

// CascadeType.ALL     = 所有操作级联
// CascadeType.PERSIST = 保存时级联
// CascadeType.MERGE   = 更新时级联
// CascadeType.REMOVE  = 删除时级联
// orphanRemoval=true  = 从集合中移除时自动删除数据库记录

第二章:N+1 问题与解决方案

2.1 问题示例

// 场景:查询 10 个用户及其订单
List<User> users = userRepository.findAll();  // 1 条 SQL: SELECT * FROM users

for (User user : users) {
    // 每个 user.getOrders() 触发 1 条 SQL
    System.out.println(user.getOrders().size());  // 10 条 SQL: SELECT * FROM orders WHERE user_id=?
}
// 总计:11 条 SQL(N+1)

2.2 解决方案

// 方案 1:@EntityGraph
public interface UserRepository extends JpaRepository<User, Long> {
    @EntityGraph(attributePaths = {"orders"})
    List<User> findAll();
}

// 方案 2:JOIN FETCH
@Query("SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.orders")
List<User> findAllWithOrders();

// 方案 3:@BatchSize
@Entity
public class User {
    @BatchSize(size = 100)
    @OneToMany(mappedBy = "user")
    private List<Order> orders;
}
// 将 N 条 SQL 缩减为 ceil(N/100) 条

第三章:审计与乐观锁

3.1 自动审计

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @LastModifiedBy
    private String lastModifiedBy;
}

@Entity
public class Product extends BaseEntity {
    // 业务字段...
}

// 启用审计:@Configuration + @EnableJpaAuditing
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JpaConfig {
    @Bean
    public AuditorAware<String> auditorProvider() {
        return () -> Optional.of(SecurityContextHolder.getContext()
            .getAuthentication().getName());
    }
}

3.2 乐观锁

@Entity
public class Product {
    @Version
    private Long version;

    // 更新时 Hibernate 自动:
    // UPDATE ... SET version = version + 1 WHERE id = ? AND version = ?
    // 如果 version 不匹配,抛出 OptimisticLockException
}

第四章:性能最佳实践

4.1 批量操作

// 批量保存(JDBC batch)
spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true

// 大量数据用 JdbcTemplate 而非 JPA
@Autowired
private JdbcTemplate jdbcTemplate;

public void batchInsert(List<Product> products) {
    jdbcTemplate.batchUpdate(
        "INSERT INTO products(name, price, stock) VALUES (?, ?, ?)",
        products, 100,
        (ps, product) -> {
            ps.setString(1, product.getName());
            ps.setDouble(2, product.getPrice());
            ps.setInt(3, product.getStock());
        }
    );
}

4.2 只读优化

@Transactional(readOnly = true)
public List<Product> findAllProducts() {
    return productRepository.findAll();
}
// Hibernate 脏检查被禁用,减少内存占用

4.3 DTO 投影

// 接口投影(推荐)
public interface ProductSummary {
    String getName();
    Double getPrice();
}

// Repository
@Query("SELECT p.name AS name, p.price AS price FROM Product p")
List<ProductSummary> findSummaries();

// 类投影
@Query("SELECT new com.example.dto.ProductDto(p.name, p.price) FROM Product p")
List<ProductDto> findDtos();

思考题

  1. 为什么在多对一关联中推荐使用 FetchType.LAZY
  2. @Transactional(readOnly = true) 在 JPA 中的具体优化是什么?
  3. 如何避免批量处理中的 OOM(内存溢出)?
  4. JPA 的 Persistence Context 和一级缓存有什么关系?