You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring JPA @ManyToMany关联查询遇StackOverFlow异常求助

Spring JPA @ManyToMany关联引发StackOverflow异常的解决

问题场景

我在Spring JPA中实现了Category与Product的多对多关联,实体代码如下:

Product.java

import jakarta.persistence.*;
import lombok.*;

import java.util.Objects;
import java.util.Set;

import org.hibernate.Hibernate;


@Table(name = "Product")
@Entity(name = "Product")
@Builder
@RequiredArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ToString
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String name;

    @Column
    private String description;

    @Column
    private double price;

    @Column
    private Integer quantity;
    
    
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
            name = "product_category",
            joinColumns = @JoinColumn(name = "product_id"),
            inverseJoinColumns = @JoinColumn(name = "category_id")
    )
    private Set<Category> categories;

    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
        Product product = (Product) o;
        return id != null && Objects.equals(id, product.id);
    }

    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
}

Category.java

import jakarta.persistence.*;
import lombok.*;

import java.util.Objects;
import java.util.Set;

import org.hibernate.Hibernate;

@Table(name = "Category")
@Entity(name = "Category")
@Builder
@RequiredArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ToString
public class Category {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String name;


    @ManyToMany(mappedBy = "categories" , fetch = FetchType.EAGER)
    private Set<Product> products;
    
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
        Category category = (Category) o;
        return id != null && Objects.equals(id, category.id);
    }

    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
}

调用CategoryRepo的findAll()方法查询所有分类时,抛出StackOverflow异常,关键异常栈如下:

at java.base/java.lang.String.valueOf(String.java:4216) ~[na:na]
at java.base/java.lang.StringBuilder.append(StringBuilder.java:173) ~[na:na]
at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:457) ~[na:na]
at org.hibernate.collection.spi.PersistentSet.toString(PersistentSet.java:296) ~[hibernate-core-6.1.5.Final.jar:6.1.5.Final]
at java.base/java.lang.StringConcatHelper.stringOf(StringConcatHelper.java:453) ~[na:na]
at com.app.ecommerce.entity.Category.toString(Category.java:48) ~[classes/:na]
at java.base/java.lang.String.valueOf(String.java:4216) ~[na:na]
at java.base/java.lang.StringBuilder.append(StringBuilder.java:173) ~[na:na]
at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:457) ~[na:na]
at org.hibernate.collection.spi.PersistentSet.toString(PersistentSet.java:296) ~[hibernate-core-6.1.5.Final.jar:6.1.5.Final]
at java.base/java.lang.StringConcatHelper.stringOf(StringConcatHelper.java:453) ~[na:na]
at com.app.ecommerce.entity.Product.toString(Product.java:63) ~[classes/:na]
...(循环重复上述调用)

我试过用@JsonIgnore和@JsonIgnoreProperties注解,但没有效果。最终希望返回的分类数据格式为:

[
    {
       "id" : 1,
       "name" : "Mobile"
    }
]

问题原因

从异常栈可以明确看出,StackOverflow是Lombok自动生成的toString()方法循环引用导致的:

  • Category的toString()会打印products集合
  • 集合中每个Product的toString()又会打印categories集合
  • 如此往复调用,直到栈内存耗尽

@JsonIgnore这类注解仅解决JSON序列化时的循环引用,而当前问题出在toString()方法的调用(比如日志打印、调试触发),所以这类注解无效。

解决方法

1. 修改Lombok的@ToString注解,排除关联集合

直接在实体类的@ToString注解中排除对方的关联集合属性:

修改后的Product.java

@Table(name = "Product")
@Entity(name = "Product")
@Builder
@RequiredArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ToString(exclude = "categories") // 排除categories集合
public class Product {
    // 原有代码不变
}

修改后的Category.java

@Table(name = "Category")
@Entity(name = "Category")
@Builder
@RequiredArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ToString(exclude = "products") // 排除products集合
public class Category {
    // 原有代码不变
}

2. 自定义toString()方法,只保留基础属性

如果不想依赖Lombok的配置,可以手动重写toString(),只包含核心属性:

比如Category的toString():

@Override
public String toString() {
    return "Category{" +
            "id=" + id +
            ", name='" + name + '\'' +
            '}';
}

Product的toString()同理:

@Override
public String toString() {
    return "Product{" +
            "id=" + id +
            ", name='" + name + '\'' +
            ", description='" + description + '\'' +
            ", price=" + price +
            ", quantity=" + quantity +
            '}';
}

3. 调整FetchType为LAZY(可选优化)

当前两个实体的关联都用了FetchType.EAGER,会导致查询时立即加载关联集合,不仅可能引发性能问题,也增加了循环引用触发的概率。如果业务允许,建议改为懒加载:

// Product中的关联
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(...)
private Set<Category> categories;

// Category中的关联
@ManyToMany(mappedBy = "categories", fetch = FetchType.LAZY)
private Set<Product> products;

4. 使用DTO精准控制返回格式

如果需要返回的JSON仅包含id和name,可以创建一个CategoryDTO:

public class CategoryDTO {
    private Long id;
    private String name;

    public CategoryDTO(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    // 生成getter方法
    public Long getId() { return id; }
    public String getName() { return name; }
}

然后通过Repository直接查询返回DTO,或者在Service层转换:

// Repository方式:直接查询构造DTO
@Query("SELECT new com.app.ecommerce.dto.CategoryDTO(c.id, c.name) FROM Category c")
List<CategoryDTO> findAllCategoryDTO();

// Service层转换方式
List<Category> categories = categoryRepo.findAll();
List<CategoryDTO> dtoList = categories.stream()
        .map(c -> new CategoryDTO(c.getId(), c.getName()))
        .collect(Collectors.toList());

这种方式既避免了循环引用,又能严格控制返回的数据结构,完全符合需求。

内容的提问来源于stack exchange,提问作者Youssef Gamal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 00:47:55