文档站点
Skip to content

JPA 关联关系 ​

Eloquent 的 hasMany / belongsTo / belongsToMany 在 JPA 里对应 @OneToMany / @ManyToOne / @ManyToMany。概念一一对应,但 JPA 默认懒加载,还有「N+1 问题」,比 Eloquent 更需要注意性能。

一对多:hasMany / belongsTo ​

User hasMany Post、Post belongsTo User。

一方(User,对应 hasMany) ​

java
@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) ​

java
@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

EloquentJPA
User::hasMany(Post::class)@OneToMany(mappedBy = "user")
Post::belongsTo(User::class)@ManyToOne @JoinColumn(name = "user_id")
$post->userpost.getUser()
$user->postsuser.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:

java
@Entity
@Table(name = "tags")
@Data
public class Tag {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
}
java
@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:

java
@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:

java
// ❌ 这样查列表: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。

对照速查 ​

EloquentJPA
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)

总结三句话

  1. 一对多:多方放 @ManyToOne + 外键,一方 @OneToMany(mappedBy=...)
  2. 关联默认懒加载,事务外访问报 LazyInitializationException
  3. 列表查询用 @EntityGraph 防 N+1,和 Eloquent 的 with() 一个道理

面向 PHP / Laravel 开发者的 Spring Boot 中文文档