Nullability issues with the ModelMapper framework

16 views Asked by At

I’m facing a conversion problem using ModelMapper. I’m developing an API and, when updating the object, ModelMapper is setting one of the fields to NULL. I’ve tried configuring it in several ways, but none of them solved the problem. In one of these configurations, I even managed to save in the database, but there was a business rule issue.

Basically, the object has fields such as creation time, update time, and deletion time.When the object is created, it comes with the creation and update times set. However, when the object is updated, an error occurs. ModelMapper is setting the creation time to NULL, which should not happen. The framework shouldn’t even set the creation time when converting from UpdateDto to DetailDto and from DetailDto to Product.

I already know the reason for the error, but I don’t know how to correct it. The DetailDto class has the three time fields mentioned above, while the UpdateDto class has only the update field. This is causing ModelMapper to set the creation field to NULL. The Product class has all three fields. The Java classes are presented below.

@SuppressWarnings("unused")
@Configuration
public class ModelMapperConfig {    @Bean
    public ModelMapper modelMapper() {
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.getConfiguration()
                .setMatchingStrategy(MatchingStrategies.STRICT)
                .setFieldMatchingEnabled(true)
                .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
                .setPropertyCondition(Conditions.isNotNull())
                .setSkipNullEnabled(true);  // Configuração para não sobrescrever campos não nulos

        // Adicionar um mapeamento personalizado para ProductUpdateDTO e ProductDetailDTO
        modelMapper.createTypeMap(ProductUpdateDTO.class, ProductDetailDTO.class)
                .addMappings(mapper -> mapper.skip(ProductDetailDTO::setCreatedOn));

        modelMapper.createTypeMap(ProductDetailDTO.class, Product.class)
                .addMappings(mapper -> mapper.skip(Product::setCreatedOn));

        return modelMapper;
    }    @Bean
    public ModelMapperConverter modelMapperConverter() {
        return new ModelMapperConverter();
    }

}
@RestController
@RequestMapping("/products")
public class ProductController {

    @Autowired
    private ProductService service;

    @Autowired
    private ModelMapperConverter modelMapperConverter;

    @PostMapping(produces = {"application/json"}, consumes = {"application/json"})
    public ResponseEntity<ProductDetailDTO> create(@RequestBody @Valid ProductCreateDTO productCreateDTO) {
        var productDetails = service.save(modelMapperConverter.map(productCreateDTO, ProductDetailDTO.class));
        productDetails.add(linkTo(methodOn(ProductController.class).detail(productDetails.getId())).withSelfRel());
        return ResponseEntity.created(productDetails.getRequiredLink(IanaLinkRelations.SELF).toUri()).body(productDetails);
    }

    @PutMapping(produces = {"application/json"}, consumes = {"application/json"})
    public ResponseEntity<ProductDetailDTO> update(@RequestBody @Valid ProductUpdateDTO productUpdateDTO) {
        var productDetails = service.update(modelMapperConverter.map(productUpdateDTO, ProductDetailDTO.class));
        productDetails.add(linkTo(methodOn(ProductController.class).detail(productDetails.getId())).withSelfRel());
        return ResponseEntity.ok(productDetails);
    }

    @GetMapping(value = "/{id}", produces = {"application/json"})
    public ResponseEntity<ProductDetailDTO> detail(@PathVariable UUID id) {
        var productDetails = service.findById(id);
        productDetails.add(linkTo(methodOn(ProductController.class).detail(id)).withSelfRel());
        return ResponseEntity.ok(productDetails);
    }

    @GetMapping(produces = {"application/json"})
    public ResponseEntity<Page<ProductListDTO>> listAll(@PageableDefault(size = 15, sort = {"created_on"}) Pageable pageable) {
        var products = service.findAll(pageable);
        products.forEach(product -> product.add(linkTo(methodOn(ProductController.class).detail(product.getId())).withSelfRel()));
        return ResponseEntity.ok(products);
    }

