JPA 关联关系
Eloquent 的
hasMany/belongsTo/belongsToMany在 JPA 里对应@OneToMany/@ManyToOne/@ManyToMany。概念一一对应,但 JPA 默认懒加载,还有「N+1 问题」,比 Eloquent 更需要注意性能。
一对多:hasMany / belongsTo
User hasMany Post、Post belongsTo User。
一方(User,对应 hasMany)
@Data
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
@OneToMany(mappedBy = "user") // 由对方维护外键
private List<Post> posts = new ArrayList<>();
}多方(Post,对应 belongsTo)
@Data
@Entity
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToOne(fetch = FetchType.LAZY) // 默认懒加载
@JoinColumn(name = "user_id") // 外键字段
private User user;
}对照 Eloquent
| Eloquent | JPA |
|---|---|
User::hasMany(Post::class) | @OneToMany(mappedBy = "user") |
Post::belongsTo(User::class) | @ManyToOne @JoinColumn(name = "user_id") |
$post->user | post.getUser() |
$user->posts | user.getPosts() |
外键在 posts.user_id 表 | @JoinColumn(name = "user_id") 声明 |
mappedBy = "user" 表示:外键在 Post 表(user_id),由 Post 维护。User 这边只是「反向读取」。
懒加载的坑
JPA 的集合/关联默认 LAZY(懒加载):查 User 时不查 posts,访问 user.getPosts() 时才发第二条 SQL。这个设计高效,但配合 JPA 入门 里的 open-in-view: false,在事务外访问懒加载属性会抛 LazyInitializationException。
解决:在 Service 事务内把数据查询完整(user.getPosts() 在事务内访问一次),或在查询时用 join fetch 预加载(见下节)。
多对多:belongsToMany
Post belongsToMany Tag。需要中间表 post_tag:
@Entity
@Table(name = "tags")
@Data
public class Tag {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}@Entity
@Table(name = "posts")
@Data
public class Post {
@ManyToMany
@JoinTable(
name = "post_tag", // 中间表
joinColumns = @JoinColumn(name = "post_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id"))
private List<Tag> tags = new ArrayList<>();
}TIP
Eloquent 里你只需建中间表 + 模型关联;JPA 里用 @JoinTable 声明中间表,和 Eloquent 的 belongsToMany(..., 'post_tag', 'post_id', 'tag_id') 完全对应。
级联:cascade
Eloquent 里 $post->comments()->delete() 手动级联;JPA 有 cascade:
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();cascade = CascadeType.ALL:操作 Post 时连带操作 Comment(保存/删除都带)orphanRemoval = true:从列表移除的 Comment 自动删除
cascade 别滥用
cascade = ALL 一删全删,风险很大。多数场景建议手动管理子实体(commentRepository.delete(...)),别把所有关系都挂级联。这也是 Eloquent 社区「慎用自动级联」的建议。
N+1 查询问题(重点)
Eloquent 有 with('user') 防 N+1;JPA 用 @EntityGraph 或 join fetch:
// ❌ 这样查列表:N 条帖子,访问每条 post.getUser() 又发 N 次 SQL → N+1
List<Post> posts = postRepository.findAll();
// ✅ 方式一:join fetch(JPQL 里写)
public interface PostRepository extends JpaRepository<Post, Long> {
@Query("SELECT p FROM Post p JOIN FETCH p.user")
List<Post> findAllWithUser();
}
// ✅ 方式二:@EntityGraph(推荐,更声明式)
public interface PostRepository extends JpaRepository<Post, Long> {
@EntityGraph(attributePaths = {"user", "comments"})
List<Post> findAll();
}TIP
@EntityGraph = Eloquent 的 with(['user', 'comments'])。一条 SQL 用 JOIN 把关联数据一次取出来,避免 N+1。凡是列表接口返回实体且带关联,记得加 EntityGraph。
对照速查
| Eloquent | JPA |
|---|---|
hasMany | @OneToMany(mappedBy = "xxx") |
belongsTo | @ManyToOne @JoinColumn(name = "xxx_id") |
hasOne | @OneToOne |
belongsToMany | @ManyToMany @JoinTable(...) |
with('relation') 防 N+1 | @EntityGraph(attributePaths = {...}) / JOIN FETCH |
$post->user(懒加载) | post.getUser()(LAZY) |
总结三句话
- 一对多:多方放
@ManyToOne+ 外键,一方@OneToMany(mappedBy=...) - 关联默认懒加载,事务外访问报
LazyInitializationException - 列表查询用
@EntityGraph防 N+1,和 Eloquent 的with()一个道理