Random LazyInitializationException with #OneToMany - java

I have a legacy database with the following Tables:
Police
id (PK)
data...
Contract
id(PK)
version(PK)
type
Code
tab(PK)
code(PK)
name
I have a jpa Entity Police
#Entity
public class Police implements Serializable {
#Id
private long id
#OneToMany(fetch = FetchType.LAZY)
#JoinColumns(value = { #JoinColumn(name = "id", referencedColumnName = "id") })
private Set<Contract> contracts;
}
the Contract entity looks like this:
#Entity
public class Contract implements Serializable {
#Id
private long id;
#Id
private long version;
private String type;
#OneToMany(fetch = FetchType.LAZY)
#JoinColumns({ #JoinColumn(name = "code", referencedColumnName = "type") })
#Where(clause = "tab = 'Type'")
private Set<Code> type;
}
Code:
#Entity
public class Code implements Serializable {
#Id
private String tab;
#Id
private String code;
private String name;
}
In the Code table are many different key/values for different applications.
In my case i need the one, where the "tab = 'Type'" and code = type from my Contract.
My Problem is, that if i have more than one Contract for my police, I RANDOMLY? get a org.hibernate.LazyInitializationException.
In my testcase, I do the following:
public static void main(String[] args) {
int countErrors = 0;
for (int i = 0; i < 15; i++) {
try {
readPolice();
} catch (Exception e) {
e.printStackTrace();
countErrors++;
}
}
System.err.println("errors: " + countErrors);
}
private static void readPolice() throws Exception {
EntityManagerFactory factory = EntityManagerFactoryHelper.getFactory(PersistenceUnitsEnum.TEST_STAGE);
EntityManager em = factory.createEntityManager();
TypedQuery<Police> namedQuery = em.createNamedQuery(...);
Police result = namedQuery.getSingleResult();
Set<Contract> contracts = result.getContract();
Contract contract = contracts.iterator().next();
Set<Code> type = contract.getType(); //should be a set with one Entry
System.out.println(type.size()); //<--- Chance for Exception!!
em.close();
}
I try this whole thing in a loop 15 times.
In about 5-8 tries, i get the LazyInitializationException.
The other times it works.
Any thoughts about this? Why doesn't it fail all the time?

Just encountered this one. The key word being 'random'. One of my colleagues encountered this exception consistently on her laptop while I never encountered the same. She could reproduce the exception only on IE & Edge.
Finally realized it had to do with Tomcat versions. She was running an older version, while I had a later version - 8.5.8. She upgraded the local Tomcat to this version and the exception is no more encountered.

Make sure the code with println is in the transaction.
LazyInitializationException means that you have loaded entity in some transaction, stepped out of it and then tried to use some lazy-loaded property of this entity.

since your whole code is not here, i assume your name query is not always returning the same entity and when the entity returned contains some Code it trows error.
your transaction must be closed cause otherwise u will not receive lazy initialyzing problem.

You should check that the creation of EntityManagerFactory is executed only once. For example:
public static void main(String[] args) {
EntityManagerFactory emf = EntityManagerFactoryHelper.getFactory(PersistenceUnitsEnum.TEST_STAGE);
for (int i = 0; i < 15; i++) {
readPolice(emf);
}
}
private static void readPolice(EntityManagerFactory emf) throws Exception {
EntityManager em = emf.createEntityManager();
...
}
If you're using some kind of singleton pattern in EntityManagerFactoryHelper.getFactory(), make sure it is thread safe.
You can also try to wrap readPolice() inside a transaction by calling em.getTransaction().begin() and em.getTransaction().commit(). For example:
private static void readPolice(EntityManagerFactory emf) throws Exception {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
...
em.getTransaction().commit();
em.close();
}

If I override the equals/hashcode in the Contract entity it works.
Why the problem occured randomly ... I don't get it ...

Related

I'm trying to understand LazyInitializationException and #Transactional

