Spring Data JPA 入门
本教程共 48 篇 · 第 27 篇 · 更新于 2026-08-13 · 约 6 分钟阅读
本节目标:理解 ORM 和 JPA 的关系,会写实体类和 Repository 接口,用方法名自动生成查询,完成基本 CRUD。
从对象到表:ORM 在做什么
上一节用 JdbcTemplate 写 SQL,字段一多就很啰嗦。每次都要写 insert into product (name, price) values (?, ?),查出来还要手动 rs.getString 赋值。
ORM(对象关系映射)换个思路:让 Java 对象直接对应数据库表,字段对应列。你操作对象,框架翻译成 SQL。
JPA 是 Java 官方的 ORM 规范,只定义接口和注解,不提供实现。Hibernate 是最流行的 JPA 实现。Spring Data JPA 再进一步:连实现类都不用写,你定义接口,它运行时帮你生成。
三层关系捋一遍:JPA 是标准(规矩),Hibernate 是标准的一个实现(干活的人),Spring Data JPA 是 Spring 家族的便捷封装(发工资的管家)。
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
一个 starter 带齐了三样东西:Hibernate(JPA 实现)、Spring Data JPA(仓库支持)、Spring ORM(事务集成)。
实体类:一张表的 Java 画像
实体类就是「表」的 Java 版本。每个实例对应一行数据:
package com.example.demo.book;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
@Entity
@Table(name = "books")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String title;
private BigDecimal price;
private LocalDate publishDate;
protected Book() {
// JPA 需要无参构造,protected 即可
}
public Book(String title, BigDecimal price, LocalDate publishDate) {
this.title = title;
this.price = price;
this.publishDate = publishDate;
}
// getter / setter
public Long getId() { return id; }
public String getTitle() { return title; }
public BigDecimal getPrice() { return price; }
public LocalDate getPublishDate() { return publishDate; }
}
几个注解各司其职:
@Entity:声明这是 JPA 实体,要建表。@Table(name = "books"):指定表名。不写就用类名。@Id:主键,必须有。@GeneratedValue:主键生成策略,IDENTITY表示数据库自增。@Column:列细节,比如非空、长度。不写注解的字段也会映射,列名默认是属性名。
表名和列名的默认规则是「类名/属性名原样用」。实体 Book 对应表 book,属性 publishDate 对应列 publishDate。想让列名变成下划线风格,可以显式写:
@Column(name = "publish_date")
private LocalDate publishDate;
NoteJPA 规范要求实体有无参构造。你写了带参构造后,无参构造会被覆盖,必须手动补一个。这是初学者最常见的报错来源。
命名空间统一用
jakarta.persistence.*。Spring Boot 3 之前是javax.persistence.*,4.x 全线jakarta,看到旧教程别照抄。
字段类型怎么映射
Java 类型和数据库类型的对应,JPA 基本自动处理:
| Java 类型 | 数据库类型 | 说明 |
|---|---|---|
String | VARCHAR | 可加 length 限制 |
int/Integer | INT | |
long/Long | BIGINT | 主键常用 |
BigDecimal | DECIMAL | 金额必用,别用 double |
LocalDate | DATE | 日期 |
LocalDateTime | TIMESTAMP | 日期时间 |
boolean | BOOLEAN |
枚举要单独处理。默认存的是枚举的 name()(字符串),想要更稳定的存储,用 @Enumerated:
public enum BookStatus {
DRAFT, PUBLISHED, ARCHIVED
}
@Enumerated(EnumType.STRING) // 存字符串 DRAFT/PUBLISHED
private BookStatus status;
EnumType.ORDINAL 存数字下标,字段顺序一变数据就错位,生产环境别用。
主键生成策略 @GeneratedValue 的取值也值得花两句:IDENTITY 依赖数据库自增,MySQL 常用;AUTO 让 Hibernate 按数据库选;UUID 生成字符串主键,适合分布式分库分表场景。单表应用 IDENTITY 最省心。
Repository:只写接口,不写实现
Spring Data JPA 的核心魔法在这里。定义一个接口,继承 JpaRepository:
package com.example.demo.book;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BookRepository extends JpaRepository<Book, Long> {
}
两个泛型参数:实体类型 Book,主键类型 Long。什么都不用写,就能用了。
@Service
public class BookService {
private final BookRepository bookRepository;
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
public Book create(Book book) {
return bookRepository.save(book);
}
public Optional<Book> findById(Long id) {
return bookRepository.findById(id);
}
public List<Book> findAll() {
return bookRepository.findAll();
}
public void deleteById(Long id) {
bookRepository.deleteById(id);
}
}
JpaRepository 自带的 CRUD 方法够日常用:save、findById、findAll、deleteById、count、existsById。findById 返回 Optional,提醒你处理「查不到」的情况,别直接 get()。
save 的语义值得说清楚:它兼做新增和更新。判断依据是主键——主键为 null 就执行 INSERT,主键有值就执行 UPDATE。所以更新前要把 id 塞进实体里。
每次 Repository 调用都在一个隐式事务里。单条操作没问题;多条操作要保证原子性,就在 Service 方法上加 @Transactional(第 26 章讲过)。
save 还有个小坑:传进来的实体和返回的实体不一定是同一个。JPA 可能返回托管后的新实例,所以要用返回值接住:book = bookRepository.save(book);。
派生查询:方法名就是 SQL
想按条件查?给接口加个方法,方法名就是查询规则:
public interface BookRepository extends JpaRepository<Book, Long> {
// 按书名精确查询
List<Book> findByTitle(String title);
// 按价格区间查询:小于等于
List<Book> findByPriceLessThanEqual(BigDecimal price);
// 组合条件:书名 + 出版日期之后
List<Book> findByTitleAndPublishDateAfter(String title, LocalDate date);
// 忽略大小写
List<Book> findByTitleIgnoreCase(String title);
// 排序:价格降序
List<Book> findByPriceGreaterThanOrderByPriceDesc(BigDecimal price);
// 统计
long countByTitle(String title);
}
Spring Data 解析方法名,自动生成实现。规则拆开看很简单:findBy 开头,后面跟属性名,And/Or 连接多个条件,LessThan、After、GreaterThan 是比较操作符。
常用操作符再列一张表:
| 关键字 | 示例 | 生成的 SQL 语义 |
|---|---|---|
Containing | findByTitleContaining | LIKE %值% |
StartingWith | findByTitleStartingWith | LIKE 值% |
Between | findByPriceBetween | BETWEEN 两个值 |
In | findByIdIn | IN 列表 |
IsNull/IsNotNull | findByAuthorIsNull | IS NULL / IS NOT NULL |
OrderBy...Asc/Desc | findByPriceOrderByPriceDesc | 排序 |
Top/First | findFirst3ByOrderByPriceDesc | 限制条数 |
findFirst3ByOrderByPriceDesc 这种「取最贵的前三本」的需求,一行方法名就表达了。
Warning属性名必须和实体字段完全一致(驼峰命名)。写错启动不报错,运行时才报「无法解析属性」,排查时先对字段名。
跑通第一个完整示例
学了就敲,敲了就跑。加一个启动即执行的 Runner,验证整条链路:
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class BookDataRunner implements CommandLineRunner {
private final BookRepository bookRepository;
public BookDataRunner(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@Override
public void run(String... args) {
// 新增
bookRepository.save(new Book("Spring Boot 入门", new BigDecimal("59.00"), LocalDate.of(2026, 6, 1)));
bookRepository.save(new Book("Java 核心技术", new BigDecimal("99.00"), LocalDate.of(2025, 3, 15)));
// 查询
System.out.println(bookRepository.findByTitle("Spring Boot 入门"));
System.out.println(bookRepository.findByPriceLessThanEqual(new BigDecimal("80")));
System.out.println("总数:" + bookRepository.count());
}
}
CommandLineRunner 在应用启动完成后执行,是演示数据访问的最快方式。跑 mvn spring-boot:run,控制台就能看到查询结果,配合 show-sql: true 还能观察 Hibernate 生成的 SQL。
配置:开发环境最常用设置
application.yml 里两行配置,开发调试必备:
spring:
jpa:
hibernate:
ddl-auto: update # 启动时按实体自动建表/更新表
show-sql: true # 控制台打印生成的 SQL
ddl-auto 有五个取值,开发用 update 或 create-drop,生产要慎重:
| 取值 | 行为 | 建议 |
|---|---|---|
none | 不生成表 | 生产环境 |
update | 有差异就更新表结构 | 开发方便 |
create | 每次启动先删后建 | 会丢数据 |
create-drop | 启动建表,关闭删表 | 测试 |
validate | 只校验表结构是否匹配 | 生产可选 |
Tip启动日志里出现
spring.jpa.open-in-view is enabled by default的警告时,在配置里加spring.jpa.open-in-view: false。视图层懒加载是历史包袱,REST 项目基本用不上,关掉更省心。
实体扫描:约定优于配置
Spring Boot 自动扫描主类所在包及其子包下的 @Entity。所以实体类别乱放,放在主类的子包里就对了。
想自定义扫描范围,用 @EntityScan 指定包。Repository 接口同理,Spring Boot 自动扫描,需要手动指定时用 @EnableJpaRepositories。
小结
JPA 把表和对象对应起来,Spring Data JPA 把实现类也省了。三步走:写实体类、写 Repository 接口、注入使用。派生查询靠方法名生成 SQL,简单条件不用写一句 SQL。
本章示例都基于 H2 内存库。换成 MySQL 只需改数据源配置,实体和 Repository 一行不动——这就是 JPA 屏蔽数据库差异的价值。
下一节进阶:分页、排序,以及用 @Query 写自定义查询。