下载工作台
Java 数据库实战

Spring Data JPA 对照速通

试读上半部分 · 解锁后可读全文

第 13 章 · Spring Data JPA 对照速通

本章目标:在未启动 Spring Boot 项目的前提下,通过 代码片段 + 对照表 理解 Spring Data JPA 核心能力——JpaRepository 派生查询、@Query JPQL/原生 SQL分页事务注解;将 ch10~ch11EntityManager / JPQL 写法映射到 Repository 风格;明确 shop-db 字段契约 slug + is_published + price(分) 在 JPA 实体中的表达;为 spring-boot-web ch05 正式跟练做好概念预热。

学时建议:4~5 小时(含 2 小时对照 ch10/ch11 复习)

前置:完成 java-database ch08~ch12(JDBC、HikariCP、JPA 实体、Flyway);可选浏览 spring-boot-web ch05 目录结构。


13.1 为什么需要 Spring Data JPA

ch10~ch11 已用 EntityManager 手写 CRUD 与 JPQL:

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Product p = em.find(Product.class, 1L);
em.getTransaction().commit();
em.close();

在 Web 工程中重复上述样板代码成本高。Spring Data JPA 在 Hibernate/JPA 之上提供:

能力纯 JPA(ch10)Spring Data JPA
按主键查em.findrepository.findById
条件查询手写 JPQL方法名派生 findBySlug
复杂 SQL@NamedQuery@Query
分页setFirstResultPageable
事务手动 begin/commit@Transactional(Boot 提供)
java-database ch10~11          本章(概念速通)           spring-boot-web ch05
EntityManager + JPQL    ──►    JpaRepository 接口    ──►   @Service + Boot 自动配置
shop-db 实体映射                @Query 对照练习              shop-spring-demo 持久化
本章不要求本地跑 Spring Boot。片段可在 IDE 阅读;完整可运行环境留到 spring-boot-web ch05

13.2 shop-db Product 实体(复习 + 契约)

ch07 电商表设计python-database shop-db 对齐:

package com.zixian.shopdb.catalog;

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

@Entity
@Table(name = "products", indexes = {
    @Index(name = "idx_products_published_id", columnList = "is_published, id"),
    @Index(name = "uk_products_slug", columnList = "slug", unique = true)
})
public class Product {

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

    @Column(nullable = false, unique = true, length = 128)
    private String slug;

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

    /** 价格(分),禁止 BigDecimal 元 */
    @Column(nullable = false)
    private Long price;

    @Column(name = "is_published", nullable = false)
    private boolean published;

    @Column(name = "category_id")
    private Long categoryId;

    @Column(name = "view_count", nullable = false)
    private Long viewCount = 0L;

    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt = LocalDateTime.now();

    protected Product() {}

    public Product(String slug, String name, long priceCents, boolean published) {
        this.slug = slug;
        this.name = name;
        this.price = priceCents;
        this.published = published;
    }

    // getters / setters 省略
}
契约字段JPA 映射常见错误
slug@Column(unique=true)用 sku 代替 slug
price 分Long / BIGINTBigDecimal
is_publishedboolean published + @Column(name="is_published")缺索引

JSON/API 层(spring-boot-web 将对接)始终输出 price 整数分

{"slug": "java-handbook", "name": "Java 手册", "price": 6800, "is_published": true}

13.3 JpaRepository 接口基础

Spring Data 核心:声明接口,运行时生成实现。

package com.zixian.shopdb.catalog;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

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

    Optional<Product> findBySlug(String slug);

    Optional<Product> findBySlugAndPublishedTrue(String slug);

    long countByPublishedTrue();
}
方法名片段生成语义
findBySELECT … WHERE
AndAND
PublishedTrueis_published = true
OrderByIdDescORDER BY id DESC
countByCOUNT

13.3.1 与 ch11 JPQL 对照

ch11 EntityManagerSpring Data
em.createQuery("SELECT p FROM Product p WHERE p.slug = :s", Product.class).setParameter("s", slug)findBySlug(slug)
… WHERE p.published = truefindByPublishedTrue()
… ORDER BY p.id DESCfindAllByOrderByIdDesc()

13.3.2 分页列表示例

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface ProductRepository extends JpaRepository<Product, Long> {

    Page<Product> findByPublishedTrue(Pageable pageable);

    Page<Product> findByPublishedTrueAndCategoryId(Long categoryId, Pageable pageable);
}

调用方(spring-boot-web ch05/ch07 将使用):

// 概念片段:第 1 页,每页 20 条,按 id 降序
Page<Product> page = productRepository.findByPublishedTrue(
    PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "id"))
);

等价 JPQL:

SELECT * FROM products WHERE is_published = 1 ORDER BY id DESC LIMIT 20 OFFSET 0;

13.4 @Query:JPQL 与原生 SQL

派生方法无法表达 JOIN FETCH、聚合、报表时,用 @Query

13.4.1 JPQL + JOIN FETCH(防 N+1)

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ProductRepository extends JpaRepository<Product, Long> {

    @Query("SELECT p FROM Product p JOIN FETCH Category c ON c.id = p.categoryId " +
           "WHERE p.published = true ORDER BY p.id DESC")
    List<Product> findPublishedWithCategoryFetch();
}
若 ch11 已建 @ManyToOne Category category,可写 JOIN FETCH p.category

13.4.2 按 slug 点查(上架商品)

@Query("SELECT p FROM Product p WHERE p.slug = :slug AND p.published = true")
Optional<Product> findPublishedBySlug(@Param("slug") String slug);

13.4.3 原生 SQL(报表/stat)

@Query(value = """
    SELECT p.slug, p.name, p.price AS price_cents, p.is_published
    FROM products p
    WHERE p.is_published = 1
    ORDER BY p.view_count DESC
    LIMIT :limit
    """, nativeQuery = true)
List<Object[]> findTopViewedPublished(@Param("limit") int limit);
类型何时用注意
JPQL实体字段名、关联表名用实体 @Table
nativeQuery复杂报表、MySQL 函数列名与表名是 DB 层

13.4.4 投影 DTO(spring-boot-web ch17 预热)

public record ProductSummary(String slug, String name, long price, boolean published) {}

@Query("SELECT new com.zixian.shopdb.catalog.ProductSummary(p.slug, p.name, p.price, p.published) " +
       "FROM Product p WHERE p.published = true")
List<ProductSummary> listPublishedSummaries();

只查四列,减少内存与序列化开销——与 spring-boot-web ch17 DTO 投影 同思路。


13.5 写操作与 @Modifying

import org.springframework.data.jpa.repository.Modifying;

public interface ProductRepository extends JpaRepository<Product, Long> {

    @Modifying(clearAutomatically = true)
    @Query("UPDATE Product p SET p.viewCount = p.viewCount + 1 WHERE p.slug = :slug")
    int incrementViewCount(@Param("slug") String slug);
}

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。