使用JPA实现一对多关系的父子持久化

27

我想要保存一个包含20个子实体的父实体,以下是我的代码

父类

@OneToMany(mappedBy = "parentId")
private Collection<Child> childCollection;

子类

@JoinColumn(name = "parent_id", referencedColumnName = "parent_id")
@ManyToOne(optional=false)
private Parent parent;

String jsonString = "json string containing parent properties and child  collection" 

ObjectMapper mapper = new ObjectMapper();
Parent parent = mapper.readValue(jsonString, Parent.class);

public void save(Parent parent) {
    Collection<Child> childCollection = new ArrayList<>() ;

    for(Child tha : parent.getChildCollection()) { 
        tha.setParent(parent);
        childCollection.add(tha);
    }

    parent.setChildCollection(childCollection);
    getEntityManager().persist(parent);
 }

所以如果有20个子表,那么我必须在每个子表中设置父引用,这样我就必须编写20个for循环吗? 可行吗?是否有其他方法或配置,可以自动保留父级和子级?


这似乎更像是一个JSON问题,而不是JPA问题。如果您的JSON已被解组,以便设置适当的关系,则在保存父对象时使子对象持久化只是将相关的级联选项添加到@OneToMany注释中的简单问题(假设您的映射是正确的)。 - Alan Hay
3
如果您没有将子-父关系返回,或者在从JSON构建的内容中未设置它,那么是的,您需要在每个子实体中手动设置它。另一种选择是使关系单向:从OneToMany中删除mappedby="parent",并指定一个JoinColumn。这将使OneToMany在子表中设置外键,而不是由子项引用其父项来设置它(然后您应该删除Child实体的父属性和映射)。 - Chris
关于@Chris所说的,只是提一下推荐使用@JoinTable来关联单向@OneToMany。从文档中可以看到:在外键上建立单向一对多关联是不寻常的情况,也不推荐这样做。你应该使用一个连接表来进行这种类型的关联。 - Marko Bonaci
5个回答

16

修复您的父类:

@OneToMany(mappedBy = "parent")

mappedBy属性应该指向关系另一端的字段。正如JavaDoc所述:

  

拥有关系的字段。除非关系是单向的,否则必填。

此外,您还应明确地在循环中持久化子实体:

for(Child tha : parent.getChildCollection()) { 
    ...
    getEntityManager().persist(tha);
    ...
}

正如Alan Hay在评论中指出的那样,您可以使用级联功能,让EntityManager自动持久化所有子实体:

@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)

你可以在Vlad Mihalcea的博客中找到有关级联(以及JPA本身)的更多详细信息。


他@OneToMany也需要级联选项吗? - Alan Hay
但是对于20个不同的子集合,我需要编写20个不同的for循环吗? - Gora
@Bharath Reddy, 我认为是的,因为20个不同的子集合是20个不同的父对象,你需要调用方法save(...) 20次。 - Ιναη ßαbαηιη
1
我曾经使用了cascade = CascadeType.PERSIST,但是我得到的外键却是null。 - Gora
正如我之前所提到的,如果内存中的对象模型正确,那么JPA部分就是微不足道的。显然,内存模型的正确性取决于您的JSON结构,但您未能发布它,因此很难提供进一步的帮助。如果JSON不是反序列化程序可以从Child > Parent设置后向引用的格式,则需要在调用persist之前在代码中处理此问题(正如您所做的那样),否则您将获得空FK,就像您所看到的那样。如果您确实想要这样做,请发布JSON并询问有关JSON而不是JPA的问题。 - Alan Hay

13

一般情况下,@JoinColumn 表示该实体是关系的所有者,而 mappedBy 表示该实体是关系的反向关系

因此,如果您想尝试以下操作:

@OneToMany(mappedBy = "parent")
private Collection<Child> childCollection;

这意味着它是关系的反向,不会将父引用设置为其子代。

要将父引用设置为其子项,您必须以以下方式使上述实体成为关系的所有者

@OneToMany(cascade = CascadeType.ALL)
@JoinColumn
private Collection<Child> childCollection;

你不需要设置任何子引用,因为上面的代码将在子表中创建一列。


