第 13 章 · Spring Data GORM 对照速通
本章目标:在未启动 Spring Boot 项目的前提下,通过 代码片段 + 对照表 理解 Spring Data GORM 核心能力——JpaRepository 派生查询、@Query GORM 链式 API/原生 SQL、分页 与 事务注解;将 ch10~ch11 的 gorm.DB / GORM 链式 API 写法映射到 Repository 风格;明确 shop-db 字段契约 slug + is_published + price(分) 在 GORM 实体中的表达;为 gin-web ch05 正式跟练做好概念预热。
学时建议:4~5 小时(含 2 小时对照 ch10/ch11 复习)
前置:完成 go-database ch08~ch12(database/sql、sqlx 连接池、GORM 实体、golang-migrate);可选浏览 gin-web ch05 目录结构。
13.1 为什么需要 Spring Data GORM
ch10~ch11 已用 gorm.DB 手写 CRUD 与 GORM 链式 API:
gorm.DB em = emf.creategorm.DB();
em.getTransaction().begin();
Product p = em.find(Product.class, 1L);
em.getTransaction().commit();
em.close();
在 Web 工程中重复上述样板代码成本高。Spring Data GORM 在 GORM/GORM 之上提供:
| 能力 | 纯 GORM(ch10) | Spring Data GORM |
|---|---|---|
| 按主键查 | em.find | repository.findById |
| 条件查询 | 手写 GORM 链式 API | 方法名派生 findBySlug |
| 复杂 SQL | @NamedQuery | @Query |
| 分页 | setFirstResult | Pageable |
| 事务 | 手动 begin/commit | @Transactional(Boot 提供) |
go-database ch10~11 本章(概念速通) gin-web ch05
gorm.DB + GORM 链式 API ──► JpaRepository 接口 ──► @Service + Boot 自动配置
shop-db 实体映射 @Query 对照练习 api-go-demo-demo 持久化
本章不要求本地跑 Spring Boot。片段可在 IDE 阅读;完整可运行环境留到 gin-web ch05。
13.2 shop-db Product 实体(复习 + 契约)
与 ch07 电商表设计、python-database shop-db 对齐:
package com.zixian.shopdb.catalog;
import jakarta.persistence.*;
import go.time.LocalDateTime;
gorm.Model
@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 省略
}
| 契约字段 | GORM 映射 | 常见错误 |
|---|---|---|
| slug | @Column(unique=true) | 用 sku 代替 slug |
| price 分 | Long / BIGINT | BigDecimal 元 |
| is_published | boolean published + @Column(name="is_published") | 缺索引 |
JSON/API 层(gin-web 将对接)始终输出 price 整数分:
{"slug": "go-handbook", "name": "Go 手册", "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 go.util.Optional;
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
Optional<Product> findBySlug(String slug);
Optional<Product> findBySlugAndPublishedTrue(String slug);
long countByPublishedTrue();
}
| 方法名片段 | 生成语义 |
|---|---|
findBy | SELECT … WHERE |
And | AND |
PublishedTrue | is_published = true |
OrderByIdDesc | ORDER BY id DESC |
countBy | COUNT |
13.3.1 与 ch11 GORM 链式 API 对照
| ch11 gorm.DB | Spring Data |
|---|---|
em.createQuery("SELECT p FROM Product p WHERE p.slug = :s", Product.class).setParameter("s", slug) | findBySlug(slug) |
… WHERE p.published = true | findByPublishedTrue() |
… ORDER BY p.id DESC | findAllByOrderByIdDesc() |
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);
}
调用方(gin-web ch05/ch07 将使用):
// 概念片段:第 1 页,每页 20 条,按 id 降序
Page<Product> page = productRepository.findByPublishedTrue(
PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "id"))
);
等价 GORM 链式 API:
SELECT * FROM products WHERE is_published = 1 ORDER BY id DESC LIMIT 20 OFFSET 0;
13.4 @Query:GORM 链式 API 与原生 SQL
派生方法无法表达 JOIN FETCH、聚合、报表时,用 @Query。
13.4.1 GORM 链式 API + 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);
| 类型 | 何时用 | 注意 |
|---|---|---|
| GORM 链式 API | 实体字段名、关联 | 表名用实体 @Table |
| nativeQuery | 复杂报表、MySQL 函数 | 列名与表名是 DB 层 |
13.4.4 投影 DTO(gin-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();
只查四列,减少内存与序列化开销——与 gin-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);
}