How to load the collection of a LAZY association for an already found entity

Tags: , , , ,



Consider the following:

@Entity
@Table(name = "cars")
public class Car {

    @Id
    private int id;
    private String name;

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "ownerCar")
    private Set<Wheel> wheels = new HashSet<>();

    private Car() {
    }

    public Car(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public Set<Wheel> getWheels() {
        return wheels;
    }
}
@Entity
@Table(name = "wheels")
public class Wheel {

    @Id
    private int id;

    @ManyToOne
    private Car ownerCar;

    private Wheel() {
    }

    public Wheel(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public Car getOwnerCar() {
        return ownerCar;
    }

    public void setOwnerCar(Car ownerCar) {
        this.ownerCar = ownerCar;
    }
}

@Override //CommandLineRunner
public void run(String... args) throws Exception {
    Car car = new Car(1);
    car.setName("Ferrari");

    Wheel wheel = new Wheel(1);
    wheel.setOwnerCar(car);

    car.getWheels().add(wheel);
    carService.saveCar(car);

    // Assume we have found the car already
    car = carService.getById(1).get();

    // Load the wheels of this car
    carService.loadWheelsForCar(car);

    System.out.println(car.getWheels().size());
}

The code above will throw org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: test.app.Car.wheels.

My question is how to implement loadWheelsForCar(Car c) method without having to find the car again.

With other words, how to SELECT * FROM WHEELS WHERE owner_car_id = car.id and add the result to the collection. I can probably do it manually, but is this the only way to go?

I am aware that LazyInitializationException is thrown when there is no active session(Doesn’t @Transactional cause the creation of a new one?). I have tried to:

@Transactional
public void loadWheelsForCar(Car c) {
    c.getWheels().size(); // will load wheels
}

but the exception is thrown.

In case of a XY problem, the reason I don’t do this (CarService):

@Transactional
public Optional<Car> getByIdWithWheels(int carId) {
    Optional<Car> possibleCar = carRepository.findById(carId);
    possibleCar.ifPresent(c -> c.getWheels().size());
    return possibleCar;
}

is because the parent entity (Car entity) in the main application has multiple @OneToMany associations and some them have nested ones as well. If i follow this approach I will end up with multiple @Transactional methods like getCarByIdWithWheels, getCarByIdWithSeats, getCarByIdWithSeatsAndWheels, etc. But what I want is to be able to do something like:

Car c = carService.getById(1);
carService.loadWheelsForCar(c);
carService.loadSeatsForCar(c);

I tried somethings found in web but every solution I found was “re-loading” the “Car” entity.

Answer

I am aware that LazyInitializationException is thrown when there is no active session(Doesn’t @Transactional cause the creation of a new one?)

Hibernate does create a new session. But that session is fresh and it is not aware of your Car c as that car is not fetched, saved or updated in that new session because you fetched that car in a different session.

  • You know that your car you are passing is exactly same as the car in the database and there is no update in between. Unfortunately there is no API in hibernate to tell that here is a car exactly as it is in database, don’t check it with it database and just believe you. There is no api like session.attach(car).

  • So the closest you can do is session.merge(car). Hibernate will issue a select to check if the car you are passing and the car in the database are same, and since it is same, it will not issue an update. As part of that select wheels would have been loaded too.

    @Autowired
    EntityManager em;

    @Transactional
    public Car loadWheelsForCar(Car car){
        Car merged = em.merge(car);
        merged.getWheels().size();
        return merged;
    }
  • You will see the above method does not issue any update but just one select query similar to below
    select
        car0_.id as id1_0_1_,
        car0_.name as name2_0_1_,
        wheels1_.owner_car_id as owner_ca3_1_3_,
        wheels1_.id as id1_1_3_,
        wheels1_.id as id1_1_0_,
        wheels1_.name as name2_1_0_,
        wheels1_.owner_car_id as owner_ca3_1_0_ 
    from
        cars car0_ 
    left outer join
        wheels wheels1_ 
            on car0_.id=wheels1_.owner_car_id 
    where
        car0_.id=?


Source: stackoverflow