This is a follow-up question to my previous one How to model packages, versions and licenses?.
Here is my database setup.
V1__create_table_license.sql
CREATE TABLE IF NOT EXISTS license (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
reference TEXT NOT NULL,
is_deprecated_license_id BOOLEAN NOT NULL,
reference_number INTEGER NOT NULL,
license_id TEXT NOT NULL,
is_osi_approved BOOLEAN NOT NULL
);
INSERT INTO license
("name",reference,is_deprecated_license_id,reference_number,license_id,is_osi_approved)
VALUES
('MIT License','./MIT.json',false,275,'MIT',true);
V2__create_npm_package.sql
CREATE TABLE IF NOT EXISTS npm_package (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL
);
INSERT INTO npm_package
(name, description)
VALUES
('react', 'React is a JavaScript library for building user interfaces.'),
('react-router-dom', 'DOM bindings for React Router'),
('typescript', 'TypeScript is a language for application scale JavaScript development'),
('react-dom', 'React package for working with the DOM.');
V3__create_npm_version.sql
CREATE TABLE IF NOT EXISTS npm_package_version (
npm_package_id BIGINT NOT NULL REFERENCES npm_package,
version TEXT NOT NULL,
license_id INTEGER NOT NULL REFERENCES license,
UNIQUE(npm_package_id, version)
)
Here are my Java objects.
License.java
#Entity
public class License {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String reference;
private Boolean isDeprecatedLicenseId;
private Integer referenceNumber;
private String name;
private String licenseId;
private Boolean isOsiApproved;
}
LicenseRepository.java
public interface LicenseRepository extends JpaRepository<License, Integer> {
License findByLicenseIdIgnoreCase(String licenseId);
}
NpmPackage.java
#Entity
public class NpmPackage {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
#OneToMany(mappedBy = "npmPackage", cascade = CascadeType.ALL, orphanRemoval = true)
private List<NpmPackageVersion> versions = new ArrayList<>();
public NpmPackage() {}
public void addVersion(NpmPackageVersion version) {
this.versions.add(version);
version.setNpmPackage(this);
}
public void removeVersion(NpmPackageVersion version) {
this.versions.remove(version);
version.setNpmPackage(null);
}
}
#Entity
public class NpmPackageVersion {
public NpmPackageVersion() {}
public NpmPackageVersion(String version, License license) {
this.setVersion(version);
this.license = license;
}
#EmbeddedId private NpmPackageIdVersion npmPackageIdVersion = new NpmPackageIdVersion();
#MapsId("npmPackageId")
#ManyToOne(fetch = FetchType.LAZY)
private NpmPackage npmPackage;
#ManyToOne(fetch = FetchType.LAZY)
private License license;
#Embeddable
public static class NpmPackageIdVersion implements Serializable {
private static final long serialVersionUID = 3357194191099820556L;
private Long npmPackageId;
private String version;
// ...
}
public String getVersion() {
return this.npmPackageIdVersion.version;
}
public void setVersion(String version) {
this.npmPackageIdVersion.version = version;
}
}
MyRunner.java
#Component
class MyRunner implements CommandLineRunner {
#Autowired LicenseRepository licenseRepository;
#Autowired NpmPackageRepository npmPackageRepository;
#Override
// #Transactional
public void run(String... args) throws Exception {
// get license from database
var license = licenseRepository.findByLicenseIdIgnoreCase("mit");
// get package from db
var dbPackage = npmPackageRepository.findByNameIgnoreCase("react");
var version = new NpmPackageVersion("1.0.0", license);
dbPackage.addVersion(version);
npmPackageRepository.save(dbPackage);
}
}
In my previous question I got the answer to use fetch = FetchType.EAGER but then I learned that this is not ideal. I'd like to use lazy fetching.
#OneToMany(mappedBy = "npmPackage", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
private List<NpmPackageVersion> versions = new ArrayList<>();
So I removed the eager fetching and run into an error.
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.bom.NpmPackage.NpmPackage.versions, could not initialize proxy - no Session
With the #Transactional annotation everything works. Why is this the case? I tried to read everything online but I still don't really get it. I understand that the database session is closed at some point and I wonder where exactly this is the case. I also wonder if I could do something about, e.g. I tried to fetch all versions to ensure they are loaded before I add another one.
So do I really have to use #Transactional or is there another solution? I just want to understand the "magic" going on :)
Thank you very much!
When you use FetchType.LAZY, Hibernate ORM doesn't really return an initialized collection when you find the entity. The association is going to be a proxy and when you need access to the collection, Hibernate ORM is going to query the database and get it.
To achieve this, the entity (the NpmPackage) needs to be in a managed state. If the entity is not managed and you try to access a lazy association (versions in this case), you get the LazyInitializationException.
In your example, when you use #Transactional, the entity stays managed for the duration of the method. Without it, it becomes not managed as soon as you return from findByNameIgnoreCase.
If you know that you will need the association versions, you could also use a fetch join query to get the NpmPackage:
from NpmPackage p left join fetch p.versions where p.name=:name
This way the associations stays lazy but you can get it with a single query.

