You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在JPA Hibernate中防止同表父子关系出现循环引用

防止JPA/Hibernate中Ingredient实体的循环引用

嗨,作为JPA和Hibernate新手,你遇到的循环引用问题其实是典型的有向图环检测场景——我们需要确保一个Ingredient不会成为自身的直接子成分,更不能通过多层级的子成分间接回到自己。结合你的实体结构,这里给你提供几个实用的解决方案:

问题分析

你的实体设计已经很合理:用SubIngredient作为关联实体,通过嵌入式主键实现Ingredient自身的父子关系。现在核心要解决的是,在添加或修改子成分时,阻断任何可能形成循环的路径。

解决方案:业务层环检测

JPA本身没有内置这种递归关系的约束,所以我们需要在业务逻辑中手动检测循环。这里提供两种实现思路:

1. 在服务层统一处理检测

创建一个服务类,在添加子成分前递归遍历候选子成分的整个子树,检查是否包含父成分:

@Service
public class IngredientService {

    @Autowired
    private IngredientRepository ingredientRepository;

    public void addSubIngredient(Long parentId, Long subIngredientId, double quantity) {
        Ingredient parent = ingredientRepository.findById(parentId)
                .orElseThrow(() -> new RuntimeException("父成分不存在"));
        Ingredient subIngredient = ingredientRepository.findById(subIngredientId)
                .orElseThrow(() -> new RuntimeException("子成分不存在"));

        // 检测是否存在循环引用
        if (detectCycle(parent, subIngredient)) {
            throw new IllegalArgumentException("添加该子成分会导致循环引用,操作被禁止");
        }

        SubIngredient subIng = new SubIngredient();
        subIng.setIngredient(parent);
        subIng.setSubIngredient(subIngredient);
        subIng.setQuantity(quantity);

        parent.addSubIngredient(subIng);
        ingredientRepository.save(parent);
    }

    /**
     * 递归检测是否存在从current到target的环
     */
    private boolean detectCycle(Ingredient target, Ingredient current) {
        // 如果当前成分就是目标,说明存在环
        if (target.getId() == current.getId()) {
            return true;
        }
        // 遍历当前成分的所有子成分,递归检测
        for (SubIngredient sub : current.getSubIngredients()) {
            if (detectCycle(target, sub.getSubIngredient())) {
                return true;
            }
        }
        return false;
    }
}

2. 在实体类内聚检测逻辑

你也可以把检测逻辑封装到Ingredient实体的addSubIngredient方法中,让实体自己维护数据完整性:

修改Ingredient类的addSubIngredient方法:

public void addSubIngredient(SubIngredient subIngredient) {
    Ingredient candidateSub = subIngredient.getSubIngredient();
    // 检测循环
    if (hasCycle(this, candidateSub)) {
        throw new IllegalArgumentException("不允许添加导致循环引用的子成分");
    }
    this.subIngredients.add(subIngredient);
}

/**
 * 递归检查是否存在循环
 */
private boolean hasCycle(Ingredient target, Ingredient current) {
    if (target.getId() == current.getId()) {
        return true;
    }
    for (SubIngredient sub : current.getSubIngredients()) {
        if (hasCycle(target, sub.getSubIngredient())) {
            return true;
        }
    }
    return false;
}

注意事项

  • 如果你把subIngredients的加载方式从EAGER改成LAZY,需要确保检测环时Hibernate Session处于打开状态,或者提前通过JOIN FETCH加载所有子成分,否则会抛出懒加载异常。
  • 不仅添加子成分时要检测,修改SubIngredientsubIngredient属性时也要执行同样的逻辑,避免后续修改产生循环。
  • 若处理极深的层级,递归可能导致栈溢出,这时可以改用迭代方式(比如用队列/栈遍历子成分树)。

你的原始实体代码参考

SubIngredientKey 嵌入式主键类

@Embeddable
public class SubIngredientKey implements Serializable {
    private Long ingredientId;
    private Long subIngredientId;

