Reputation: 1117
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
Reputation: 1117
I finally understood the logic behind persisting one to many entities. The process is:
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
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
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
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
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