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
get****
usually are not.CarReadService
annotated with@Transactional(readOnly = true)
and a read/write transactional write serviceCarWriteService
annotated by@Transactional