3

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:

  1. I change fetchType of customer_info to EAGER
  2. 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,

  1. 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.
  2. 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;
}
Abhinav Prasun
  • 63
  • 1
  • 1
  • 6
  • In addition to the entities, could you add all the code that leads to your error? As you say, the error comes from the session being closed. If you use `@Transactional`, make sure that the method is called from another class. If you call a `@Transactional` method from within the same class, it doesn't work. – tbjorch May 10 '22 at 06:39
  • Please show your Code not just your entities. – GJohannes May 10 '22 at 07:23
  • I have attached the service class code. LazyInitialisationException is thrown when Gson serialiser is called to serialise an object. – Abhinav Prasun May 11 '22 at 05:23
  • `Hibernate.initialize(customer);` inside `private void fillData(Request request, String session)` should work fine – XtremeBaumer May 12 '22 at 07:51

1 Answers1

3

As you've already discovered, the issue is that the transaction session is closed, and when you call getCustomerInfoList(), you will get a LazyInitializationException.

A couple of possible ways of solving this (both good and less good):

  1. with open-in-view=true, the session will be kept alive during the life cycle of a request, allowing GsonSerializer to execute a db query to get the data
  2. Set fetch type EAGER on the relation to customerInfoList (will always execute a separate query to fetch the CustomerInfoList data from db)
  3. Use an entitygraph by annotating the repository method findBySessionId with @EntityGraph("customerInfoList") which will include a join in the db statement to always fetch customerInfoList when calling findBySessionId. (as opposed to EAGER which would execute a separate query to fetch the data)
  4. Execute all the logic inside 1 transaction to keep the session alive.
  5. use JPA projection and project the db data to DTOs (this is an option in the cases where you don't intend to change the state of your entities, rather just consume the values)
tbjorch
  • 1,544
  • 1
  • 8
  • 21
  • In my opinion. 1 is quite bad. 2 is seldom acceptable. 3. Seems rather good, but leads to boilerplate. 4. Can be quite difficult, if not impossible. 5. Increases cost of maintenance. Might I suggest, preloading the lazy collection: access the CustomerInfo while there is a Hibernate session connected to the User. For instance just calling customerInfoList.size() – Mads Hoel Jan 30 '23 at 12:27