Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mapping an object to an immutable object with builder (using immutables annotation processor) in mapstruct

We are using the immutables framework to generate all DTOs. Now we would like to map these objects one to another with mapstruct. But the generated DTOs are immutable and have no setters and no constructor, corresponding to the builder pattern. They are only filled through the corresponding builder accessed by a static builder()-method.

We instead tried to map DTO1 to DTO2.Builder which would work if mapstruct would recognize the setter in the Builder but these do not have void return type but return the Builder itself for fluent concatenation.

So here is the code of the example.

We have two Interfaces

@Value.Immutable
public interface MammalDto {
  public Integer getNumberOfLegs();
  public Long getNumberOfStomachs();
}

and

@Value.Immutable
public interface MammalEntity {
  public Long getNumberOfLegs();
  public Long getNumberOfStomachs();
}

Then we have the Mapper interface for mapstruct:

@Mapper(uses = ObjectFactory.class)
public interface SourceTargetMapper {
  SourceTargetMapper MAPPER = Mappers.getMapper( SourceTargetMapper.class );

  ImmutableMammalEntity.Builder toTarget(MammalDto source);
}

For mapstruct to find the Builder we need a Factory:

public class ObjectFactory {

  public ImmutableMammalDto.Builder createMammalDto() {
    return ImmutableMammalDto.builder();
  }

  public ImmutableMammalEntity.Builder createMammalEntity() {
    return ImmutableMammalEntity.builder();
  }
}

In order to generate the code the compiler plugin was instructed to use both annotation processors:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.6.1</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <annotationProcessorPaths>
            <path>
                <groupId>org.immutables</groupId>
                <artifactId>value</artifactId>
                <version>2.2.8</version>
            </path>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>1.2.0.Beta3</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

Note: This will work only with mapstruct version > 1.2.x. Older versions have the problem in a clean build (mvn clean compile) that they do not find the sources that immutables just built. In a second build (without clean) they would find the immutables implementations because they were on the classpath before annotation processors were run. This bug is fixed now.

This works like a charm. First the Immutable implementations of the interfactes are generated and mapstruct uses them to generate the builder.

But the Test shows that no properties are set:

@Test
public void test() {
  MammalDto s = ImmutableMammalDto.builder().numberOfLegs(4).numberOfStomachs(3l).build();
  MammalEntity t = SourceTargetMapper.MAPPER.toTarget(s).build();
    assertThat(t.getNumberOfLegs()).isEqualTo(4);
    assertThat(t.getNumberOfStomachs()).isEqualTo(3);
}

The asserts fail. One look at the mapper generated by mapstruct shows that it has obviously not found any setters:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    //...
)
public class SourceTargetMapperImpl implements SourceTargetMapper {
    private final ObjectFactory objectFactory = new ObjectFactory();

    @Override
    public Builder toTarget(MammalDto source) {
        if ( source == null ) {
            return null;
        }

        Builder builder = objectFactory.createMammalEntity();
        return builder;
    }
}

The empty builder is returned. I think the reason is the setter implementation of the generated builder because it returns itself to create a fluent API:

public final Builder numberOfLegs(Long numberOfLegs) {
  this.numberOfLegs = Objects.requireNonNull(numberOfLegs, "numberOfLegs");
  return this;
}

Is there a way to let mapstruct find these setters? Or even a better way to deal with such immutable objects with builders?

EDIT: As I stated in the comment I ran into Issue #782. In version 1.2.0.Beta3 builders are still not supported. But there are several discussions on this topic so it might be interesting to follow the issue if one has the same problem.

like image 366
Marc von Renteln Avatar asked Aug 04 '16 14:08

Marc von Renteln


People also ask

How do I map a MapStruct collection?

In general, mapping collections with MapStruct works the same way as for simple types. Basically, we have to create a simple interface or abstract class, and declare the mapping methods. Based on our declarations, MapStruct will generate the mapping code automatically.

Does MapStruct use builder?

Java Prime PackMapStruct allows to use Builders. We can use Builder frameworks or can use our custom builder. In below example, we are using a custom builder.

What is componentModel in MapStruct?

Enclosing class: MappingConstants public static final class MappingConstants.ComponentModel extends Object. Specifies the component model constants to which the generated mapper should adhere. It can be used with the annotation Mapper.componentModel() or MapperConfig.componentModel()

What is the use of MapStruct?

MapStruct is a code generator that greatly simplifies the implementation of mappings between Java bean types based on a convention over configuration approach. The generated mapping code uses plain method invocations and thus is fast, type-safe and easy to understand.


2 Answers

You can configure Immutables to generate setters in the builder:

@Value.Immutable
@Value.Style(init = "set*")
public interface MammalEntity {
    public Long getNumberOfLegs();
    public Long getNumberOfStomachs();
}

And you don't need the ObjectBuilder, you can directly use the generated Immutable class

@Mapper(uses = ImmutableMammalEntity.class)
public interface SourceTargetMapper {
    SourceTargetMapper MAPPER = Mappers.getMapper( SourceTargetMapper.class );

    ImmutableMammalEntity.Builder toTarget(MammalDto source);
}

You can even define these settings in your own annotation

@Value.Style(init = "set*")
public @interface SharedData {}

and use that instead

@SharedData
@Value.Immutable
public interface MammalEntity {
    public Long getNumberOfLegs();
    public Long getNumberOfStomachs();
}
like image 153
Tobias Schulte Avatar answered Oct 04 '22 16:10

Tobias Schulte


Since 1.3 MapStruct supports Immutables. Look here for more details.

like image 20
Sjaak Avatar answered Oct 04 '22 17:10

Sjaak