MapStruct nullValuePropertyMappingStrategy
使用详解
在使用 MapStruct 进行 DTO 与实体对象映射时,经常会遇到字段为 null
时的处理问题。MapStruct 提供了 nullValuePropertyMappingStrategy
参数,用于控制 源对象字段为 null
时目标对象的映射行为。本文将详细解释三种常用策略:IGNORE
、SET_TO_NULL
、SET_TO_DEFAULT
,以及实际使用场景,并补充常见问题及解决方法。
1. 配置位置
nullValuePropertyMappingStrategy
可以在 Mapper 接口全局配置,也可以在单个方法上配置:
import org.mapstruct.Mapper;
import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers;
@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface UserUpdateDTOConverter {
UserUpdateDTOConverter INSTANCE = Mappers.getMapper(UserUpdateDTOConverter.class);
User toUser(UserUpdateDTO dto);
UserExtension toUserExtension(UserUpdateDTO dto);
}
或者针对单个方法:
import org.mapstruct.BeanMapping;
import org.mapstruct.NullValuePropertyMappingStrategy;
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_NULL)
UserExtension toUserExtension(UserUpdateDTO dto);
2. 三种策略详解
2.1 IGNORE(默认行为)
- 含义:如果源字段为
null
,忽略映射,目标字段保持原值。 - 使用场景:更新操作时,只覆盖非 null 字段,防止意外置空。
源字段 | 目标字段 | 结果字段 |
---|---|---|
null | 123 | 123 |
456 | 123 | 456 |
✅ 优点:安全,不会覆盖数据库已有值。 ❌ 缺点:无法通过 DTO 将字段置空。
2.2 SET_TO_NULL
- 含义:如果源字段为
null
,将目标字段也设置为null
。 - 使用场景:需要明确将字段置空,例如用户取消某个扩展信息。
源字段 | 目标字段 | 结果字段 |
---|---|---|
null | 123 | null |
456 | 123 | 456 |
✅ 优点:可以把 null 映射过去,覆盖数据库原值。 ❌ 缺点:更新时需要小心,不然可能误把重要字段置空。
2.3 SET_TO_DEFAULT
-
含义:如果源字段为
null
,将目标字段设置为 Java 默认值。- 对象类型 →
null
- 基本类型 → 对应默认值(如
int
→ 0,boolean
→ false)
- 对象类型 →
-
使用场景:创建新对象时,确保字段有初始值。
源字段 | 目标字段 | 结果字段 |
---|---|---|
null | 123 | 0 (int) |
null | "abc" | null |
true | false | true |
✅ 优点:确保字段有默认值。 ❌ 缺点:基本类型可能不符合业务逻辑,对对象类型效果不明显。
3. 使用建议
策略 | 适用场景 |
---|---|
IGNORE | 更新操作,保留数据库已有值 |
SET_TO_NULL | 更新操作,需要清空某些字段 |
SET_TO_DEFAULT | 新建对象,需要保证字段有默认值 |
4. 常见问题:DTO null
没有覆盖实体字段
4.1 问题现象
调用如下方法:
DeptUpdateDTOConverter.INSTANCE.updateDeptFromDTO(dto, existsDept);
期望行为:DTO 中的 null
字段会覆盖实体 existsDept
的原值。
实际情况:实体字段依然保持原值,没有被置空。
4.2 常见原因
-
字段类型不匹配
- MapStruct 只能将
null
赋值给对象类型字段,基本类型(int、long、boolean)无法被 null 覆盖。 - 解决方案:把基本类型改为包装类型(Integer、Long、Boolean)。
- MapStruct 只能将
-
Lombok @Accessors 问题
-
如果实体类使用了:
@Accessors(chain = true) @Accessors(fluent = true)
会生成链式或无
get
前缀的方法,MapStruct 默认是使用标准的getXxx
/setXxx
方法访问字段。 -
后果:访问方式不匹配,MapStruct 会忽略赋值,导致
null
无法覆盖实体字段。 -
解决方法:
-
去掉
@Accessors(fluent = true)
,保持标准 getter/setter。 -
或者在 Mapper 上禁用 builder:
@Mapper(builder = @Builder(disableBuilder = true))
-
-
-
Mapper 方法或全局配置不正确
-
确认在方法或 Mapper 上配置了:
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_NULL)
-
-
getter/setter 不标准
- MapStruct 默认通过标准 getter/setter 访问字段,如果使用 fluent 风格或 chain 风格,需参考第 2 点。
-
MapStruct 版本问题
- 旧版本可能有 bug,建议使用 1.5+ 或 1.6+。
4.3 调试示例
Dept existsDept = new Dept();
existsDept.setDeptName("研发部");
DeptUpdateDTO dto = new DeptUpdateDTO();
dto.setDeptName(null);
DeptUpdateDTOConverter.INSTANCE.updateDeptFromDTO(dto, existsDept);
System.out.println(existsDept.getDeptName()); // 应该输出 null
5. 总结
IGNORE
:安全,不覆盖 null。SET_TO_NULL
:更新时覆盖 null,可置空字段。SET_TO_DEFAULT
:保证字段有默认值,适合新建对象。
注意事项:
- 确保字段类型支持 null。
- Lombok 的
@Accessors(fluent = true)
会影响 MapStruct 的映射。 - 可通过禁用 builder 或去掉 fluent 来解决访问不匹配问题。
通过合理选择 nullValuePropertyMappingStrategy
并注意字段类型、getter/setter 规范,可以让 MapStruct 在 更新操作 和 新建操作 中都符合业务逻辑,同时避免意外覆盖或丢失数据。