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
相关产品推荐
相关产品推荐