这并不是更好的方法,因为它被称为“单向@OneToMany”,然后Hibernate将在数据库中创建两倍的SQL请求。https://vladmihalcea.com/2017/03/29/the-best-way-to-map-a-onetomany-association-with-jpa-and-hibernate/ - panser
你的答案不正确。你可以参考这个链接:https://vladmihalcea.com/jpa-hibernate-synchronize-bidirectional-entity-associations/ - Ghasem Sadeghi

10

正如评论中指出的那样,您必须注意子/父关系的对象图一致性。当JSON直接来自例如POST请求时,这种一致性不会轻易得到。

您需要使用@JsonBackReference@JsonManagedReference对父字段和子字段进行注释。

父类:

@OneToMany(mappedBy = "parentId")
@JsonBackReference
private Collection<Child> childCollection;

子类:

@JoinColumn(name = "parent_id", referencedColumnName = "parent_id")
@ManyToOne(optional=false)
@JsonManagedReference
private Parent parent;

类似的问题和答案可以在这里找到。

另外,如果你在使用带有javax.persistence注解的类上结合使用@JsonBackReference/@JsonManagedReference和Lombok的@ToString注解,则会遇到堆栈溢出错误。

只需在@ToString( exclude = ...)中排除childCollectionparent字段,即可解决问题。

使用Lombok生成的equals()方法(@Data, @EqualsAndHashCode)也会发生相同的情况。您只需手动实现这些方法或仅使用@Getter@Setter注解。


1
这真是太有帮助了,但你把东西搞反了,应该在父类上添加JsonManagedReference,在子类上添加JsonBackReference。如果你有JsonIgnore的话,不要忘记删除它,因为它会在请求中给你带来“不支持的媒体类型”错误。 - Guilherme Alencar
@JsonBackReference注解不能用于子对象的集合上,而是应该使用相反的方式。在父类中的子属性列表上使用@OneToMany@JsonManagedReference,在指向父类的子实体类的属性上使用@ManyToOne@JsonBackReference - cvnew

2
我会让父级保存它自己的子项。
package com.greg;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;

@Entity(name = "PARENT")
public class Parent {

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

    @Column(name = "NAME")
    private String name;

    @Column(name = "DESCRIPTION")
    private String description;

    @OneToMany(cascade = CascadeType.ALL, fetch=FetchType.EAGER)
    @JoinColumn(name = "parent", referencedColumnName = "id", nullable = false)
    private List<Child> children = new ArrayList<Child>();

    public Long getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public List<Child> getChildren() {
        return children;
    }

    public void setChildren(List<Child> children) {
        this.children = children;
    }

}

1
在我们只想让父级也持久化子级的情况下,使用FetchType.EAGER是必要的吗? - Stephane
@Stephane 不,我只是为了测试的原因这样做了。 - Essex Boy

0
我正在使用Lombok在实体类中生成getter和setter属性。当我尝试保存具有子项的父实体时,我也遇到了子实体NULL引用ID的问题。在我的父实体上添加子项时,我将父项的“this”引用设置为子项。在我的示例中,我有用户表和地址表,其中一个用户可以有多个地址。
我已经创建了以下域类。例如:address.setUser(this);

package com.payment.dfr.entities;

import lombok.Data;

import javax.persistence.*;
import java.math.BigInteger;
@Entity
@Data
@Table(name="User")
public class User {

    @Id
    @GeneratedValue
    private BigInteger RecordId;
    private String Name;
    private String Email;

    @Getter(AccessLevel.NONE)
    @Setter(AccessLevel.NONE)
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<Address> addresses = new ArrayList<>();

    public void addAddress(Address address){
        address.setUser(this);
        addresses.add(address);
    }
    
}

@Entity
@Data
@Table(name="UserAddress")
public class Address {

    @Id
    @GeneratedValue
    private BigInteger RecordId;
    private String AddressLine;
    private String City;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name="UserId")
    private User user;

}


This is how I save user with address 

    User newUser = new User();
    newUser.setName("Papa");
    newUser.setEmail("manish@gmail.com");
    Address address1 = new Address();
    address1.setAddressLine("4401 Central Ave");
    address1.setCity("Fremont");
    newUser.addAddress(address1);
    Address address2 = new Address();
    address2.setAddressLine("4402 Central Ave");
    address2.setCity("Fremont");

    newUser.addAddress(address2);
    User user1 = userRepository.save(newUser);
    log.info(user1.getRecordId().toString());


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接