In a typical Java Spring Web APP:
we have the following layers:
Model [DB Models]
Repositories [where you have queries to DB]
Services [Business service where you have the @Transactional annotation]
Controllers [Rest endpoints]
So for a simple model e.g Car
@Entity
Car {
Long id;
String name;
@ManyToOne(fetch = FetchType.LAZY) // Notice lazy here
Engine engine;
}
CarRepo extends JpaRepository {....}
@Transactional
CarService {
....
}
@RestController
CarController{
@GET
public CarDto getCar(Long id) {
???
}
}
??? : Here is the big dilemma, I use a mapstruct to convert objects to other formats, whenever I use it as in first following scenario I get LazyInitializationException
:
Scenario#1 Get the model in controller (Which is not so good to do especially that models should be encapsulated from the view layer) and convert it to CarDto
CarController{
@GET
public CarDto getCar(Long id) {
Car car= carService.getCar(id);
return carMapper.toCarDto(car); // BAM `LazyInitializationException`, on `Engine` field!!!
}
}
But here the problem, When mapper starts to convert Engine
it will get LazyInitializationException
since transaction was already committed and closed in service and Engine
is lazy initialized
That moves us to Scenario#2
Ok so do the conversions in service then daa! while you still have the transaction opened, in service, so update the getCar
method to return a CarDto
instead:
@Transactional
CarService {
CarDto getCar(Long id) {.... return mapper.toCarDto(car);} // Hurrah!!, no lazy exceptions since there is a transnational context wrapping that method
}
But here is another problem, for other services in that uses Car
suppose we have FactoryService
and we want to get a car by id so that we can assign it to a factory model so we will diffidently need the Car
model not the dto,
FactoryService {
void createFactory() {
Factory factory = ....;
Car car = carService.getCarModel...
factory.addCar(car);
}
}
so simple solution to this is to add another method with different name but will return model that time in the CarService
@Transactional
CarService {
CarDto getCar(Long id) {.... return mapper.toCarDto(car);}
Car getCarModel(Long id) {.... return car;}
}
But as you can see it is now ugly! to have the same function twice with same logic only with 2 different return types, that will also lead to have aloooot of same type logic method across the services
Eventually we have Scenario#3 and that is simply use Solution#1 but move the @Transactional annotation to the controller now we won't get the lazy exception when we use mapstruct in controller (But this is not very recommended thing to do since we are taking controlling transactions out of the service (business) layer)
@Transactional
CarController{
@GET
public CarDto getCar(Long id) {
???
}
}
So what would be the best approach to follow here
1 Answer 1
The first scenario seems a good approach. By default, the method getCar
should just return the Car
object without the Engine
(as it is set to be fetched lazily). If the controller needs the Car
and the Engine
, an option is to provide an additional method CarService#getCarWithEngine
. Such a method, will explicitly load and return the Car
with the Engine
(see this answer). This can be done in many ways, see here.
For example, as suggested in the linked tutorial, you can create a method in CarRepo
that uses Join Fetching:
public interface CarRepo extends JpaRepository<Car, Long> {
@Query("SELECT c FROM Car c JOIN FETCH c.engine")
Optional<Car> findCarByIdWithEngine(Long id);
}
This method can then be used by CarService#getCarWithEngine
(with or without @Transactional) to implement scenario 1. Mapping the entity to the DTO in the controller layer keeps the service layer independent of the particular request so that it can be better reused and tested. Such an approach is also suggested here.
Scenario 2 makes CarService
dependent on the DTO, which (as you have demonstrated) is not ideal. A new API that needs a different view of the Car
, or even a slightly different response in the controller layer, will require CarService
to change.
An additional approach is to keep the session open with spring.jpa.open-in-view=true
, so that Hibernate can resolve lazy associations even after returning from an explicit @Transactional service. More info here.
There are more than one solution, it depends on the abstraction level that you need for your project, and if the additional complexity is worth it.
-
Thanks for your answer, For
CarService#getCarWithEngine
I like this but doing this for all models could be a pretty damn job to cover all view scenarios and also will pollute the service imagine that we havewithEngine
withEngineAndManufacturer
withEngineAndManufacturerWithOriginCountry
however I would think that this would be the best approach yet I can think of right now at least it will avoid issuing the query to load the lazy field since on that case it will be returned by the original queryYouans– Youans2021年01月11日 15:08:01 +00:00Commented Jan 11, 2021 at 15:08 -
1For
spring.jpa.open-in-view=true
the OSIV is considered an Anti-Pattern check @Vlad Mihalcea answer here stackoverflow.com/a/37526397/1460591Youans– Youans2021年01月11日 15:08:07 +00:00Commented Jan 11, 2021 at 15:08 -
@YouYou I am glad I could help. I see that there are a lot of discussions about OSIV. Even though it's considered an anti-pattern by many it's enabled by default in Spring Boot (with a warning). So I listed it as an additional approach since in some use cases can still be relevant.Marc– Marc2021年01月11日 15:49:04 +00:00Commented Jan 11, 2021 at 15:49
-
Yeah, I just thought you might not know that, since I have just know it recentlyYouans– Youans2021年01月11日 16:56:03 +00:00Commented Jan 11, 2021 at 16:56
-
to me this method is bypassing the LAZY configuration and force it to be EAGER. If I'm right you may as well set engine to EAGER.Julien– Julien2024年11月29日 10:18:11 +00:00Commented Nov 29, 2024 at 10:18
Explore related questions
See similar questions with these tags.
get****
usually are not.CarReadService
annotated with@Transactional(readOnly = true)
and a read/write transactional write serviceCarWriteService
annotated by@Transactional