Why unidirectional #OneToMany update is not working

I am completely new to JPA and ORM concept, so I am hoping someone can lucidly explain what might be the problem with my code.
#Entity
#Table(name = "PERSISTENCE_customer")
public class Customer implements Serializable {
private static final long serialVersionUID = 1005220876458L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String firstName;
private String lastName;
#OneToMany (cascade = CascadeType.ALL, orphanRemoval = true)
private List<CustomerOrder> orders;
}
#Entity
#Table(name = "PERSISTENCE_ORDER")
public class CustomerOrder implements Serializable{
private static final long serialVersionUID = 199102142021L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#NotNull
String status;
#NotNull
#OneToMany (cascade = CascadeType.ALL, orphanRemoval = true)
private List<LineItem> lineItems = new ArrayList();
#NotNull
private String orderNumber;
................
................
}
#Entity
#Table(name = "PERSISTENCE_LINEITEM")
public class LineItem implements Serializable {
private static final long serialVersionUID = 1991217202100959L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#NotNull
private Integer quantity;
#NotNull
private Part part;
}
Initially, the Customer entity is created through the user interface and persisted successfully. Later, the customer has an order and I update the Customer with CustomerOrder as follow:
private void UpdateCustomer(Customer customer) {
FacesContext fc = FacesContext.getCurrentInstance();
List<ShoppingCartItem> shoppingCart = getShoppingCart();
CustomerOrder order = new CustomerOrder();
List<CustomerOrder> orders = customer.getOrders();
order.setLastUpdated(new Date());
order.setOrderNumber(getInvoiceNumber());
List<LineItem> lineItems = shoppingCart
.stream()
.map(e -> (new LineItem(e.getPart(), e.getQuantity())))
.collect(Collectors.toList());
order.setLineItems(lineItems);
order.setStatus("Pending Shipment");
order.setTotal(getTotal());
orders.add(order);
customer.setOrders(orders);
try {
updateOrders(customer, orders);
fc.addMessage(null,
new FacesMessage("Customer order added successfuly"));
} catch (ListServiceException e) {
FacesMessage errMsg = new FacesMessage(FacesMessage.SEVERITY_FATAL,
"Error while adding customer order: ", e.getMessage());
fc.addMessage(null, errMsg);
}
}
private void updateOrders(Customer cust, List<CustomerOrder> orders) throws ListServiceException {
try { //em is the EntityManager injected as the field member
if (em != null) {
if (em.isOpen()) {
Customer c = getCustomer(cust.getId());
c.setOrders(orders);
em.merge(c);
} else {
logger.severe("Entity manager is closed");
}
else {
logger.severe("Entity manager is NULL");
}
} catch (Exception e) {
throw ThrowListServiceException.wrapException(e);
}
}
Once the EntityManage merges I get the following exception. I was under the impression that I don't need to explicitly persist the LineItem and CustomerOrder entities myself. I thought that the JPA will persist all the entities in the object graph. Why am I getting this exception? (I am using GlassFish 5.1 server with EclipseLink JPA)
Thanks in advance.
Internal Exception: java.sql.SQLIntegrityConstraintViolationException: Column 'ORDERS_ID' cannot accept a NULL value.
Error Code: 30000
Call: INSERT INTO PERSISTENCE_CUSTOMER_PERSISTENCE_ORDER (orders_ID, Customer_ID) VALUES (?, ?)
bind => [2 parameters bound]
Query: DataModifyQuery(name="orders" sql="INSERT INTO PERSISTENCE_USER_PERSISTENCE_ORDER (orders_ID, User_ID) VALUES (?, ?)")
at org.eclipse.persistence.exceptions.DatabaseException.sqlException(DatabaseException.java:331)
at org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.executeDirectNoSelect(DatabaseAccessor.java:905)
...............................
.................................
Caused by: java.sql.SQLIntegrityConstraintViolationException: Column 'ORDERS_ID' cannot accept a NULL value.
Update
Using the IDE (Netbeans) debugger, I stepped through the code, and as I predicted, during the entity merge the JPA does not add new entities that are part of the object graph to the persistence context. For instance, in the updateOrders() method when I try to update the existing Customer object with a list of new CustomerOrder object, the JPA doesn't figure out that the elements in the List are not part of the persistence context yet and they need to be added. As a result, I had to modify my code to first add the List to the persistence context and then merge the Customer object with the newly persisted List. By doing so I no longer get the exception.
By the way, at moment, all the mapping relationships are unidirectional because I didn't see any reasons to use bidirectional mapping. However, would I gain anything by making these mappings bidirectionals?
Your OneToMany mapping is missing join specification or mappedBy value
I noticed that.
Firstly You should commit new order to database.Then you should link it with user.I'm not sure if this solves your problem but this is a problem.Can you check it ?
In my opinion, if you keep customer information in your Order entity, it may solve this problem.
#ManyToOne ()
private Customer customer;
And in your Customer entity you should put mappedBy=customer for orders field.
After that, instead of giving orders for customer, you can give customer for a specific order. In my opinion it will achieve a better relationship mapping;
order.setCustomer(customer);
I hope i understood it right and this will solve your problem. When you give customer detail for order, you dont need to give orderlist detail for the same customer. Only one of them should be enough.

LazyInitializationException Spring Boot

I know there are a lot of similar threads out there but i just can't figure it out from those threads on how to overcome this problem.
I have 3 classes Car, Brand, Color.
A Car has just one Brand and a list of Colors.
Brand has a List of Cars.
Color does not have any relation.
Getters, Setters, ToString and Constructors are not provided for simplicity sake.
I'm able to save objects into database and database is already populated.
--------------------------------------------------------------------------------
#Entity
#Table(catalog = "spring_project")
public class Car {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String model;
#ManyToMany(cascade=CascadeType.ALL, fetch = FetchType.LAZY)
#JoinTable( name = "car_color", catalog = "spring_project",
joinColumns = { #JoinColumn(name = "car_id") },
inverseJoinColumns = { #JoinColumn(name = "colors_id") }
)
private List<Color> colors = new ArrayList<>();
#ManyToOne(cascade=CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name="brand_id", referencedColumnName="id")
private Brand brand;
--------------------------------------------------------------------------------
#Entity
#Table(catalog = "spring_project")
public class Brand {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#OneToMany(mappedBy = "brand", fetch = FetchType.LAZY)
private List<Car> cars = new ArrayList<>();
--------------------------------------------------------------------------------
#Entity
#Table(catalog = "spring_project")
public class Color {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
--------------------------------------------------------------------------------
Everything runs just fine if i fetch like Eager, but i know it is a bad practice and it should be used Lazy loading instead. But i keep getting the LazyInitializationException.
I understand from the error that a session is required but i dont know how to provide one since im working with Spring Data JPA neither where i should declare one...
#SpringBootApplication
public class SrpingJpaApplication {
private static final Logger log =
LoggerFactory.getLogger(SrpingJpaApplication.class);
public static void main(String[] args) {
SpringApplication.run(SrpingJpaApplication.class, args);
}
#Bean
public CommandLineRunner demo(CarRepository carRepository,
ColorRepository colorRepository,
BrandRepository brandRepository) {
return (args) -> {
log.info("Reads all cars....");
for (Car c : carRepository.findAll()) {
System.out.println(c.toString());
}
};
}
}
Thank you so much.
Edited----->>>
The error is thrown on c.toString();
Error: Caused by: org.hibernate.LazyInitializationException: could not initialize
proxy [com.readiness.moita.SrpingJPA.Models.Brand#1] - no Session
The default for the #OneToMany annotation is FetchType.LAZY so your collections are loaded lazily.
In order to be able to access the collection after you've retrieved the object you need to be in a transactional context (you need an open session)
When you call:
carRepository.findAll();
internally a new session is created, the object is retrieved and as soon as the findAll method returns the session is closed.
What you should do is make sure you have an open session whenever you access the lazy collection in your Car object (which the toString does).
The simplest way is to have another service handle the car loading and annotate the showCars method with #Transactional the method is in another service because of the way AOP proxies are handled.
#Service
public CarService {
final CarRepository carRepository;
public CarService(CarRepository carRepository) {
this.carRepository = carRepository;
}
#Transactional
public void showCars(String... args) {
for (Car c : carRepository.findAll()) {
System.out.println(c.toString());
}
}
}
and then you call:
#Bean
public CommandLineRunner demo(CarService carService) {
return (args) -> service.showCars(args);
}
Because the FetchType of Brand is lazy, it will not automatically be loaded into the session with call to fetchAll(). To have it automatically load into the session, you need to:
Change
#ManyToOne(cascade=CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name="brand_id", referencedColumnName="id")
private Brand brand;
to
#ManyToOne(cascade=CascadeType.ALL, fetch = FetchType.EAGER)
Ex
#ManyToOne(cascade=CascadeType.ALL, fetch = FetchType.EAGER)
#JoinColumn(name="brand_id", referencedColumnName="id")
private Brand brand;
If you do not want to set the fetch type to eager, then you need to move your call to toString to a service method Ex
#Component
public CarService implements ICarService {
#Autowired
CarRepository carRepository;
#Transactional
public void printAllCars() {
for (Car c : carRepository.findAll()) {
System.out.println(c.toString());
}
}
}
The correct way to do this however would be to write a criteria query or hql

Failed to lazily initialize a collection of role during conversion of object to json

I am new to spring and while fetching records from a table having relationship with other tables getting this lazily initialling error.
I have read a lot online but not getting a appropriate approach.
Table1:
#SuppressWarnings("serial")
#Entity
public class Terminal extends BaseEntity {
#Column(length = 100, unique = true)
private String shortName;
#Column
private short number; // short stores up to 32767 value
#Column
private String description;
#OneToMany
#JoinColumn(name = "terminal_id", referencedColumnName = "uuid")
#Cascade({ CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DELETE })
private Set<BusinessHour> businessHour;
public String getShortName() {
return shortName;
}
public void setShortName(String shortName) {
this.shortName = shortName;
}
public short getNumber() {
return number;
}
public void setNumber(short number) {
this.number = number;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Set<BusinessHour> getBusinessHour() {
return businessHour;
}
public void setBusinessHour(Set<BusinessHour> businessHour) {
this.businessHour = businessHour;
}
Table2:
#SuppressWarnings("serial")
#Entity
public class BusinessHour extends BaseEntity {
#Column
private DayOfWeek dayOfWeek;
#Column
private LocalTime startOfOperation;
#Column
private LocalTime endOfOperation;
public DayOfWeek getDayOfWeek() {
return dayOfWeek;
}
}
Service Code:
#Service
public class TerminalServiceImpl implements TerminalService {
#Autowired
TerminalRepository terminalRepository;
Iterable<Terminal> allTerminals = terminalRepository.findAll();
List<Terminal> terminalList = new ArrayList<Terminal>();
for (Terminal terminal : allTerminals) {
terminalList.add(terminal);
}
return terminalList;
}
Terminal Repository code:
#Transactional
public interface TerminalRepository extends CrudRepository<Terminal, Long> {
}
Code where i got error during debug:
private List<Terminal> updateTerminalList() {
List<Terminal> allTerminals = terminalService.fetchAllTerminal();
return allTerminals;
}
public void terminalWrapperRun() {
try {
Payload payload = createTerminalPayload(applicationId);
String json3 = object2Json(payload);
kafkaRESTUtils.sendServerPayload(json3);
} catch (Exception e1) {
e1.printStackTrace();
}
}
public String object2Json(Object dataArray) throws JsonProcessingException {
return mapper.writeValueAsString(dataArray);
}
Error:
com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection of role: terminal.model.Terminal.businessHour, could not initialize proxy - no Session (through reference chain:
Getting exception while converting fetching object to json. which i found due to proxy object return due to fetch type lazy(which i want to kept as it is).
I believe this issue relates to the LAZY loading of Collections by default by your ORM.
#OneToMany
#JoinColumn(name = "terminal_id", referencedColumnName = "uuid")
#Cascade({ CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DELETE })
private Set<BusinessHour> businessHour;
The #OneToMany annotation has a fetch property which is set to LAZY by default.
FetchType fetch() default LAZY;
OneToMany reference
This means that it will only be loaded when the data is accessed. In the case of your example, this will happen when you try to create the JSON string. By this point, however, you are outside the scope of the ORM session so it does not know how to load the data.
Therefore you have 2 options.
Change your annotation to eagerly load the data (which means the BusinessHour Set will be loaded at the same time as the parent object
#OneToMany(fetch = FetchType.EAGER)
perform your JSON generation within an ORM session (I would only really recommend doing this is the first option causes performance issues).
If I recall correctly this is the kind of error caused by an Entity being detached from the EntityManager at the time of its use (being it a Proxy it cannot perform a database query to retrive the data).
You can use:
#OneToMany(fetch = FetchType.EAGER)
...
private Set<BusinessHour> businessHour;
Using FetchType=EAGER means that any query against your entity will load the whole bunch of annotated entities.
Imho, this is only a sensible action if you are 100% sure that your entity will only be used for your special business case.
In all other cases - like programming a data acess as a library, or accepting different kinds of queries on your entities, you should use entity graphs (https://docs.oracle.com/javaee/7/tutorial/persistence-entitygraphs002.htm) or explicit loading (Hibernate.initialize, join-fetch, see for example https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/).
If your use case only is the transformation, you have two good options:
Transform your entity to JSON within a Transactional method (as PillHead suggested)
Load your entity explicitly with all the entities needed (via entity graphs or Hibernate.initialize) within the transaction, and then convert to JSON where you need it.

JPA persist entities with one to many relation

Config
EcliplseLink 2.3.2
JPA 2.0
The entities are auto created from the db schema from netbeans with Entity Classes from Database... wizard.
The controller classes are auto created from netbeans with JPA Controller Classes from Entity Classes... wizard
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();
}
}
}
I finally understood the logic behind persisting one to many entities. The process is:
Create parent class
Persist it
Create child class
Associate child with parent
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.
#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.
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.
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.
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)

Categories