JPA使用一对多关系持久化实体

13

配置

  • EclipseLink 2.3.2
  • JPA 2.0
  • 实体是使用NetBeans的“从数据库创建实体类...”向导自动创建的,从数据库模式中。
  • 控制器类是使用NetBeans的“从实体类创建JPA控制器类...”向导自动生成的。

问题简述

在一个经典的情况下,有两个具有一对多关系的表。我先创建父实体,然后再创建子实体,并将子实体附加到父实体的集合中。当我 创建(控制器方法)父实体时,我希望子实体也会被创建并与父实体相关联。为什么不会发生这种情况?

详细描述

父类

@Entity
@XmlRootElement
public class Device implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Basic(optional = false)
    private Integer id;
    @Column(unique=true)
    private String name;
    @Temporal(TemporalType.TIMESTAMP)
    private Date updated;
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "deviceId")
    private Collection<NetworkInterface> networkInterfaceCollection;

    public Device() {
    }

    public Device(String name) {
        this.name = name;
        updated = new Date();
    }

    // setters and getters...

    @XmlTransient
    public Collection<NetworkInterface> getNetworkInterfaceCollection() {
        return networkInterfaceCollection;
    }

    public void setNetworkInterfaceCollection(Collection<NetworkInterface> networkInterfaceCollection) {
        this.networkInterfaceCollection = networkInterfaceCollection;
    }

    public void addNetworkInterface(NetworkInterface net) {
        this.networkInterfaceCollection.add(net);
    }

    public void removeNetworkInterface(NetworkInterface net) {
        this.networkInterfaceCollection.remove(net);
    }
    // other methods
}

子类

@Entity
@Table(name = "NETWORK_INTERFACE")
@XmlRootElement
public class NetworkInterface implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Basic(optional = false)
    private Integer id;
    private String name;
    @Temporal(TemporalType.TIMESTAMP)
    private Date updated;
    @JoinColumn(name = "DEVICE_ID", referencedColumnName = "ID")
    @ManyToOne(optional = false)
    private Device deviceId;

    public NetworkInterface() {
    }

    public NetworkInterface(String name) {
        this.name = name;
        this.updated = new Date();
    }

    // setter and getter methods...

    public Device getDeviceId() {
        return deviceId;
    }

    public void setDeviceId(Device deviceId) {
        this.deviceId = deviceId;
    }
}

主类

public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("wifi-dbPU");
        DeviceJpaController deviceController = new DeviceJpaController(emf);
        NetworkInterfaceJpaController netController = new NetworkInterfaceJpaController(emf);

        Device device = new Device("laptop");
        NetworkInterface net = new NetworkInterface("eth0");

        device.getNetworkInterfaceCollection().add(net);
        deviceController.create(device);
    }
}

这个类在第device.getNetworkInterfaceCollection().add(net);行会抛出NullPointerException异常。

系统知道有一个新的实体device,并且它有一个元素net在它的集合中。我期望它将device写入数据库,获取设备ID,将其附加到net并将其写入数据库。

但事实上,我发现我必须执行以下步骤:

deviceController.create(device);
net.setDeviceId(device);
device.getNetworkInterfaceCollection().add(net);
netController.create(net);

当父类知道它的子类并应该为我创建它时,为什么我需要创建子类?

来自DeviceJpaController的create方法(对于字段中的长名称很抱歉,它们是自动生成的)。

public EntityManager getEntityManager() {
    return emf.createEntityManager();
}

public void create(Device device) {
    if (device.getNetworkInterfaceCollection() == null) {
        device.setNetworkInterfaceCollection(new ArrayList<NetworkInterface>());
    }
    EntityManager em = null;
    try {
        em = getEntityManager();
        em.getTransaction().begin();
        Collection<NetworkInterface> attachedNetworkInterfaceCollection = new ArrayList<NetworkInterface>();
        for (NetworkInterface networkInterfaceCollectionNetworkInterfaceToAttach : device.getNetworkInterfaceCollection()) {
            networkInterfaceCollectionNetworkInterfaceToAttach = em.getReference(networkInterfaceCollectionNetworkInterfaceToAttach.getClass(), networkInterfaceCollectionNetworkInterfaceToAttach.getId());
            attachedNetworkInterfaceCollection.add(networkInterfaceCollectionNetworkInterfaceToAttach);
        }
        device.setNetworkInterfaceCollection(attachedNetworkInterfaceCollection);
        em.persist(device);
        for (NetworkInterface networkInterfaceCollectionNetworkInterface : device.getNetworkInterfaceCollection()) {
            Device oldDeviceIdOfNetworkInterfaceCollectionNetworkInterface = networkInterfaceCollectionNetworkInterface.getDeviceId();
            networkInterfaceCollectionNetworkInterface.setDeviceId(device);
            networkInterfaceCollectionNetworkInterface = em.merge(networkInterfaceCollectionNetworkInterface);
            if (oldDeviceIdOfNetworkInterfaceCollectionNetworkInterface != null) {
                oldDeviceIdOfNetworkInterfaceCollectionNetworkInterface.getNetworkInterfaceCollection().remove(networkInterfaceCollectionNetworkInterface);
                oldDeviceIdOfNetworkInterfaceCollectionNetworkInterface = em.merge(oldDeviceIdOfNetworkInterfaceCollectionNetworkInterface);
            }
        }
        em.getTransaction().commit();
    } finally {
        if (em != null) {
            em.close();
        }
    }
}
5个回答

