0
votes

I am using Spring Data and Mapstruct and I don't want hibernate to blindly load all the elements while mapping entity to dto.

Example:

public class VacancyEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Integer id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "job_category_id", nullable = false)
    JobCategoryEntity jobCategory;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "company_id", nullable = false)
    CompanyEntity company;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "employer_created_by", nullable = false)
    EmployerProfileEntity employerCreatedBy;

    @Column(nullable = false)
    String title;
.... }

DTO:

public class VacancyDto {

    Integer id;

    String title;

    CompanyDto company;

    EmployerProfileDto employerCreatedBy;

    JobCategoryDto jobCategory;

    ...} 

So I have two methods findByIdWithCompanyAndCity and findByIdWithJobAndCityAndEmployer in VacancyRepository to perform only one SQL request.

And two @Transactional methods in my VacancyService: findWithCompanyAndCity and findWithCompanyAndCityAndEmployer.

Best practice is returning Dto from Service layer, so we need to parse Entity to Dto in the Service.

And I really don't want to just leave whole mapping in @Transactional (session) because if I add some field really deep into my entity, Mapstruct just trigger N+1 problem.

Best that I came up with, is to include each inner entity into method and check manually that Mapstruct don't add some new methods. (it is faster then checking names) Ex:

    @Mapping(target = "id", source = "entity.id")
    @Mapping(target = "description", source = "entity.description")
    @Mapping(target = "jobCategory", source = "jobCategoryDto")
    @Mapping(target = "employerCreatedBy", source = "employerProfileDto")
    @Mapping(target = "city", source = "cityDto")
    @Mapping(target = "company", ignore = true)
    VacancyDto toDto(VacancyEntity entity,
                     JobCategoryDto jobCategoryDto,
                     EmployerProfileDto employerProfileDto,
                     CityDto cityDto);
   ....

But this doesn't fix the real issue. There are still session while mapping, so it can lead to N+1 problem.

So I came up with several solutions

  • Use special method in Service to trigger @Transactional method and then map into DTO out of session scope. But it seems really ugly to double methods in Service
  • Return Entity from Service (which is Bad Practice) and map into DTO there.

I know that I'll get LazyInitializationException in both cases, but it seems to me like it more robust and scalable then just unpredictably SELECT.

How do I perform the mapping from entity to DTO in the service layer but outside the Hibernate session in an elegant way?

2

2 Answers

1
votes

You didn't ask a question but it seems the question is supposed to be:

How do I perform the mapping from entity to DTO in the service layer but outside the Hibernate session in an elegant way.

I'd recommend the TransactionTemplate for this. Usage looks like this:

@Autowired 
VacancyRepository repo;

@Autowired
TransactionTemplate tx;

void someMethod(String company, String city){

    VacancyEntity vac = tx.execute(__ -> repo.findWithCompanyAndCity(company, city));
    
    return mappToDto(vac);
}

That said, I think you are using the wrong a approach to solve the underlying problem. I suggest you take a look at having a test to verify the number of SQL statements executed. See https://vladmihalcea.com/how-to-detect-the-n-plus-one-query-problem-during-testing/ for a way to do that.

0
votes

To avoid the N + 1 problem you still need to use an entity graph, although I think this is a perfect use case for Blaze-Persistence Entity Views.

I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.

A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:

@EntityView(VacancyEntity.class)
public interface VacancyDto {
    @IdMapping
    Integer getId();
    String getTitle();
    CompanyDto getCompany();
    EmployerProfileDto getEmployerCreatedBy();
    JobCategoryDto getJobCategory();

    @EntityView(CompanyEntity.class)
    interface CompanyDto {
        @IdMapping
        Integer getId();
        String getName();
    }

    @EntityView(EmployerProfileEntity.class)
    interface EmployerProfileDto {
        @IdMapping
        Integer getId();
        String getName();
    }

    @EntityView(JobCategoryEntity.class)
    interface JobCategoryDto {
        @IdMapping
        Integer getId();
        String getName();
    }
}

Querying is a matter of applying the entity view to a query, the simplest being just a query by id.

VacancyDto a = entityViewManager.find(entityManager, VacancyDto.class, id);

The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features

Page<VacancyDto> findAll(Pageable pageable);

The best part is, it will only fetch the state that is actually necessary!