一、项目结构与职责划分
- 实体类(Entity) :写
@ManyToOne、@OneToMany等关系映射注解 - Repository 接口 :写
@EntityGraph、@Query等查询定制注解 - 测试类 / Service:只负责调用方法,不写这些配置
二、核心组件实现
2.1 JpaApplication 启动类
java
@SpringBootApplication
@EnableJpaRepositories(basePackages = "com.example.repository")
public class JpaApplication {
public static void main(String[] args) {
SpringApplication.run(JpaApplication.class, args);
}
}
注解说明:
@SpringBootApplication:组合注解,包含@Configuration、@EnableAutoConfiguration和@ComponentScan,用于启动 Spring Boot 应用@EnableJpaRepositories:启用 Spring Data JPA 仓库功能,指定扫描com.example.repository包下的仓库接口main方法:启动 Spring Boot 应用
2.2 User 实体类
java
@Entity
@Table(name = "sys_user")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", length = 50, nullable = false, unique = true)
private String username;
@Column(name = "email", length = 100)
private String email;
@Column(name = "age")
private Integer age;
@Enumerated(EnumType.STRING)
@Column(name = "status", length = 20)
private UserStatus status = UserStatus.ACTIVE;
@Column(name = "create_time", updatable = false)
private LocalDateTime createTime;
@Column(name = "update_time")
private LocalDateTime updateTime;
@PrePersist
public void prePersist() {
this.createTime = LocalDateTime.now();
this.updateTime = LocalDateTime.now();
}
@PreUpdate
public void preUpdate() {
this.updateTime = LocalDateTime.now();
}
}
重要提醒:
- 数据库里的约束 → 是给数据库自己用的
- Java 代码里的注解 → 是给 JPA 框架用的
- 👉 JPA 根本不会去读取数据库的表结构、自动识别主键/自增!它没这个功能!
JPA 标准注解详解:
| 注解 | 作用 |
|---|---|
@Entity |
告诉 JPA ------ 这个类是实体类,对应数据库一张表 |
@Table |
指定对应的数据库表名为 sys_user |
@Data |
Lombok 注解,自动生成 getter、setter、toString 等方法 |
@Id |
指定主键 |
@GeneratedValue |
指定主键生成策略为自增 |
@Column |
指定字段对应的数据库列名、长度、是否可为空等属性 |
@Enumerated(EnumType.STRING) |
指定枚举类型的存储方式为字符串 |
@PrePersist / @PreUpdate |
JPA 的生命周期回调方法,分别在实体插入和更新前执行 |
2.3 UserStatus 枚举类
java
public enum UserStatus {
ACTIVE, INACTIVE, DELETED
}
定义用户状态的枚举类型。
2.4 UserRepository 接口
java
public interface UserRepository extends JpaRepository<User, Long> {
// JpaRepository 已提供:
// - save(S entity) / saveAll(Iterable<S> entities)
// - findById(ID id) / findAll() / findAllById(Iterable<ID> ids)
// - count() / existsById(ID id)
// - deleteById(ID id) / delete(T entity) / deleteAll()
// - findAll(Sort sort) / findAll(Pageable pageable)
// 自定义查询方法
User findByUsername(String username);
List<User> findByStatus(UserStatus status);
List<User> findByAgeBetween(Integer min, Integer max);
@Query("SELECT u FROM User u WHERE u.email LIKE %:domain")
List<User> findByEmailDomain(@Param("domain") String domain);
}
说明:
- 继承
JpaRepository,获得基本的 CRUD 操作能力 - 自定义查询方法,Spring Data JPA 会根据方法名自动生成 SQL 查询
2.5 UserService 服务类
java
@Service
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
// 业务方法
public List<User> listAll() {
return userRepository.findAll();
}
public Page<User> listPage(int page, int size) {
return userRepository.findAll(PageRequest.of(page, size));
}
}
说明:
@Service:表明这是一个服务层组件@Transactional:开启事务管理- 通过
@Autowired注入UserRepository,实现用户管理的业务逻辑
2.6 QuickStartTest 测试类
java
@SpringBootTest
public class QuickStartTest {
@Autowired
private UserRepository userRepository;
// 测试方法
}
说明:
@SpringBootTest:用于启动 Spring Boot 应用上下文进行测试- 包含插入查询、更新、删除等测试方法,验证代码的功能
三、实体类映射详解
3.1 基础注解
java
@Entity
@Table(
name = "sys_user", // 1. 表名(你懂的)
schema = "mydb", // 2. 数据库名(库名)
indexes = { // 3. 定义【普通索引/唯一索引】
@Index(name = "idx_username", columnList = "username", unique = true),
@Index(name = "idx_create_time", columnList = "create_time")
},
uniqueConstraints = { // 4. 定义【唯一约束】
@UniqueConstraint(name = "uk_email", columnNames = {"email"})
}
)
@Data
public class User {
/**
* @Id - 主键
* @GeneratedValue - 主键生成策略
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
// GenerationType 可选值:
// - IDENTITY: 数据库自增(MySQL)
// - SEQUENCE: 序列(Oracle)
// - TABLE: 使用单独的表生成主键
// - AUTO: JPA 自动选择(默认)
private Long id;
/**
* @Column - 字段映射
*/
@Column(name = "username", // 数据库列名
length = 50, // 长度
nullable = false, // 是否允许 null
unique = true, // 是否唯一
insertable = true, // 是否参与 insert
updatable = true, // 是否参与 update
columnDefinition = "VARCHAR(50) COMMENT '用户名'") // DDL 定义
private String username;
/**
* @Enumerated - 枚举映射
*/
@Enumerated(EnumType.STRING) // 存储枚举名称
// EnumType.ORDINAL: 存储枚举序号(默认,不推荐)
// EnumType.STRING: 存储枚举名称
@Column(name = "status")
private UserStatus status;
/**
* @Temporal - 日期时间映射
*/
@Temporal(TemporalType.TIMESTAMP)
// TemporalType.DATE: 只存日期
// TemporalType.TIME: 只存时间
// TemporalType.TIMESTAMP: 日期时间
@Column(name = "create_time")
private Date createTime;
/**
* @Lob - 大对象
*/
@Lob
@Column(name = "avatar", columnDefinition = "LONGBLOB")
private byte[] avatar;
@Lob
@Column(name = "description", columnDefinition = "LONGTEXT")
private String description;
/**
* @Transient - 忽略映射,临时字段,只在代码运行时用,用完就丢,不存库
*/
@Transient
private String extraField;
/**
* @Basic - 基本配置
*/
@Basic(fetch = FetchType.LAZY) // 延迟加载
@Column(name = "large_content")
private String largeContent;
}
@Lob 详解:
- Lob = Large Object(大对象)
- 是 JPA 标准注解,专门用来存超大数据,分两种:
- BLOB(Binary):存二进制数据 → 图片、文件、音频
- CLOB:存大文本数据 → 超长文章、富文本
- 作用:告诉 JPA → 这个字段不是普通字符串/数字,是大文件,别按普通字段处理!
两个加载模式:
- FetchType.EAGER(默认,立即加载):查询用户时,把所有字段一次性查出来
- FetchType.LAZY(延迟加载) :查询用户时,先不查这个大文本字段,等到代码里调用
user.getLargeContent()时才去数据库查
3.2 复合主键
java
@Data
@Embeddable
public class OrderItemId implements Serializable {
@Column(name = "order_id")
private Long orderId;
@Column(name = "product_id")
private Long productId;
}
说明:
@Embeddable:我是一个复合主键组件,可以被嵌入到实体类里Serializable:JPA 要求复合主键类必须加(固定写法)
java
@Entity
@Table(name = "order_item")
@Data
public class OrderItem {
@EmbeddedId // 用这个注解引入复合主键类
private OrderItemId id;
@Column(name = "quantity")
private Integer quantity;
@Column(name = "price")
private BigDecimal price;
}
另一种方式:@IdClass(过时)
java
@Entity
@Table(name = "order_item2")
@IdClass(OrderItemId.class)
@Data
public class OrderItem2 {
@Id
@Column(name = "order_id")
private Long orderId;
@Id
@Column(name = "product_id")
private Long productId;
@Column(name = "quantity")
private Integer quantity;
}
复合主键场景:
- 一个订单下有多个商品
- 单独
order_id会重复 - 单独
product_id也会重复 order_id + product_id合起来绝对唯一 → 这就是复合主键!
四、关联映射详解
4.1 一对一关联
java
@Entity
@Table(name = "user")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
/**
* 一对一 - 主表
*/
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(
name = "profile_id", // 当前表(user)的外键列名
referencedColumnName = "id" // 关联表(user_profile)的主键
)
private UserProfile profile;
}
java
@Entity
@Table(name = "user_profile")
@Data
public class UserProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String realName;
private String phone;
/**
* 一对一 - 从表(可选)
*/
@OneToOne(mappedBy = "profile", fetch = FetchType.LAZY)
private User user;
}
说明:
@OneToOne:定义一对一关联关系。fetch = FetchType.LAZY表示懒加载,cascade = CascadeType.ALL表示级联操作@JoinColumn:指定外键列名和关联列名mappedBy:表示关系的被维护方- User 有外键 → 用
@JoinColumn - UserProfile 没外键 → 用
mappedBy
4.2 一对多关联
java
@Entity
@Table(name = "dept")
@Data
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
/**
* 一对多 - 部门方
*/
@OneToMany(mappedBy = "department", // 关系由员工方维护
fetch = FetchType.LAZY,
cascade = CascadeType.ALL, // 删部门,员工全删
orphanRemoval = true) // 把员工从部门移除,员工直接删
private List<User> users = new ArrayList<>();
}
java
@Entity
@Table(name = "user")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
/**
* 多对一 - 用户方
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dept_id")
private Department department;
}
orphanRemoval vs CascadeType.REMOVE:
- CascadeType.REMOVE(级联删除):删父 → 子跟着全删,父对象没了,子才删
- orphanRemoval = true(孤儿删除):父还在,只是把子"踢走"(解除关联)→ 子直接删
orphanRemoval = true:谁没人管(孤儿),就把谁清理掉,子对象不能单独存在,脱离父对象就没用了,必须删掉!
4.3 多对多关联
java
@Entity
@Table(name = "user")
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@EqualsAndHashCode.Include
private Long id;
private String username;
/**
* 多对多 - User 方(维护方)
*/
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@JoinTable(name = "user_role", // 中间表的表名
joinColumns = @JoinColumn(name = "user_id"), // 当前表在中间表的外键列
inverseJoinColumns = @JoinColumn(name = "role_id")) // 关联表在中间表的外键列
private Set<Role> roles = new HashSet<>();
}
java
@Entity
@Table(name = "role")
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@EqualsAndHashCode.Include
private Long id;
private String name;
/**
* 多对多 - Role 方(被维护方)
*/
@ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
private Set<User> users = new HashSet<>();
}
说明:
@JoinTable:定义中间表名字 + 两个外键(同时说明了 User 是关系维护方,有外键)joinColumns:当前表(维护方)的外键inverseJoinColumns:对方表的外键mappedBy:标记被维护方,不操作中间表
4.4 关联映射最佳实践
java
@SpringBootTest
public class AssociationMappingTest {
@Autowired
private UserRepository userRepository;
@Autowired
private DepartmentRepository departmentRepository;
/**
* 级联操作
*/
@Test
public void testCascade() {
// 创建部门
Department dept = new Department();
dept.setName("技术部");
// 创建用户
User user1 = new User();
user1.setUsername("张三");
user1.setDepartment(dept); // 🔥 关键:给用户绑定部门(维护外键)
User user2 = new User();
user2.setUsername("李四");
user2.setDepartment(dept);
// 给部门绑定用户(双向关联,完整绑定关系)
dept.setUsers(Arrays.asList(user1, user2));
// 🔥 只保存部门!用户自动保存(级联的魔力)
departmentRepository.save(dept);
}
}
级联的方向:谁配 cascade,操作谁,谁带动别人!
java
@Test
public void testManyToMany() {
// 1. 从数据库查出 id=1 的用户(张三)
User user = userRepository.findById(1L).orElse(null);
// 2. 构造一个已存在的角色对象(id=1,管理员,数据库里已经有了)
// 因为角色已存在,不用查库,只需要设置id,JPA就能识别
Role role = new Role();
role.setId(1L);
// ===================== 第一步:给用户【绑定】角色 =====================
// 把角色添加到用户的角色集合里
user.getRoles().add(role);
// 保存用户(维护方操作 → JPA自动往中间表插数据)
userRepository.save(user);
// ===================== 第二步:给用户【解绑】角色 =====================
// 把角色从用户的角色集合里移除
user.getRoles().remove(role);
// 保存用户(维护方操作 → JPA自动从中间表删数据)
userRepository.save(user);
}
4.5 N+1 查询问题与解决方案
N+1 问题 = 查一次主表,再循环查 N 次子表,SQL 太多,性能爆炸
原因分析:
- 因为用了懒加载(LAZY)
- 查用户时,不查部门(节省性能)
- 调用
getDepartment()时,才临时查部门 - 循环调用 → 循环查库 → N+1
问题代码:
java
@Test
public void testNPlusOneProblem() {
List<User> users = userRepository.findAll();
for (User user : users) {
// 每次 getDepartment() 都会发起一次查询
System.out.println(user.getDepartment().getName());
}
}
解决方案:
| 方案 | 实现方式 | 适用场景 |
|---|---|---|
| JOIN FETCH(最常用、最推荐) | @Query("SELECT u FROM User u JOIN FETCH u.department") |
强制连表查询,一次性把用户+部门全部查出来 |
| EntityGraph(注解版) | @EntityGraph(attributePaths = {"department"}) |
不用写 SQL |
| 批量抓取(折中方案) | @BatchSize(size = 100) 在部门实体的用户集合上加注解 |
一次批量查100个 |
五、JPA 继承映射策略
5.1 SINGLE_TABLE(单表继承)
java
@Entity
@Table(name = "payment")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(
name = "payment_type", // 数据库新建字段用作区分不同的子类
discriminatorType = DiscriminatorType.STRING // 规定这个字段存字符串(ALIPAY/WECHAT)
)
public abstract class Payment {
// 公有字段
private Long id;
private BigDecimal amount; // 金额
private LocalDateTime payTime; // 支付时间
}
@Entity
@DiscriminatorValue("ALIPAY")
public class AlipayPayment extends Payment {
// 子类独有字段
private String alipayAccount; // 支付宝账号
}
说明:
@Inheritance(SINGLE_TABLE)= 父子类共用 1 张表@DiscriminatorColumn= 表中加个类型字段@DiscriminatorValue= 给子类贴个类型标签
5.2 JOINED(联合表继承)
策略对比:
| 策略 | 表数量 | 特点 |
|---|---|---|
| SINGLE_TABLE(单表) | 1 张 | 所有数据挤一起,有鉴别列,有空字段 |
| JOINED(联合表) | 父 1 张 + 子 N 张 | 表拆分最规范,无空值、无冗余 |
java
@Entity
@Table(name = "vehicle")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Vehicle {
private Long id; // 主键
private String brand; // 品牌(公共字段)
private Double price; // 价格(公共字段)
}
@Entity
@Table(name = "car")
@PrimaryKeyJoinColumn(name = "vehicle_id") // 修改子类表的主键列名
@Data
@EqualsAndHashCode(callSuper = true)
public class Car extends Vehicle {
private Integer seatCount; // 座位数(独有字段)
}
说明:
- 父类:只存储所有子类共用的字段,单独建一张表 vehicle
- 子类:只存自己特有的字段,不重复存品牌/价格
5.3 TABLE_PER_CLASS(每类一张表)
java
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Document {
@Id
@GeneratedValue(strategy = GenerationType.AUTO) // 🔥 重点:不能用 IDENTITY
private Long id;
private String title; // 公共字段
private String content; // 公共字段
}
@Entity
@Table(name = "pdf_document")
public class PdfDocument extends Document {
private Integer pageCount; // 独有字段
}
说明:
- 父类是抽象类,不生成数据库表
- 主键必须用 AUTO,不能用 IDENTITY(因为多张子类表需要全局唯一 ID)
- AUTO 全局唯一 ID,不依赖单表自增
六、Repository 自定义扩展
6.1 Repository 继承体系
Repository (标记接口)
└── CrudRepository (CRUD 操作)
└── PagingAndSortingRepository (分页排序)
└── JpaRepository (JPA 扩展,最强大)
6.2 自定义 Repository 三步法
第 1 步:定义自定义接口(写方法名)
java
public interface CustomUserRepository {
// 查询所有激活用户
List<User> findActiveUsers();
// 批量更新用户状态
void updateUserStatusBatch(List<Long> userIds, UserStatus status);
}
第 2 步:写实现类(真正干活的地方)
java
public class CustomUserRepositoryImpl implements CustomUserRepository {
@PersistenceContext
private EntityManager entityManager; // JPA 核心工具
@Override
public List<User> findActiveUsers() {
// JPQL 查询:查状态为 ACTIVE 的用户
return entityManager.createQuery("SELECT u FROM User u WHERE u.status = 'ACTIVE'")
.getResultList();
}
}
说明:
EntityManager:JPA 原生的、最底层的数据库操作工具,我们平时用的JpaRepository,底层全是靠它实现的@PersistenceContext:专门用来注入EntityManager的注解,作用 ≈@Autowired,但这是 JPA 专属注解
工具对比:
| 工具 | 层级 | 用法 | 场景 |
|---|---|---|---|
| JpaRepository | 高层封装 | 简单 CRUD、方法名查询 | 90% 日常开发 |
| EntityManager | 底层原生 | 自定义复杂查询、批量操作 | 自定义 Repository、高级场景 |
为什么不用 @Autowired,要用 @PersistenceContext?
- 因为
EntityManager不是普通的 Bean,它是与事务绑定的线程安全对象
java
@Override
@Transactional
public void updateUserStatusBatch(List<Long> userIds, UserStatus status) {
entityManager.createQuery(
"UPDATE User u SET u.status = :status WHERE u.id IN :ids")
.setParameter("status", status)
.setParameter("ids", userIds)
.executeUpdate();
}
链式调用(方法链) :和 MyBatis-Plus 的
LambdaQueryWrapper/QueryWrapper是完全一样的设计思路!
第 3 步:让你的 UserRepository 继承它
java
public interface UserRepository
extends JpaRepository<User, Long>, CustomUserRepository {
}
6.3 测试效果
java
@SpringBootTest
public class Test {
@Autowired
private UserRepository userRepository;
@Test
public void test() {
// 1. 用 JPA 默认方法
userRepository.findById(1L);
userRepository.findAll();
// 2. 用自定义方法!
List<User> activeUsers = userRepository.findActiveUsers();
userRepository.updateUserStatusBatch(Arrays.asList(1L, 2L), UserStatus.ACTIVE);
}
}
七、查询方法详解
7.1 方法名查询(JPA 最核心、最常用、最推荐)
规则格式: findBy + 属性名 + 关键字 + [And/Or] + 属性名 + 关键字
java
public interface UserRepository extends JpaRepository<User, Long> {
// 1. 等值查询(最最常用)
User findByUsername(String username);
// 2. 不等查询
List<User> findByStatusNot(UserStatus status);
// 3. 模糊查询(包含)
List<User> findByUsernameContaining(String name);
// 4. 范围查询
List<User> findByAgeBetween(Integer min, Integer max);
// 5. IN 查询(批量状态/ID)
List<User> findByStatusIn(List<UserStatus> statuses);
// 6. 非空/为空
List<User> findByEmailIsNull();
// 7. 排序
List<User> findByStatusOrderByCreateTimeDesc(UserStatus status);
// 8. 分页(必用)
Page<User> findByStatus(UserStatus status, Pageable pageable);
// 9. 关联对象查询(连表不用写JOIN)
List<User> findByDepartmentName(String deptName);
// 10. 去重
List<User> findDistinctByUsername(String username);
// 11. 限制 Limit 5
User findTop5ByUsername(String username);
}
JPA vs MyBatis-Plus 对比:
| 操作 | MyBatis-Plus 写法 | JPA 方法命名查询 |
|---|---|---|
| 用户名 = 张三 | eq(User::getUsername, "张三") |
findByUsername("张三") |
| 用户名包含张 | like(User::getUsername, "%张%") |
findByUsernameContaining("张") |
| 年龄 20-30 | between(age, 20, 30) |
findByAgeBetween(20, 30) |
| 状态在指定列表中 | in(status, list) |
findByStatusIn(list) |
| 按创建时间倒序 | orderByDesc(createTime) |
findByStatusOrderByCreateTimeDesc |
7.2 @Query 查询
java
public interface UserRepository extends JpaRepository<User, Long> {
/**
* 基本 JPQL 查询
*/
@Query("SELECT u FROM User u WHERE u.username = :username")
User findByUsernameJPQL(@Param("username") String username);
}
注意事项:
- ⚠️ 更新/删除操作 2 个注解缺一不可
java
@Modifying // 标记这是更新/删除语句
@Transactional // 必须开启事务(写在测试类/Service层)
@Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
int updateStatus(...);
- ⚠️ @Modifying 缓存坑(超级常见)
java
@Modifying(clearAutomatically = true, flushAutomatically = true)
// flushAutomatically = true:执行前刷新缓存
// clearAutomatically = true:执行后清空缓存
- ⚠️ 关联查询用 JOIN FETCH 解决 N+1
java
@Query("SELECT u FROM User u JOIN FETCH u.department WHERE u.id = :id")
// 普通 JOIN 无法解决 N+1,必须用 JOIN FETCH
- ⚠️ 原生 SQL 分页必须写 countQuery
java
@Query(
value = "SELECT * FROM user WHERE status = :status",
countQuery = "SELECT COUNT(*) FROM user WHERE status = :status",
nativeQuery = true
)
Page<User> findByStatusNative(...);
- ⚠️ DTO 投影必须写全限定类名
java
@Query("SELECT new com.example.dto.UserDTO(u.id, u.username) FROM User u")
// 类名必须全路径,DTO 必须有对应参数的构造方法
- ⚠️ 模糊查询 LIKE 写法
java
@Query("SELECT u FROM User u WHERE u.username LIKE %:keyword%")
// 不要在传参时手动加 %,直接写在语句里
- ⚠️ 批量更新用 IN,参数支持集合
java
@Query("UPDATE User u SET u.status = :status WHERE u.id IN :ids")
int batchUpdateStatus(@Param("ids") List<Long> ids);
- ⚠️ GROUP BY 分组返回 List<Object[]>
java
@Query("SELECT u.status, COUNT(u) FROM User u GROUP BY u.status")
List<Object[]> countGroupByStatus();
// 下标 0 = status,下标 1 = 数量
7.3 极简口诀(记这个就够)
- 查询不加 Modifying,更新必须加
- 更新必配 Transactional
- JPQL 操作实体,SQL 操作表
- 原生分页要写 countQuery
- 缓存刷新加 clearAutomatically
- 关联查询用 JOIN FETCH
7.4 总结
@Query是 JPA 处理复杂查询的终极方案- 简单查询:用方法命名(不写 SQL)
- 复杂/更新 :用
@Query(手写 JPQL/SQL) - 只要遵守上面的规则,基本不会出 bug!
延伸阅读: