Home > Software engineering >  Mapstruct - How to convert a DTO String parameter to an Entity object?
Mapstruct - How to convert a DTO String parameter to an Entity object?

Time:05-17

I'm new to Mapstruct and I'm trying to understand it properly.

What I want to achieve is converting from a DTO String parameter (carModel) to his Entity, retrieve using Service and Repository.

The problem is that Mapper class generated by Mapstruct is trying to inject the Service class with @Autowired annotation, but it's not working. The service is null.

Here's my @Mapper class:

@Mapper(componentModel = "spring", uses = CarModelService.class)
public interface KitMapper extends EntityMapper<KitDTO, Kit> {
    KitMapper INSTANCE = Mappers.getMapper(KitMapper.class);

    @Mapping(source = "weight", target = "systemWeight")
    @Mapping(source = "carModel", target = "carModel")
    Kit toEntity(KitDTO kitDTO);
}

public interface EntityMapper<D, E> {
    E toEntity(D dto);
    List<E> toEntity(List<D> dtoList);
}

The @Service class:

@Service
@Transactional
public class CarModelService {
    private final CarModelRepository carModelRepository;

    @Transactional(readOnly = true)
    public CarModel findByName(String name) {
        return carModelRepository.findByName(name).orElse(null);
    }
}

The @Repository class:

@Repository
public interface CarModelRepository extends JpaRepository<CarModel, Long> {
    Optional<CarModel> findByName(String carModelName);
}

The DTO and Entity classes:

public class KitDTO {
    private String id;
    private String carModel; // e.g. "Ferrari Monza"
    ....
}
@Entity
@Table(name = "kit")
public class Kit implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
    @SequenceGenerator(name = "sequenceGenerator")
    @Column(name = "id")
    private Long id;

    @ManyToOne
    private CarModel carModel;

    ...
}
@Entity
@Table(name = "car_model")
public class CarModel implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
    @SequenceGenerator(name = "sequenceGenerator")
    @Column(name = "id")
    private Long id;

    @Column(name = "name")
    private String name;

    ...
}

The build work properly but the application stop when I try to use the Mapper. It says that carModelService is null. Here's the mapper generated implementation class:

@Component
public class KitMapperImpl implements KitMapper {

    @Autowired // <-- this seems not working
    private CarModelService carModelService;

    @Override
    public Kit toEntity(KitDTO kitDTO) {
        if ( kitDTO == null ) {
            return null;
        }

        Kit kit = new Kit();

        kit.setSystemWeight( String.valueOf( kitDTO.getWeight() ) );
        kit.carModel( carModelService.findByName(kitDTO.getCarModel()) ); // <-- carModelService is null!

        // other setters

        return kit;
    }
}

I've tried many things, using Decorator, @Context, expression, inject the @Mapper class into the @Service class.

I've found many questions but actually no one helped me:

Mapstruct - How can I inject a spring dependency in the Generated Mapper class

@Service Class Not Autowired in org.mapstruct.@Mapper Class

MapStruct mapper not initialized with autowired when debug

Any help would be appreciated! Thanks in advance!

CodePudding user response:

Can you please share the error message? From the information that you shared, I can see the carModel in KitDto is String and in Entity is CarModel class. Not sure how mapstruct's auto generated implementation class implemented this: kit.carModel( carModelService.findByName(kitDTO.getCarModel()) );.

But I would like to share another approach,Don't know this is a best practice or not. In this approach you can create a abstarct class of mapper, in which you can @Autowired repository can manually add those mapping. I shared the snippet for it. Hopefully this will help you.

    @Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public abstract class ProductMapper {
    @Autowired 
    private CarModelService carModelService;
    
    public abstract Kit convertDTOToEntity(KitDTO kitDTO);
    
       public Kit toEntity(KitDTO kitDTO);
        {   
        Kit kit = convertDTOToEntity(kitDTO);
        kit.setCarModel(carModelService.findByName(kitDTO.getCarModel()));
        return kit;
        }
    }

Curious about the other approaches, will follow this thread. We can discuss the best practices

CodePudding user response:

Found the solution!

Instead of calling directly the Mapper method toEntity() from the @RestController class, I injected the mapper in the CarModelService class and created a method that call the mapper. In this way the flow is:

Controller --> Service --> Mapper

@Service
@Transactional
public class KitService {
    private final KitRepository kitRepository;

    private final KitSearchRepository kitSearchRepository;

    private final KitMapper kitMapper; // <-- mapper declaration

    public KitService(KitRepository kitRepository, KitSearchRepository kitSearchRepository, KitMapper kitMapper) {
        this.kitRepository = kitRepository;
        this.kitSearchRepository = kitSearchRepository;
        this.kitMapper = kitMapper; // <-- mapper initilization
    }

    // here the method which calls mapper
    public Kit convertDTOToEntity(KitDTO kitDTO) {
        return kitMapper.toEntity(kitDTO);
    }

In this way, the generated class by Mapstruct doesn't give error on the CarModelService.

Seems like this approach is the only way to achieve this, create a king of "bridge" between services and mappers.

(You can use also the @Autowired annotation instead of constructor)

  • Related