    @Override
    public int hashCode() {
        return Objects.hash(ingredientId, subIngredientId);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof SubIngredientKey)) {
            return false;
        }
        SubIngredientKey other = (SubIngredientKey) obj;
        return Objects.equals(ingredientId, other.ingredientId) && Objects.equals(subIngredientId, other.subIngredientId);
    }

    // Getters and Setters
    public Long getIngredientId() {
        return ingredientId;
    }

    public void setIngredientId(Long ingredientId) {
        this.ingredientId = ingredientId;
    }

    public Long getSubIngredientId() {
        return subIngredientId;
    }

    public void setSubIngredientId(Long subIngredientId) {
        this.subIngredientId = subIngredientId;
    }
}

SubIngredient 关联实体类

@Entity
public class SubIngredient {
    @EmbeddedId
    private SubIngredientKey embId = new SubIngredientKey();

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("ingredientId")
    private Ingredient ingredient;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("subIngredientId")
    private Ingredient subIngredient;

    private double quantity;

    @JsonIgnore
    public SubIngredientKey getId() {
        return embId;
    }

    public void setId(SubIngredientKey id) {
        this.embId = id;
    }

    @JsonIgnoreProperties({"subIngredients","photo","photoContentType","ingredientType"})
    public Ingredient getIngredient() {
        return ingredient;
    }

    public void setIngredient(Ingredient ingredient) {
        this.ingredient = ingredient;
    }

    @JsonIgnoreProperties({"subIngredients","photo","photoContentType","ingredientType"})
    public Ingredient getSubIngredient() {
        return subIngredient;
    }

    public void setSubIngredient(Ingredient subIngredient) {
        this.subIngredient = subIngredient;
    }

    public double getQuantity() {
        return quantity;
    }

    public void setQuantity(double quantity) {
        this.quantity = quantity;
    }

    @Override
    public String toString() {
        return "subIngredient= " + getSubIngredient().getName() + " , quantity= " + getQuantity();
    }

    @Override
    public int hashCode() {
        return Objects.hash(ingredient,subIngredient);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof SubIngredient)) {
            return false;
        }
        SubIngredient other = (SubIngredient) obj;
        return Objects.equals(ingredient, other.ingredient) && Objects.equals(subIngredient, other.subIngredient);
    }
}

Ingredient 主实体类

@Entity
public class Ingredient {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="ID")
    private long id;

    @NotNull
    @Column(unique=true)
    private String name;

    private String photoContentType;

    @Lob
    private byte[] photo;

    @JsonIgnoreProperties({"photoContentType","photo"})
    @ManyToOne
    private IngredientType ingredientType;

    @OneToMany(mappedBy = "embId.ingredientId", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<SubIngredient> subIngredients = new HashSet<SubIngredient>();

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPhotoContentType() {
        return photoContentType;
    }

    public void setPhotoContentType(String photoContentType) {
        this.photoContentType = photoContentType;
    }

    public byte[] getPhoto() {
        return photo;
    }

    public void setPhoto(byte[] photo) {
        this.photo = photo;
    }

    public IngredientType getIngredientType() {
        return this.ingredientType;
    }

    public void setIngredientType(IngredientType ingredientType) {
        this.ingredientType = ingredientType;
    }

    public Set<SubIngredient> getSubIngredients() {
        return subIngredients;
    }

    public void setSubIngredients(Set<SubIngredient> subIngredients) {
        this.subIngredients = subIngredients;
    }

    public void addSubIngredient(SubIngredient subIngredient) {
        this.subIngredients.add(subIngredient);
    }

    @Override
    public String toString() {
        String subIngsText = "";
        for(var subIngredient:this.subIngredients) {
            subIngsText = subIngsText + ", " + subIngredient.toString();
        }
        return "{id= "+id+",name=" + name +", ingredients="+subIngsText+"}";
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Ingredient)) {
            return false;
        }
        Ingredient other = (Ingredient) obj;
        return Objects.equals(name, other.name);
    }
}

内容的提问来源于stack exchange,提问作者Imad Che

火山引擎 最新活动