mapstruct について
Java 開発者向けの MapStruct 参照スキルで、DTO とエンティティ間のマッピング、更新方法、よくある問題を扱います。Spring や Lombok との連携、マッピングの実践方法も紹介します。
主な機能
- 01オブジェクトとコレクションのマッピング
- 02フィールドとネストしたオブジェクトのマッピング
- 03部分更新と全件更新
- 04列挙型と条件付きマッピング
- 05マッピング前後のカスタム処理
- 06設定とトラブルシューティング
活用シーン
- 01エンティティをレスポンス DTO に変換
- 02DTO から既存エンティティを更新
- 03異なるフィールド名やネスト型をマッピング
MCP 対応
`mcp__documentation__fetch_docs` で MapStruct の詳細ドキュメントを取得できると記載されています。
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill mapstructインストール後、Claude Code で/mapstructを入力して読み込まれたか確認します。
詳細
MapStruct オブジェクトマッピング
詳細な知識:包括的なドキュメントについては、technology を
mapstructに設定してmcp__documentation__fetch_docsを使用してください。
基本的なマッパー
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UserMapper {
UserResponse toResponse(User user);
List<UserResponse> toResponseList(List<User> users);
@Mapping(target = "id", ignore = true)
@Mapping(target = "createdAt", ignore = true)
@Mapping(target = "updatedAt", ignore = true)
@Mapping(target = "password", ignore = true)
User toEntity(CreateUserRequest dto);
}
マッピングの更新
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UserMapper {
// Partial update - ignores null values
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateEntity(UpdateUserRequest dto, @MappingTarget User user);
// Full update - sets all fields including nulls
void updateEntityFull(UpdateUserRequest dto, @MappingTarget User user);
}
フィールドマッピング
@Mapper(componentModel = "spring")
public interface OrderMapper {
// Different field names
@Mapping(source = "customer.name", target = "customerName")
@Mapping(source = "customer.email", target = "customerEmail")
@Mapping(source = "items", target = "orderItems")
OrderResponse toResponse(Order order);
// Constant values
@Mapping(target = "status", constant = "PENDING")
@Mapping(target = "createdAt", expression = "java(java.time.LocalDateTime.now())")
Order toEntity(CreateOrderRequest dto);
// Expression
@Mapping(target = "fullName", expression = "java(user.getFirstName() + \" \" + user.getLastName())")
UserResponse toResponse(User user);
// Date formatting
@Mapping(source = "createdAt", target = "createdDate", dateFormat = "yyyy-MM-dd")
OrderResponse toResponse(Order order);
}
ネストされたオブジェクト
@Mapper(componentModel = "spring", uses = {AddressMapper.class, ItemMapper.class})
public interface OrderMapper {
// Uses AddressMapper for address field
// Uses ItemMapper for items collection
OrderResponse toResponse(Order order);
}
@Mapper(componentModel = "spring")
public interface AddressMapper {
AddressResponse toResponse(Address address);
}
@Mapper(componentModel = "spring")
public interface ItemMapper {
ItemResponse toResponse(Item item);
}
Enum マッピング
@Mapper(componentModel = "spring")
public interface StatusMapper {
@ValueMappings({
@ValueMapping(source = "ACTIVE", target = "ENABLED"),
@ValueMapping(source = "INACTIVE", target = "DISABLED"),
@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "UNKNOWN")
})
ExternalStatus toExternalStatus(InternalStatus status);
}
カスタムメソッド
@Mapper(componentModel = "spring")
public abstract class UserMapper {
@Autowired
protected RoleRepository roleRepository;
public abstract UserResponse toResponse(User user);
@Mapping(target = "roles", source = "roleIds")
public abstract User toEntity(CreateUserRequest dto);
// Custom mapping method
protected Set<Role> mapRoles(Set<Long> roleIds) {
if (roleIds == null) return new HashSet<>();
return roleIds.stream()
.map(id -> roleRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Role", "id", id)))
.collect(Collectors.toSet());
}
}
コレクションマッピング
@Mapper(componentModel = "spring")
public interface ProductMapper {
ProductResponse toResponse(Product product);
List<ProductResponse> toResponseList(List<Product> products);
Set<ProductResponse> toResponseSet(Set<Product> products);
// Page mapping
default Page<ProductResponse> toResponsePage(Page<Product> products) {
return products.map(this::toResponse);
}
}
マッピング前後の処理
@Mapper(componentModel = "spring")
public abstract class UserMapper {
@AfterMapping
protected void afterMapping(@MappingTarget UserResponse response, User user) {
response.setDisplayName(user.getFirstName() + " " + user.getLastName().charAt(0) + ".");
}
@BeforeMapping
protected void beforeMapping(CreateUserRequest dto) {
if (dto.getEmail() != null) {
dto.setEmail(dto.getEmail().toLowerCase().trim());
}
}
}
条件付きマッピング
@Mapper(componentModel = "spring")
public abstract class UserMapper {
@Condition
public boolean isNotEmpty(String value) {
return value != null && !value.trim().isEmpty();
}
// Only maps non-empty strings
UserResponse toResponse(User user);
}
Maven の設定
<properties>
<mapstruct.version>1.6.2</mapstruct.version>
<lombok.version>1.18.30</lombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Amapstruct.defaultComponentModel=spring</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
主なアノテーション
| アノテーション | 目的 |
|---|---|
@Mapper | マッパーのインターフェース/抽象クラスを定義します |
@Mapping | フィールド単位のマッピング設定を行います |
@MappingTarget | 既存のオブジェクトを更新します |
@BeanMapping | Bean 単位のマッピング設定を行います |
@AfterMapping | マッピング後の処理を行うメソッドです |
@BeforeMapping | マッピング前の処理を行うメソッドです |
@Condition | 条件付きマッピングを行います |
@ValueMapping | Enum 値をマッピングします |
ベストプラクティス
- Spring のインジェクションには
componentModel = "spring"を使用します - フィールド数の少ない DTO には
unmappedTargetPolicy = ReportingPolicy.IGNOREを使用します - DTO からマッピングする場合は、必ず
id、createdAt、updatedAtを除外します - 部分更新には
@MappingTargetを使用します - アノテーションプロセッサーは次の順序にします:Lombok → MapStruct → Binding
- カスタムロジックにはインターフェースではなく抽象クラスを使用します
- 条件付きマッピングのロジックには
@Conditionを使用します
このスキルを使用しない場合
| シナリオ | 代わりに使用するもの |
|---|---|
| Java 言語機能 | java スキル |
| Lombok アノテーション | lombok スキル |
| Spring の設定 | backend-spring-boot スキル |
| JPA エンティティの操作 | JPA 専用スキル |
| 単純なコピー | 手動マッピングまたは BeanUtils |
アンチパターン
| アンチパターン | 問題点 | 適切な方法 |
|---|---|---|
| エンティティからエンティティへのマッピング | 変更追跡が機能しなくなります | DTO からエンティティへマッピングします |
@MappingTarget を使用しない | 更新が非効率になります | 部分更新に使用します |
| 式に複雑なロジックを記述する | テストが困難になります | カスタムメソッドを使用します |
| マッピングされない項目をすべて無視する | フィールドが気付かれずに見落とされます | WARN または ERROR ポリシーを使用します |
| 監査フィールドを除外しない | メタデータが上書きされます | id とタイムスタンプを除外します |
| 循環参照 | StackOverflowError が発生します | カスタムマッピングで循環を断ち切ります |
| カスタムロジックにインターフェースを使用する | 依存関係を注入できません | 抽象クラスを使用します |
| マッパーをテストしない | 実行時にマッピングエラーが発生します | マッパーのテストを作成します |
よくある問題のトラブルシューティング
| 問題 | 原因 | 解決策 |
|---|---|---|
| "Cannot find implementation" | アノテーション処理に失敗しました | プロセッサーの設定を確認します |
| Lombok のフィールドが見つからない | プロセッサーの順序が誤っています | MapStruct より前に Lombok を実行します |
| マッパーが autowire されない | componentModel が誤っています | componentModel = "spring" を使用します |
| 循環依存 | マッパー同士が相互参照しています | @Lazy を使用するか、設計を見直します |
| UnmappedTargetProperty 警告 | マッピングが不足しています | @Mapping を追加するか、ignore ポリシーを設定します |
| マッピング中に NullPointerException が発生する | ソースが null です | null チェックまたは NullValuePropertyMappingStrategy を追加します |
| カスタムメソッドが呼び出されない | シグネチャが誤っています | メソッドのパラメーターを正確に一致させます |
| 生成されたコードが更新されない | IDE のキャッシュ | プロジェクトをクリーンして再ビルドします |
リファレンスドキュメント
詳細な知識:包括的なドキュメントについては、technology を
mapstructに設定してmcp__documentation__fetch_docsを使用してください。
コメント
Markdown 対応