    @DeleteMapping
    public ResponseEntity<Void> remove(@RequestBody List<UUID> ids) {
        service.remove(ids);
        return ResponseEntity.noContent().build();
    }

}


@Service
public class ProductService implements JpaService<Product, ProductDetailDTO, ProductListDTO> {

    private final JpaCore<Product> jpaCore;
    private final ModelMapperConverter modelMapperConverter;

    @Autowired
    public ProductService(@Qualifier("productJpaCore") JpaCore<Product> jpaCore, ModelMapperConverter modelMapperConverter) {
        this.jpaCore = jpaCore;
        this.modelMapperConverter = modelMapperConverter;
    }

    @Override
    public ProductDetailDTO save(ProductDetailDTO productDetailDTO) {
        return modelMapperConverter.map(jpaCore.save(modelMapperConverter.map(productDetailDTO, Product.class)), ProductDetailDTO.class);
    }

    @Override
    public ProductDetailDTO update(ProductDetailDTO productDetailDTO) {
        Product product = modelMapperConverter.map(productDetailDTO, Product.class);
        if (jpaCore.findById(productDetailDTO.getId()) == null) {
            throw new EntityNotFoundException("O produto que você deseja editar não existe!");
        }
        return modelMapperConverter.map(jpaCore.update(product), ProductDetailDTO.class);
    }

    @Override
    public ProductDetailDTO findById(UUID id) {
        return modelMapperConverter.map(jpaCore.findById(id), ProductDetailDTO.class);
    }

    @Override
    public Page<ProductListDTO> findAll(Pageable pageable) {
        return jpaCore.findAllNotDeleted(pageable).map(product -> modelMapperConverter.map(product, ProductListDTO.class));
    }


    @Override
    public void remove(List<UUID> ids) {
        jpaCore.remove(ids);
    }

}

@Table(name = "products")
@Entity(name = "Product")
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private UUID id;
    @Column(name = "name", nullable = false)
    private String name;
    @Column(name = "price", nullable = false)
    private BigDecimal price;
    @Column(name = "description")
    private String description;
    @Column(name = "category", nullable = false)
    @Enumerated(EnumType.STRING)
    private Constants.ProductCategory category;
    @Column(name = "image_url")
    private String imageUrl;
    @Column(name = "created_on", nullable = false)
    private LocalDateTime createdOn;
    @Column(name = "updated_on", nullable = false)
    private LocalDateTime updatedOn;
    @Column(name = "deleted_on")
    private LocalDateTime deletedOn;

    public Product() {
    }

public class ProductUpdateDTO extends RepresentationModel<ProductUpdateDTO> implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;
    @NotNull
    private UUID id;
    private String name;
    private BigDecimal price;
    private String description;
    private Constants.ProductCategory category;
    private String imageUrl;
    private LocalDateTime UpdatedOn;

    public ProductUpdateDTO() {
        setUpdatedOn(LocalDateTime.now());
    }

@JsonInclude(JsonInclude.Include.NON_NULL)
public class ProductDetailDTO extends RepresentationModel<ProductDetailDTO> implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;
    private UUID id;
    private String name;
    private BigDecimal price;
    private String description;
    private Constants.ProductCategory category;
    private String imageUrl;
    private LocalDateTime CreatedOn;
    private LocalDateTime UpdatedOn;
    private LocalDateTime DeletedOn;

    public ProductDetailDTO() {
    }

I have attempted to reconfigure ModelMapper with the expectation of resolving the nullability issue that was occurring during the object update. My intention was to ensure that the creation time field was not set to NULL during the conversion from UpdateDto to DetailDto and from DetailDto to Product.

I was expecting that by adjusting the ModelMapper settings, it could correctly distinguish between the fields that should be mapped and those that should not. However, despite several attempts at reconfiguration, the problem persisted.

Now, I am in search of a solution that allows ModelMapper to perform the conversion correctly, keeping the creation time field intact during the object update. This is crucial to ensure data integrity and the proper functioning of my API.

0

There are 0 answers