@ManyToMany或@OneToMany/@ManyToOne导致循环依赖的问题 java.lang.StackOverflowError jpa
Bidirectional relationships must follow these rules.
- The inverse side of a bidirectional relationship must refer to its owning side by using the
mappedBy
element of the@OneToOne
,@OneToMany
, or@ManyToMany
annotation. ThemappedBy
element designates the property or field in the entity that is the owner of the relationship. - The many side of many-to-one bidirectional relationships must not define the
mappedBy
element. The many side is always the owning side of the relationship. - For one-to-one bidirectional relationships, the owning side corresponds to the side that contains the corresponding foreign key.
- For many-to-many bidirectional relationships, either side may be the owning side.
Unidirectional Relationships
In a unidirectional relationship, only one entity has a relationship field or property that refers to the other. For example, LineItem
would have a relationship field that identifies Product
, but Product
would not have a relationship field or property for LineItem
. In other words, LineItem
knows about Product
, but Product
doesn’t know which LineItem
instances refer to it.
2020年12月6日完工。从此jpa多对多不再是阻碍
2021年11月09日。在此对循环依赖做一个收尾
循环依赖出现的原因
- 情景一:在Spring中,Bean A中注入Bean B,在Bean B中又注入了Bean A。这是两层的循环链,具体业务中,循环链的层数可能更多。
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class BossServiceServiceImpl implements BossServiceService {
@Autowired
private BossProductService bossProductService;
}
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class BossProductServiceImpl implements BossProductService {
@Autowired
private BossServiceService bossServiceService;
}
- 情景二:在Java中,Entity A中有类型为Entity B的属性,Entity B中有Entity A的属性。或者层级更深。当序列化(toString())时,就会因循环依赖导致栈溢出。业务中以使用MapStruct将 entity转为dto为例。
@Getter
@Setter
@ToString
public class BossProductDTO implements Serializable {
// ID
private Long id;
// 名称
private String name;
// 类型,1-基础包产品 2-增值包产品 3-套餐产品
private Integer type;
private Set<BossProductServiceDTO> bossProductServiceEntities;
}
@Getter
@Setter
@ToString
// hashCode导致了栈溢出。所以不要用@Data
public class BossProductServiceDTO implements Serializable {
// ID
private Long id;
// 产品ID
private BossProductDTO bossProductEntity;
// 服务ID
private BossServiceDTO bossServiceEntity;
// 状态:0-下线,1-上线
private Integer status;
private Integer sequence;
}
循环依赖的解决方案
- 针对情景一,可以添加@Lazy注解解决
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class BossProductServiceImpl implements BossProductService {
// @Lazy注解注解的作用主要是减少springIOC容器启动的加载时间
// 当出现注入的循环依赖时,也可以添加@Lazy
@Lazy
@Autowired
private BossServiceService bossServiceService;
}
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class BossServiceServiceImpl implements BossServiceService {
@Lazy
@Autowired
private BossProductService bossProductService;
}
- 针对情景二。
MapStruct官方已给出解决MapStruct中循环依赖的方法; MapStruct关于循环依赖的讨论
但这个只是解决了在toDto、toEntity时的循环依赖问题,在调用toString()方法时,依然会栈溢出。且若实体有重写toString()方法,在转换时(toDto、toEntity中),就可以看到循环调用的情况(不重写toString()就不会有这种情况)。
所以当前找到的解决方法,还是使用额外的Dto,来中断循环链
@Getter
@Setter
@ToString
public class BossProductDTO implements Serializable {
// ID
private Long id;
// 名称
private String name;
// 类型,1-基础包产品 2-增值包产品 3-套餐产品
private Integer type;
private Set<BossProductServiceDTO> bossProductServiceEntities;
}
@Getter
@Setter
@ToString
public class BossProductServiceDTO implements Serializable {
// ID
private Long id;
// 产品ID
private BossProductSmallDTO bossProductEntity;
// 服务ID
private BossServiceSmallDTO bossServiceEntity;
// 状态:0-下线,1-上线
private Integer status;
private Integer sequence;
}
@Data
public class BossProductSmallDTO implements Serializable {
// ID
private Long id;
// 名称
private String name;
// 类型,1-基础包产品 2-增值包产品 3-套餐产品
private Integer type;
}
这里将Entity B中的Entity A属性的类型,调整为 Entity Asmall,其中没有Entity B,从而中断了循环链。
场景记录
- 未配置CycleAvoid、未使用SmallDTO时,
- 若重写了hashCode,会报栈溢出。
- 若未重写,则在转换时,转换方法内调toDTO方法栈溢出
- 配置CycleAvoid,未使用SmallDTO时,
- 若重写了toString(),则在转换时会循环调用toDTO系列方法,但不会报栈溢出,在调toString()时,报栈溢出。
- 若未重写toString(),则在转换时不会出现循环依赖的问题,但在序列化时,依旧出现栈溢出,在序列化(类似于调toString())之前,一切正常。
- 若使用了SmallDTO,则可以解决循环依赖问题,是否使用CycleAvoid当前影响不大
@ManyToMany 适合于单方维护多对多关系的业务。维护方使用@JoinTable注解,被维护方使用mappedBy属性配置。被维护方无法更新关联表属性。一定不要双方都使用@JoinTable,否则在更新时,会错误的移除部分关联数据,切记!!!
可以将返回的实体封装成DTO,然后在DTO中不包含另一个的依赖
@OneToMany和@ManyToOne适用于双向维护多对多关系的业务。双方使用@OneToMany注解,在关联表上使用@ManyToOne注解。这种方式还适用于关联表有其他业务字段的场景。
这时可按下面的方式。不使用@Data注解。而是使用@Getter @Setter甚至是@ToString。
原因:
使用lombak的@Data,会自动生成hashCode()方法。就是这个方法导致了栈溢出。另建议重写toString,排除关联字段。
解决:
将@Data换成
@Setter
@Getter
具体代码github地址。主体更推荐使用@OneToMany和@ManyToOne的方式
其中main仓库针对@OneToMany和@ManyToOne的方式;master分支针对@ManyToMany的方式
https://github.com/lWoHvYe/spring-boot-jpa-cascade
主要是DTO和SmallDTO中的处理
一对多对多方排序及筛选
@OneToMany
@JoinColumn
//@JoinFormula("upper(substring(middle_name from 0 for 1))") call native SQL functions,可替代@JoinColumn
//@ColumnTransformer(read="decrypt(credit_card_num)", write="encrypt(?)") 对于rw的,还有@ColumnTransformer可以使用
// 排序 加到sql上,但后续Java层的处理可能导致前端收到的数据不是此顺序
@OneToMany
@JoinColumn
// 排序 加到sql上
@OrderBy(value = "sequence DESC, id ASC")
// 前端传的实体会按照此顺序排列,影响jpa级联更新的顺序
@OrderColumn("sequence DESC")
// 筛选 加到sql上,不太建议使用这个,因为及联更新时会略去 !=condition的,从而导致重复
@Where(clause = " status = 1 ")
// 注意类型用Set会重新排序。导入OrderBy无效。但级联更新正常
// 使用Set存取顺序不一致是因为使用的实现是HashSet,
private Set<UserRole> userRoles;
// 类型用List时,OrderBy正常。但级联更新有问题且返回的记录中有null。所以推荐使用Set
//private List<UserRole> userRoles;
实体A中的关联集合A-B.若在更新时,对集合进行了操作。则那些执行修改操作的关联记录不会交由jpa级联处理。
比如产品中有产品与服务的关联关系。若关联关系中不设置产品id,是可以正常保存的。jpa会先将关联中的产品id设置为null。然后执行update设置其值。但如果在保存方法中操作了关联集合。则jpa不会执行后面的update。此时,若产品id还不传,记录对应字段将为null(针对update的部分,insert的部分正常)。推测为实体的状态发生了变化
当关联属性字段 userRoles 是List时,在更新User时,userRoles中的每条记录userRole可以不包含id和user.id,这样会清空然后全部重建,但如果包含了id,则必须有user.id,不然该属性会被置null,但使用Set时,是没有这个问题的,所以各方面都推荐使用Set
todo
public enum CascadeType {
ALL,
PERSIST, // 持久化,新增支配方时,若带有关联关系,且被支配方已存在,会报detached entity passed to persist,若不存在会新增;更新支配方时,若关联的被支配方不存在,会insert,但insert的结果好像有问题。
MERGE, // 更新,新增支配方时,可带着关联关系(被支配方需存在);更新支配方时,除了关联关系,还可以对被支配方update (未传的被支配方字段 会被置空),且若被支配方不存在,会新增;
REMOVE, // 删除,删除支配方时,除了关联关系,还会将被支配方delete,这个慎用
REFRESH, // 刷新,获取最新的数据,因为在操作期间可能别的线程已改了数据并持久化了
DETACH; // 游离,删除支配方时,若其有与其相关的外键,会撤销所以相关的外键关联
private CascadeType() {
}
}
针对otm/mto 可以使用all。但针对mtm一定不能包含remove,推荐 merge,可加上refresh
2022-07-09更新
虽然上面已经讲过,这里还是重复一下,调整一些细节
- 针对@ManyToMany这种,可以在一方的对应属性上加上 @JsonIgnore,试图来打破序列化时的循环依赖问题
- 针对toString,实际上除了不重写toString方法或不使用@ToString,也可以重写但剔除另一个相关的属性
@JsonIgnore // 视情况使用 @JsonBackReference或 @JsonManagedReference
@ManyToMany(mappedBy = "bossServices")
private List<BossProductEntity> bossProducts;
// 剔除关联的属性
@Override
public String toString() {
return "BossServiceEntity(id=" + this.getId() + ", code=" + this.getCode() + ", name=" + this.getName() + ")";
}