I have a customer and a customer info entity in my spring boot project. They have an one to many relationship.
@Data
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "customer")
public class Customer implements Serializable{
@Serial
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long serialNumber;
private Long customerId;
private String name;
@Column(name = "session_id", length = 128)
private String sessionId;
@JsonManagedReference("customer-customer_info")
@OneToMany(targetEntity = CustomerInfo.class, mappedBy="Customer", cascade = CascadeType.ALL)
private List<CustomerInfo> customerInfoList;
}
@Data
@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "customer_info")
public class CustomerInfo implements Serializable{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long CustomerInfoId;
@ManyToOne
@JsonBackReference("customer-customer_info")
@ToString.Exclude
@JoinColumn(name="customer_session_id", nullable=false, referencedColumnName = "session_id")
private Customer customer;
private String metaKey;
@Convert(converter = Encryptor.class)
private String metaValue;
}
Whenever I try to fetch customerInfo with the help of a getter function(customer.getCustomerInfo()) using customer entity. The above exception is thrown. I have read in many places that the error is caused because the JPA session gets closed and does not persist the child entity. However, I have not got a solution out of any of the other stack overflow answers.
Solutions which have worked partially, however are not desirable:
- I change fetchType of customer_info to EAGER
- Ran a seperate query in the customer_info table.
The problem sadly does not replicate itself in my local environment. I do not understand why? (I understood why I could not replicate the problem, I saw @RestController methods seem to be Transactional by default, Why? and added spring.jpa.open-in-view=false in my application.properties file)
Solutions which were suggested in Stack overflow and I have tried,
- I have tried out the @Transactional fix over the service class's method calling the customer entity is called. However, the customer entity is then used in some other function down the line.
List<CustomerInfo> customerInfoList = customer.getCustomerInfoList();
[This line does not throw the error, I had event printed the className of customerInfoList, it was persistentBag. The below line throws the error.]Hibernate.initialise(customerList)
Spring boot version : 2.6.2 Java : 17
There are two services with the issue, ConnectionServiceImpl and CompletionServiceImpl. Service classes where the issue happens :
ConnectionServiceImpl ->
@Service(“ConnectionServiceImpl”)
public class ConnectionServiceImpl implements ConnectionService {
@Autowired
private CompletionService completionService;
@Override
public boolean test(){
Request request = completionService.recordData(“session-id”);
try {
System.out.println(request.toString()); //this is working fine
String str = GsonSerializer.getInstance().toJson(request); //this is throwing the exception
System.out.println(str);
}
catch(Exception ex) {
System.out.println(ex);
//org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.test.entity.Customer.customerInfoList, could not initialize proxy - no Session
}
return true;
}
}
CompletionServiceImpl->
@Service(“CompletionServiceImpl”)
public class CompletionServiceImpl implements CompletionService {
@Autowired
private CustomerInfoRepository customerInfoRepository;
@Override
public Request recordData(String session){
return prepareData(session);
}
private Request prepareData(String session){
Request request = new Request();
fillData(request, session);
return request;
}
private void fillData(Request request, String session){
Customer customer = customerRepository.findBySessionId(sessionId);
request.setCustomerId(customer.getCustomerId());
request.setData(parseResponse(customer, request));
return ;
}
private List<CustomerInfo> parseResponse(Customer customer, Request request){
List<CustomerInfo> customerInfoList = customerInfoRepository.findBySessionId(customer.getSessionId());
CustomerInfo customerInfo = customerInfoList.stream()
.filter(info -> info.getMetaKey().equalsIgnoreCase(KEY))
.findFirst()
.orElse(null);
request.setKey(customerInfo.getMetaValue());
return customerInfoList;
}
}
When I run the code in debug mode, I see this error at the breakpoint, Method threw 'org.hibernate.LazyInitializationException' exception. Cannot evaluate com.test.entity.Customer.toString()
The request class Request, serialising which is causing the whole problem is :
@Data
public class Request implements Serializable {
private Long customerId;
private List<?> data;
private String key;
}
The only solution I have found which seems to be an expensive operation is converting the data customerInfoList into InfoDao by BeanUtils, so that all the fields of CustomerInfoList is copied to InfoDao(without the Customer field). Please suggest a better solution.
private List<InfoDao> convertToArray(List<?> list){
List<InfoDao> list1 = new ArrayList<>();
for(Object v:list){
InfoDao infoDao = new InfoDao();
BeanUtils.copyProperties(v,infoDao);
list1.add(infoDao);
}
return list1;
}
InfoDao has the same schema as CustomerInfo but without Customer reference with it.
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class InfoDao implements Serializable{
private Long CustomerInfoId;
private String metaKey;
private String metaValue;
}