Aris F.
Aris F.

Reputation: 1117

JPA persist entities with one to many relation

Config

Short version of question

In a classic scenario, two tables with one to many relation. I create the parent entity, then the child entity and I attach the child to the parent's collection. When I create (controller method) the parent entity, I expect the child entity to be created to and associated with parent. Why doesn't it happen?

Long version

Parent class

@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
}

Child class

@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;
    }
}

Main class

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);
    }
}

This class throws a NullPointerException in line: device.getNetworkInterfaceCollection().add(net);

The system knows that there is a new entity device and it has an element net in it's collection. I expected it to write device in db, get device's id, attach it to net and write it in db.

Instead of this, I found that these are the steps I have to do:

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

Why do I have to create the child when the parent class knows it's child and it should create it for me?

The create method from DeviceJpaController (sorry for the long names in fields, they are auto generated).

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();
        }
    }
}

Upvotes: 13

Views: 49998

Answers (5)

Aris F.
Aris F.

Reputation: 1117

I finally understood the logic behind persisting one to many entities. The process is:

  1. Create parent class
  2. Persist it
  3. Create child class
  4. Associate child with parent
  5. Persist child (the parent collection is updated)

With code:

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     
    }
}

Now, I can find a device (with id for example) and I can get all its children using

Collection<NetworkInterface> netCollection = device.getNetworkInterfaceCollection()

In the device entity class, which I posted in the question, there is no need for the methods addNetworkInterface and removeNetwokrInterface.

Upvotes: 21

Ilan M
Ilan M

Reputation: 307

In order to enable save ability on a @OneToMany relation e.g.

@OneToMany(mappedBy="myTable", cascade=CascadeType.ALL) 
private List<item> items;

Then you have to tell to your @ManyToOne relation that it is allowed to update myTable like this updatable = true

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

Upvotes: -1

Sotirios Delimanolis
Sotirios Delimanolis

Reputation: 280168

@Dima K is correct in what they say. When you do this:

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

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

The collection in device hasn't been initialized and so you get a NPE when trying to add to it. In your Device class, when declaring your Collection, you can also initialize it:

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

As for persisting, your assumptions are correct but I think the execution is wrong. When you create your device, make it persistent with JPA right away (doing transaction management wherever needed).

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

Do the same for the NetworkInterface:

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

Now since both your entities are persisted, you can add one to the other.

device.getNetworkInterfaceCollection().add(net);

JPA should take care of the rest without you having to call any other persists.

Upvotes: 5

Dima K
Dima K

Reputation: 91

This exception means you're trying to locate an entity (probably by em.getReference()) that hasn't been persisted yet. You cannot you em.getReference() or em.find() on entities which still don't have a PK.

Upvotes: 0

Dima K
Dima K

Reputation: 91

This is a known behavior of collection data members. The easiest solution is to modify your collection getter to lazily create the collection.

@XmlTransient
public Collection<NetworkInterface> getNetworkInterfaceCollection() {
    if (networkInterfaceCollection == null) {
        networkInterfaceCollection = new Some_Collection_Type<NetworkInterface>();
    }
    return networkInterfaceCollection;
}

Also, remember to refer to this data member only through the getter method.

Upvotes: 2

Related Questions