javamapstruct

Can mapstruct do a partial update on records?


I was led to believe by IntelliJ's code AI that mapstruct could do a partial update on a Java record.

Update: The AI said mapstruct would automatically (automagically?) use a builder or canonical constructor to create a new instance merging the two records and return that new instance.

Am I doing something wrong or was I mislead by the AI?

Thanks for thinking about my question!

My records and mapper code

public record Example(Long id, String name){}

public record ExampleWithBuilder(Long id, String name) {
    public static ExampleWithBuilderBuilder builder() {
        return new ExampleWithBuilderBuilder();
    }

    public static class ExampleWithBuilderBuilder {
        private Long id;
        private String name;

        ExampleWithBuilderBuilder() {
        }

        public ExampleWithBuilderBuilder id(Long id) {
            this.id = id;
            return this;
        }

        public ExampleWithBuilderBuilder name(String name) {
            this.name = name;
            return this;
        }

        public ExampleWithBuilder build() {
            return new ExampleWithBuilder(this.id, this.name);
        }

        public String toString() {
            return "ExampleWithBuilder.ExampleWithBuilderBuilder(id=" + this.id + ", name=" + this.name + ")";
        }
    }
}

@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = MappingConstants.ComponentModel.SPRING)
public interface RacerMapper {
    @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    void partialUpdate(ExampleWithBuilder exampleWithBuilder, @MappingTarget Example example);

    @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    ExampleWithBuilder partialUpdate(Example example, @MappingTarget ExampleWithBuilder exampleWithBuilder);
}

Generated code is showing just returning the MappingTarget

@Override
public Example partialUpdate(ExampleWithBuilder exampleWithBuilder, Example example) {
    if ( exampleWithBuilder == null ) {
        return example;
    }

    return example;
}

@Override
public ExampleWithBuilder partialUpdate(Example example, ExampleWithBuilder exampleWithBuilder) {
    if ( example == null ) {
        return exampleWithBuilder;
    }

    return exampleWithBuilder;
}

pom.xml dependency

    <!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct -->
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>1.6.3</version>
    </dependency>


Solution

  • As I said, records are immutable and do not provide a setter for its attributes. Hence, @MappingTarget cannot be used. If you want to stick to records, you need to implement the logic by yourself for the "patch" part or use use mapstruct's defaultExpression for fallback values. The latter only applies if source value is null. In any case the resulting object will be a new and not a patched one.

    following an Example:

    public record SourceRecord(Long uid, String uname) {}
    public record TargetRecord(Long id, String name) {}
    
    @Mapper
    public interface RecordMapper {
    
      RecordMapper INSTANCE = Mappers.getMapper(RecordMapper.class);
    
      @Mapping(target = "id", source = "source.uid", defaultExpression = "java(patchObject.id())")
      @Mapping(target = "name", source = "source.uname", defaultExpression = "java(patchObject.name())")
      TargetRecord map(SourceRecord source, TargetRecord patchObject); 
    
      default TargetRecord updateManual(TargetRecord existing, SourceRecord input) {
        return new TargetRecord(
            input.uid() != null ? input.uid() : existing.id(),
            input.uname() != null ? input.uname() : existing.name()
        );
      }
    }
    
     public void map() {
        final TargetRecord existing = new TargetRecord(1L, "John");
        final SourceRecord source = new SourceRecord(null, "Joanna");
        final TargetRecord mapped = RecordMapper.INSTANCE.map(source); // Results in TargetRecord[id=1, name=Joanna]
        final TargetRecord update = RecordMapper.INSTANCE.updateManual(existing, source); // Results in TargetRecord[id=1, name=Joanna]
      }