Before we dive into the deep end, let's quickly recap what MapStruct is all about. It's a code generator that creates type-safe bean mappers at compile time. No runtime overhead, no reflection magic - just pure, efficient Java code. Here's a taste of what a basic mapper looks like:
@Mapper
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
UserDTO mapToDTO(User user);
}
Simple, right? But oh boy, we're just getting started!
The Top 10 MapStruct Tricks You Never Knew You Needed
1. Mapping with Context: The Secret Sauce
Ever needed to pass extra info to your mapper? Enter the @Context annotation:
@Mapper
public interface UserMapper {
@Mapping(target = "role", source = "user")
UserDTO mapToDTO(User user, @Context SecurityContext securityContext);
default String mapRole(User user, @Context SecurityContext securityContext) {
return securityContext.isAdmin() ? "ADMIN" : user.getRole();
}
}
Now you can inject runtime context into your mapping logic. Neat, huh?
2. Custom Method Mappings: When Standard Just Won't Cut It
Sometimes, you need to roll up your sleeves and write custom mapping logic:
@Mapper
public interface DateMapper {
@Mapping(target = "formattedDate", source = "date", qualifiedByName = "formatDate")
DateDTO map(DateEntity date);
@Named("formatDate")
default String formatDate(LocalDate date) {
return date != null ? date.format(DateTimeFormatter.ISO_LOCAL_DATE) : null;
}
}
Pro tip: Use @Named to keep your custom methods organized and reusable across different mappers.
3. Collection Mapping: Because One Is Never Enough
MapStruct handles collections like a champ:
@Mapper
public interface ProductMapper {
List mapToDTOList(List products);
Set mapToProductNames(List products);
}
It automatically applies the element mapping to each item in the collection. Magic!
4. Nested Object Mapping: Diving Deep
Dealing with complex object graphs? MapStruct's got your back:
@Mapper
public interface OrderMapper {
@Mapping(target = "customerName", source = "customer.name")
@Mapping(target = "orderItems", source = "items")
OrderDTO mapToDTO(Order order);
@Mapping(target = "productName", source = "product.name")
OrderItemDTO mapToItemDTO(OrderItem item);
}
It seamlessly handles nested objects, saving you from the nightmare of manual mapping.
5. Conditional Mapping: The Art of Decision Making
Sometimes, you need to make mapping decisions on the fly:
@Mapper
public interface UserMapper {
@Mapping(target = "status", expression = "java(mapStatus(user))")
UserDTO mapToDTO(User user);
default String mapStatus(User user) {
return user.getLastLoginDate() != null &&
user.getLastLoginDate().isAfter(LocalDate.now().minusDays(30))
? "ACTIVE" : "INACTIVE";
}
}
Use expressions to inject complex logic into your mappings.
6. Ignoring Fields: The Art of Selective Mapping
Sometimes, less is more. Ignore fields you don't need:
@Mapper
public interface UserMapper {
@Mapping(target = "password", ignore = true)
@Mapping(target = "createdAt", ignore = true)
UserDTO mapToDTO(User user);
}
Perfect for when you're dealing with sensitive data or fields that don't belong in your DTO.
7. Reverse Mapping: Going Both Ways
Need to map back and forth? MapStruct's got you covered:
@Mapper
public interface UserMapper {
@Mapping(target = "id", ignore = true)
User mapToEntity(UserDTO dto);
UserDTO mapToDTO(User user);
}
Just define both methods, and MapStruct will handle the bidirectional mapping for you.
8. Default Values: Filling in the Blanks
Sometimes, you need to provide default values for missing data:
@Mapper
public interface ProductMapper {
@Mapping(target = "inStock", constant = "true")
@Mapping(target = "category", defaultValue = "UNCATEGORIZED")
ProductDTO mapToDTO(Product product);
}
Use constant for hardcoded values and defaultValue for fallbacks when the source is null.
9. Before and After Mapping: The Final Touch
Need to perform some operations before or after the mapping? Say hello to @BeforeMapping and @AfterMapping:
@Mapper
public abstract class OrderMapper {
@Mapping(target = "total", ignore = true)
public abstract OrderDTO mapToDTO(Order order);
@AfterMapping
protected void calculateTotal(Order order, @MappingTarget OrderDTO orderDTO) {
orderDTO.setTotal(order.getItems().stream()
.mapToDouble(item -> item.getPrice() * item.getQuantity())
.sum());
}
}
Perfect for complex calculations or last-minute adjustments.
10. Custom MapStruct Annotations: The Power of Abstraction
Create your own annotations to encapsulate common mapping patterns:
@Retention(RetentionPolicy.CLASS)
@Mapping(target = "id", ignore = true)
@Mapping(target = "createdAt", expression = "java(java.time.LocalDateTime.now())")
public @interface ToEntity { }
@Mapper
public interface UserMapper {
@ToEntity
User mapToEntity(UserDTO dto);
}
This approach helps keep your mappers clean and promotes reusability across your codebase.
Wrapping Up: The MapStruct Magic Unleashed
There you have it - 10 MapStruct tricks that'll take your Quarkus mapping game to the next level. From context-aware mappings to custom annotations, MapStruct is packed with features that can simplify even the most complex object transformations.
Remember, the key to mastering MapStruct is experimentation. Don't be afraid to dive into the documentation and try out different combinations of these techniques. Your future self (and your code reviewers) will thank you for the clean, efficient, and maintainable mapping code you'll produce.
So, the next time you find yourself reaching for that manual setter soup, take a step back and ask yourself: "What would MapStruct do?" Chances are, it's got a trick up its sleeve that'll make your life a whole lot easier.
Happy mapping, Quarkus ninjas! 🚀
"The art of mapping is not in creating new objects, but in revealing the relationships between existing ones." - Anonymous Developer (probably)
P.S. Don't forget to star the MapStruct repository on GitHub if you find it useful. Open source projects thrive on community support!