mirror of
https://github.com/mapstruct/mapstruct.git
synced 2025-07-12 00:00:08 +08:00
491 lines
21 KiB
Plaintext
491 lines
21 KiB
Plaintext
== Advanced mapping options
|
||
This chapter describes several advanced options which allow to fine-tune the behavior of the generated mapping code as needed.
|
||
|
||
[[default-values-and-constants]]
|
||
=== Default values and constants
|
||
|
||
Default values can be specified to set a predefined value to a target property if the corresponding source property is `null`. Constants can be specified to set such a predefined value in any case. Default values and constants are specified as String values. When the target type is a primitive or a boxed type, the String value is taken literal. Bit / octal / decimal / hex patterns are allowed in such a case as long as they are a valid literal.
|
||
In all other cases, constant or default values are subject to type conversion either via built-in conversions or the invocation of other mapping methods in order to match the type required by the target property.
|
||
|
||
A mapping with a constant must not include a reference to a source property. The following example shows some mappings using default values and constants:
|
||
|
||
.Mapping method with default values and constants
|
||
====
|
||
[source, java, linenums]
|
||
[subs="verbatim,attributes"]
|
||
----
|
||
@Mapper(uses = StringListMapper.class)
|
||
public interface SourceTargetMapper {
|
||
|
||
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
|
||
|
||
@Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")
|
||
@Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")
|
||
@Mapping(target = "stringConstant", constant = "Constant Value")
|
||
@Mapping(target = "integerConstant", constant = "14")
|
||
@Mapping(target = "longWrapperConstant", constant = "3001")
|
||
@Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014")
|
||
@Mapping(target = "stringListConstants", constant = "jack-jill-tom")
|
||
Target sourceToTarget(Source s);
|
||
}
|
||
----
|
||
====
|
||
|
||
If `s.getStringProp() == null`, then the target property `stringProperty` will be set to `"undefined"` instead of applying the value from `s.getStringProp()`. If `s.getLongProperty() == null`, then the target property `longProperty` will be set to `-1`.
|
||
The String `"Constant Value"` is set as is to the target property `stringConstant`. The value `"3001"` is type-converted to the `Long` (wrapper) class of target property `longWrapperConstant`. Date properties also require a date format. The constant `"jack-jill-tom"` demonstrates how the hand-written class `StringListMapper` is invoked to map the dash-separated list into a `List<String>`.
|
||
|
||
[[expressions]]
|
||
=== Expressions
|
||
|
||
By means of Expressions it will be possible to include constructs from a number of languages.
|
||
|
||
Currently only Java is supported as a language. This feature is e.g. useful to invoke constructors. The entire source object is available for usage in the expression. Care should be taken to insert only valid Java code: MapStruct will not validate the expression at generation-time, but errors will show up in the generated classes during compilation.
|
||
|
||
The example below demonstrates how two source properties can be mapped to one target:
|
||
|
||
.Mapping method using an expression
|
||
====
|
||
[source, java, linenums]
|
||
[subs="verbatim,attributes"]
|
||
----
|
||
@Mapper
|
||
public interface SourceTargetMapper {
|
||
|
||
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
|
||
|
||
@Mapping(target = "timeAndFormat",
|
||
expression = "java( new org.sample.TimeAndFormat( s.getTime(), s.getFormat() ) )")
|
||
Target sourceToTarget(Source s);
|
||
}
|
||
----
|
||
====
|
||
|
||
The example demonstrates how the source properties `time` and `format` are composed into one target property `TimeAndFormat`. Please note that the fully qualified package name is specified because MapStruct does not take care of the import of the `TimeAndFormat` class (unless it's used otherwise explicitly in the `SourceTargetMapper`). This can be resolved by defining `imports` on the `@Mapper` annotation.
|
||
|
||
.Declaring an import
|
||
====
|
||
[source, java, linenums]
|
||
[subs="verbatim,attributes"]
|
||
----
|
||
imports org.sample.TimeAndFormat;
|
||
|
||
@Mapper( imports = TimeAndFormat.class )
|
||
public interface SourceTargetMapper {
|
||
|
||
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
|
||
|
||
@Mapping(target = "timeAndFormat",
|
||
expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
|
||
Target sourceToTarget(Source s);
|
||
}
|
||
----
|
||
====
|
||
|
||
[[default-expressions]]
|
||
=== Default Expressions
|
||
|
||
Default expressions are a combination of default values and expressions. They will only be used when the source attribute is `null`.
|
||
|
||
The same warnings and restrictions apply to default expressions that apply to expressions. Only Java is supported, and MapStruct will not validate the expression at generation-time.
|
||
|
||
The example below demonstrates how a default expression can be used to set a value when the source attribute is not present (e.g. is `null`):
|
||
|
||
.Mapping method using a default expression
|
||
====
|
||
[source, java, linenums]
|
||
[subs="verbatim,attributes"]
|
||
----
|
||
imports java.util.UUID;
|
||
|
||
@Mapper( imports = UUID.class )
|
||
public interface SourceTargetMapper {
|
||
|
||
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
|
||
|
||
@Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )")
|
||
Target sourceToTarget(Source s);
|
||
}
|
||
----
|
||
====
|
||
|
||
The example demonstrates how to use defaultExpression to set an `ID` field if the source field is null, this could be used to take the existing `sourceId` from the source object if it is set, or create a new `Id` if it isn't. Please note that the fully qualified package name is specified because MapStruct does not take care of the import of the `UUID` class (unless it’s used otherwise explicitly in the `SourceTargetMapper`). This can be resolved by defining imports on the @Mapper annotation (see <<expressions>>).
|
||
|
||
[[sub-class-mappings]]
|
||
=== Subclass Mapping
|
||
|
||
When both input and result types have an inheritance relation, you would want the correct specialization be mapped to the matching specialization.
|
||
Suppose an `Apple` and a `Banana`, which are both specializations of `Fruit`.
|
||
|
||
.Specifying the sub class mappings of a fruit mapping
|
||
====
|
||
[source, java, linenums]
|
||
[subs="verbatim,attributes"]
|
||
----
|
||
@Mapper
|
||
public interface FruitMapper {
|
||
|
||
@SubclassMapping( source = AppleDto.class, target = Apple.class )
|
||
@SubclassMapping( source = BananaDto.class, target = Banana.class )
|
||
Fruit map( FruitDto source );
|
||
|
||
}
|
||
----
|
||
====
|
||
|
||
If you would just use a normal mapping both the `AppleDto` and the `BananaDto` would be made into a `Fruit` object, instead of an `Apple` and a `Banana` object.
|
||
By using the subclass mapping an `AppleDtoToApple` mapping will be used for `AppleDto` objects, and an `BananaDtoToBanana` mapping will be used for `BananaDto` objects.
|
||
If you try to map a `GrapeDto` it would still turn it into a `Fruit`.
|
||
|
||
In the case that the `Fruit` is an abstract class or an interface, you would get a compile error.
|
||
|
||
To allow mappings for abstract classes or interfaces you need to set the `subclassExhaustiveStrategy` to `RUNTIME_EXCEPTION`, you can do this at the `@MapperConfig`, `@Mapper` or `@BeanMapping` annotations. If you then pass a `GrapeDto` an `IllegalArgumentException` will be thrown because it is unknown how to map a `GrapeDto`.
|
||
Adding the missing (`@SubclassMapping`) for it will fix that.
|
||
|
||
[TIP]
|
||
====
|
||
If the mapping method for the subclasses does not exist it will be created and any other annotations on the fruit mapping method will be inherited by the newly generated mappings.
|
||
====
|
||
|
||
[NOTE]
|
||
====
|
||
Combining `@SubclassMapping` with update methods is not supported.
|
||
If you try to use subclass mappings there will be a compile error.
|
||
The same issue exists for the `@Context` and `@TargetType` parameters.
|
||
====
|
||
|
||
[[determining-result-type]]
|
||
=== Determining the result type
|
||
|
||
When result types have an inheritance relation, selecting either mapping method (`@Mapping`) or a factory method (`@BeanMapping`) can become ambiguous. Suppose an Apple and a Banana, which are both specializations of Fruit.
|
||
|
||
.Specifying the result type of a bean mapping method
|
||
====
|
||
[source, java, linenums]
|
||
[subs="verbatim,attributes"]
|
||
----
|
||
@Mapper( uses = FruitFactory.class )
|
||
public interface FruitMapper {
|
||
|
||
@BeanMapping( resultType = Apple.class )
|
||
Fruit map( FruitDto source );
|
||
|
||
}
|
||
----
|
||
[source, java, linenums]
|
||
[subs="verbatim,attributes"]
|
||
----
|
||
public class FruitFactory {
|
||
|
||
public Apple createApple() {
|
||
return new Apple( "Apple" );
|
||
}
|
||
|
||
public Banana createBanana() {
|
||
return new Banana( "Banana" );
|
||
}
|
||
}
|
||
----
|
||
====
|
||
|
||
So, which `Fruit` must be factorized in the mapping method `Fruit map(FruitDto source);`? A `Banana` or an `Apple`? Here's where the `@BeanMapping#resultType` comes in handy. It controls the factory method to select, or in absence of a factory method, the return type to create.
|
||
|
||
[TIP]
|
||
====
|
||
The same mechanism is present on mapping: `@Mapping#resultType` and works like you expect it would: it selects the mapping method with the desired result type when present.
|
||
====
|
||
|
||
[TIP]
|
||
====
|
||
The mechanism is also present on iterable mapping and map mapping. `@IterableMapping#elementTargetType` is used to select the mapping method with the desired element in the resulting `Iterable`. For the `@MapMapping` a similar purpose is served by means of `#MapMapping#keyTargetType` and `MapMapping#valueTargetType`.
|
||
====
|
||
|
||
[[mapping-result-for-null-arguments]]
|
||
=== Controlling mapping result for 'null' arguments
|
||
|
||
MapStruct offers control over the object to create when the source argument of the mapping method equals `null`. By default `null` will be returned.
|
||
|
||
However, by specifying `nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT` on `@BeanMapping`, `@IterableMapping`, `@MapMapping`, or globally on `@Mapper` or `@MapperConfig`, the mapping result can be altered to return empty *default* values. This means for:
|
||
|
||
* *Bean mappings*: an 'empty' target bean will be returned, with the exception of constants and expressions, they will be populated when present.
|
||
* *Iterables / Arrays*: an empty iterable will be returned.
|
||
* *Maps*: an empty map will be returned.
|
||
|
||
The strategy works in a hierarchical fashion. Setting `nullValueMappingStrategy` on mapping method level will override `@Mapper#nullValueMappingStrategy`, and `@Mapper#nullValueMappingStrategy` will override `@MapperConfig#nullValueMappingStrategy`.
|
||
|
||
[[mapping-result-for-null-collection-or-map-arguments]]
|
||
=== Controlling mapping result for 'null' collection or map arguments
|
||
|
||
With <<mapping-result-for-null-arguments>> it is possible to control how the return type should be constructed when the source argument of the mapping method is `null`.
|
||
That is applied for all mapping methods (bean, iterable or map mapping methods).
|
||
|
||
However, MapStruct also offers a more dedicated way to control how collections / maps should be mapped.
|
||
e.g. return default (empty) collections / maps, but return `null` for beans.
|
||
|
||
For collections (iterables) this can be controlled through:
|
||
|
||
* `MapperConfig#nullValueIterableMappingStrategy`
|
||
* `Mapper#nullValueIterableMappingStrategy`
|
||
* `IterableMapping#nullValueMappingStrategy`
|
||
|
||
For maps this can be controlled through:
|
||
|
||
* `MapperConfig#nullValueMapMappingStrategy`
|
||
* `Mapper#nullValueMapMappingStrategy`
|
||
* `MapMapping#nullValueMappingStrategy`
|
||
|
||
How the value of the `NullValueMappingStrategy` is applied is the same as in <<mapping-result-for-null-arguments>>
|
||
|
||
|
||
[[mapping-result-for-null-properties]]
|
||
=== Controlling mapping result for 'null' properties in bean mappings (update mapping methods only).
|
||
|
||
MapStruct offers control over the property to set in an `@MappingTarget` annotated target bean when the source property equals `null` or the presence check method results in 'absent'.
|
||
|
||
By default the target property will be set to null.
|
||
|
||
However:
|
||
|
||
1. By specifying `nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT` on `@Mapping`, `@BeanMapping`, `@Mapper` or `@MapperConfig`, the mapping result can be altered to return *default* values.
|
||
For `List` MapStruct generates an `ArrayList`, for `Map` a `LinkedHashMap`, for arrays an empty array, for `String` `""` and for primitive / boxed types a representation of `false` or `0`.
|
||
For all other objects an new instance is created. Please note that a default constructor is required. If not available, use the `@Mapping#defaultValue`.
|
||
|
||
2. By specifying `nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE` on `@Mapping`, `@BeanMapping`, `@Mapper` or `@MapperConfig`, the mapping result will be equal to the original value of the `@MappingTarget` annotated target.
|
||
|
||
The strategy works in a hierarchical fashion. Setting `nullValuePropertyMappingStrategy` on mapping method level will override `@Mapper#nullValuePropertyMappingStrategy`, and `@Mapper#nullValuePropertyMappingStrategy` will override `@MapperConfig#nullValuePropertyMappingStrategy`.
|
||
|
||
[NOTE]
|
||
====
|
||
Some types of mappings (collections, maps), in which MapStruct is instructed to use a getter or adder as target accessor (see `CollectionMappingStrategy`), MapStruct will always generate a source property
|
||
null check, regardless of the value of the `NullValuePropertyMappingStrategy`, to avoid addition of `null` to the target collection or map. Since the target is assumed to be initialised this strategy will not be applied.
|
||
====
|
||
|
||
[TIP]
|
||
====
|
||
`NullValuePropertyMappingStrategy` also applies when the presence checker returns `not present`.
|
||
====
|
||
|
||
[[checking-source-property-for-null-arguments]]
|
||
=== Controlling checking result for 'null' properties in bean mapping
|
||
|
||
MapStruct offers control over when to generate a `null` check. By default (`nullValueCheckStrategy = NullValueCheckStrategy.ON_IMPLICIT_CONVERSION`) a `null` check will be generated for:
|
||
|
||
* direct setting of source value to target value when target is primitive and source is not.
|
||
* applying type conversion and then:
|
||
.. calling the setter on the target.
|
||
.. calling another type conversion and subsequently calling the setter on the target.
|
||
.. calling a mapping method and subsequently calling the setter on the target.
|
||
|
||
First calling a mapping method on the source property is not protected by a null check. Therefore generated mapping methods will do a null check prior to carrying out mapping on a source property. Handwritten mapping methods must take care of null value checking. They have the possibility to add 'meaning' to `null`. For instance: mapping `null` to a default value.
|
||
|
||
The option `nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS` will always include a null check when source is non primitive, unless a source presence checker is defined on the source bean.
|
||
|
||
The strategy works in a hierarchical fashion. `@Mapping#nullValueCheckStrategy` will override `@BeanMapping#nullValueCheckStrategy`, `@BeanMapping#nullValueCheckStrategy` will override `@Mapper#nullValueCheckStrategy` and `@Mapper#nullValueCheckStrategy` will override `@MaperConfig#nullValueCheckStrategy`.
|
||
|
||
[[source-presence-check]]
|
||
=== Source presence checking
|
||
Some frameworks generate bean properties that have a source presence checker. Often this is in the form of a method `hasXYZ`, `XYZ` being a property on the source bean in a bean mapping method. MapStruct will call this `hasXYZ` instead of performing a `null` check when it finds such `hasXYZ` method.
|
||
|
||
[TIP]
|
||
====
|
||
The source presence checker name can be changed in the MapStruct service provider interface (SPI). It can also be deactivated in this way.
|
||
====
|
||
|
||
[NOTE]
|
||
====
|
||
Some types of mappings (collections, maps), in which MapStruct is instructed to use a getter or adder as target accessor (see `CollectionMappingStrategy`), MapStruct will always generate a source property
|
||
null check, regardless the value of the `NullValueCheckStrategy` to avoid addition of `null` to the target collection or map.
|
||
====
|
||
|
||
[[conditional-mapping]]
|
||
=== Conditional Mapping
|
||
|
||
Conditional Mapping is a type of <<source-presence-check>>.
|
||
The difference is that it allows users to write custom condition methods that will be invoked to check if a property needs to be mapped or not.
|
||
|
||
A custom condition method is a method that is annotated with `org.mapstruct.Condition` and returns `boolean`.
|
||
|
||
e.g. if you only want to map a String property when it is not `null, and it is not empty then you can do something like:
|
||
|
||
.Mapper using custom condition check method
|
||
====
|
||
[source, java, linenums]
|
||
[subs="verbatim,attributes"]
|
||
----
|
||
@Mapper
|
||
public interface CarMapper {
|
||
|
||
CarDto carToCarDto(Car car);
|
||
|
||
@Condition
|
||
default boolean isNotEmpty(String value) {
|
||
return value != null && !value.isEmpty();
|
||
}
|
||
}
|
||
----
|
||
====
|
||
|
||
The generated mapper will look like:
|
||
|
||
.Custom condition check in generated implementation
|
||
====
|
||
[source, java, linenums]
|
||
[subs="verbatim,attributes"]
|
||
----
|
||
// GENERATED CODE
|
||
public class CarMapperImpl implements CarMapper {
|
||
|
||
@Override
|
||
public CarDto carToCarDto(Car car) {
|
||
if ( car == null ) {
|
||
return null;
|
||
}
|
||
|
||
CarDto carDto = new CarDto();
|
||
|
||
if ( isNotEmpty( car.getOwner() ) ) {
|
||
carDto.setOwner( car.getOwner() );
|
||
}
|
||
|
||
// Mapping of other properties
|
||
|
||
return carDto;
|
||
}
|
||
}
|
||
----
|
||
====
|
||
|
||
When using this in combination with an update mapping method it will replace the `null-check` there, for example:
|
||
|
||
.Update mapper using custom condition check method
|
||
====
|
||
[source, java, linenums]
|
||
[subs="verbatim,attributes"]
|
||
----
|
||
@Mapper
|
||
public interface CarMapper {
|
||
|
||
CarDto carToCarDto(Car car, @MappingTarget CarDto carDto);
|
||
|
||
@Condition
|
||
default boolean isNotEmpty(String value) {
|
||
return value != null && !value.isEmpty();
|
||
}
|
||
}
|
||
----
|
||
====
|
||
|
||
The generated update mapper will look like:
|
||
|
||
.Custom condition check in generated implementation
|
||
====
|
||
[source, java, linenums]
|
||
[subs="verbatim,attributes"]
|
||
----
|
||
// GENERATED CODE
|
||
public class CarMapperImpl implements CarMapper {
|
||
|
||
@Override
|
||
public CarDto carToCarDto(Car car, CarDto carDto) {
|
||
if ( car == null ) {
|
||
return carDto;
|
||
}
|
||
|
||
if ( isNotEmpty( car.getOwner() ) ) {
|
||
carDto.setOwner( car.getOwner() );
|
||
} else {
|
||
carDto.setOwner( null );
|
||
}
|
||
|
||
// Mapping of other properties
|
||
|
||
return carDto;
|
||
}
|
||
}
|
||
----
|
||
====
|
||
|
||
[IMPORTANT]
|
||
====
|
||
If there is a custom `@Condition` method applicable for the property it will have a precedence over a presence check method in the bean itself.
|
||
====
|
||
|
||
[NOTE]
|
||
====
|
||
Methods annotated with `@Condition` in addition to the value of the source property can also have the source parameter as an input.
|
||
====
|
||
|
||
<<selection-based-on-qualifiers>> is also valid for `@Condition` methods.
|
||
In order to use a more specific condition method you will need to use one of `Mapping#conditionQualifiedByName` or `Mapping#conditionQualifiedBy`.
|
||
|
||
[[exceptions]]
|
||
=== Exceptions
|
||
|
||
Calling applications may require handling of exceptions when calling a mapping method. These exceptions could be thrown by hand-written logic and by the generated built-in mapping methods or type-conversions of MapStruct. When the calling application requires handling of exceptions, a throws clause can be defined in the mapping method:
|
||
|
||
.Mapper using custom method declaring checked exception
|
||
====
|
||
[source, java, linenums]
|
||
[subs="verbatim,attributes"]
|
||
----
|
||
@Mapper(uses = HandWritten.class)
|
||
public interface CarMapper {
|
||
|
||
CarDto carToCarDto(Car car) throws GearException;
|
||
}
|
||
----
|
||
====
|
||
|
||
The hand written logic might look like this:
|
||
|
||
.Custom mapping method declaring checked exception
|
||
====
|
||
[source, java, linenums]
|
||
[subs="verbatim,attributes"]
|
||
----
|
||
public class HandWritten {
|
||
|
||
private static final String[] GEAR = {"ONE", "TWO", "THREE", "OVERDRIVE", "REVERSE"};
|
||
|
||
public String toGear(Integer gear) throws GearException, FatalException {
|
||
if ( gear == null ) {
|
||
throw new FatalException("null is not a valid gear");
|
||
}
|
||
|
||
if ( gear < 0 && gear > GEAR.length ) {
|
||
throw new GearException("invalid gear");
|
||
}
|
||
return GEAR[gear];
|
||
}
|
||
}
|
||
----
|
||
====
|
||
|
||
MapStruct now, wraps the `FatalException` in a `try-catch` block and rethrows an unchecked `RuntimeException`. MapStruct delegates handling of the `GearException` to the application logic because it is defined as throws clause in the `carToCarDto` method:
|
||
|
||
.try-catch block in generated implementation
|
||
====
|
||
[source, java, linenums]
|
||
[subs="verbatim,attributes"]
|
||
----
|
||
// GENERATED CODE
|
||
@Override
|
||
public CarDto carToCarDto(Car car) throws GearException {
|
||
if ( car == null ) {
|
||
return null;
|
||
}
|
||
|
||
CarDto carDto = new CarDto();
|
||
try {
|
||
carDto.setGear( handWritten.toGear( car.getGear() ) );
|
||
}
|
||
catch ( FatalException e ) {
|
||
throw new RuntimeException( e );
|
||
}
|
||
|
||
return carDto;
|
||
}
|
||
----
|
||
====
|
||
|
||
Some **notes** on null checks. MapStruct does provide null checking only when required: when applying type-conversions or constructing a new type by invoking its constructor. This means that the user is responsible in hand-written code for returning valid non-null objects. Also null objects can be handed to hand-written code, since MapStruct does not want to make assumptions on the meaning assigned by the user to a null object. Hand-written code has to deal with this.
|