21

我终于理解了持久化一对多实体的逻辑。过程如下:

  1. 创建父类
  2. 持久化它
  3. 创建子类
  4. 将子类与父类关联
  5. 持久化子类(父类集合会更新)

代码如下:

public class Main {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("wifi-dbPU");
        DeviceJpaController deviceController = new DeviceJpaController(emf);
        NetworkInterfaceJpaController netController = new NetworkInterfaceJpaController(emf);

        Device device = new Device("laptop");                 // 1
        deviceController.create(device);                      // 2

        NetworkInterface net = new NetworkInterface("eth0");  // 3
        net.setDeviceId(device.getId());                      // 4
        netController.create(net);                            // 5 
        // The parent collection is updated by the above create     
    }
}

现在,我可以通过设备的id找到它,并使用下面的代码获取其所有子元素:
Collection<NetworkInterface> netCollection = device.getNetworkInterfaceCollection()

在我在问题中发布的设备实体类中,不需要addNetworkInterfaceremoveNetwokrInterface这些方法。

1
根据我从书中读到的内容,cascade=CascadeType.Persist 应该会持久化所有关联实体,你只需要更新一个实体,JPA 就会遍历关联实体并更新它们。但是... 我无法让它正常工作... - Weishi Z

5

@Dima K说得没错。当你这样做时:

    Device device = new Device("laptop");
    NetworkInterface net = new NetworkInterface("eth0");

    device.getNetworkInterfaceCollection().add(net);
    deviceController.create(device);

设备中的集合尚未初始化,因此在尝试添加到其中时会出现NPE错误。在您的Device类中,在声明Collection时,您还可以对其进行初始化:

private Collection<NetworkInterface> networkInterfaceCollection = new CollectionType<>();

关于持久化,你的假设是正确的,但我认为执行方式有误。在创建设备时,请立即使用JPA使其持久化(在必要时进行事务管理)。

Device device = new Device("laptop");
getEntityManager().persist(device);

同样的,对于NetworkInterface也要进行相同的操作:

NetworkInterface net = new NetworkInterface("eth0");
getEntityManager().persist(net);

现在,由于您的两个实体都已经持久化,您可以将一个实体添加到另一个实体中。

device.getNetworkInterfaceCollection().add(net);

JPA应该会自动处理其余的操作,而无需调用任何其他的持久化方法。


1
你们对于集合初始化都是正确的。但这是我的问题。为什么我需要持久化两个实体,当我只需要创建集合呢?JPA不应该持久化集合中的所有实体并将它们与父类关联吗? - Aris F.
JPA 知道实体及其所处的状态。因此,创建设备实体和网络接口实体就是这样。JPA 将知道两个实体。告诉 JPA 两者之间的关系是您的责任。我理解您的意思是,如果您在持久化设备并在其集合中添加元素后,JPA 应该也会持久化这些元素。如果在 persist 设备时集合已经被填充(反之亦然),那么设备及其集合元素将被持久化。 - Sotirios Delimanolis

2
这是集合数据成员的已知行为。 最简单的解决方案是修改你的集合getter以惰性创建集合。
@XmlTransient
public Collection<NetworkInterface> getNetworkInterfaceCollection() {
    if (networkInterfaceCollection == null) {
        networkInterfaceCollection = new Some_Collection_Type<NetworkInterface>();
    }
    return networkInterfaceCollection;
}

同时,请记得只通过getter方法引用此数据成员。


这是在 DeviceJpaController 的 create 方法中完成的。请查看开头的 if 语句。如果集合为 null,则会创建一个新的集合并使用 device.setNetworkInterfaceCollection 方法进行设置。我尝试了你的建议,但出现了以下错误:IllegalArgumentException: An instance of a null PK has been incorrectly provided for this find operation. - Aris F.

0

这个异常意味着你正在尝试定位一个实体(可能是通过em.getReference())但它还没有被持久化。 你不能在仍然没有主键的实体上使用em.getReference()或em.find()。


你是对的。它试图在em.getReference()调用中定位网络的Id。该id为空,因为实体尚未持久化。请参见@Sotirios Delimanolis帖子中的评论。 - Aris F.

-1
为了在@OneToMany关系上启用保存功能,例如:
@OneToMany(mappedBy="myTable", cascade=CascadeType.ALL) 
private List<item> items;

然后你需要告诉你的@ManyToOne关系,允许像这样更新myTable,updatable = true

@ManyToOne @JoinColumn(name="fk_myTable", nullable = false, updatable = true, insertable = true)

“updatable = true,insertable = true” 本来就是默认值,因此这并不能解决问题。 - Tim Büthe

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