MapStruct takes the sting out of mapping entities to DTOs, and Java records make for elegant DTOs. Add Lombok to the equation, and you end up with almost pure data classes and very little boilerplate code.
Don't take my word for it. I can prove it. Have a look at GitHub repository to see how simple a Spring Boot application can be.
Short interlude: when does mapping help you?
Readers of my previous article are entitled to be confused now. After telling you that DTOs are typical enterprise bloatware, I'm writing an article about DTO and MapStruct?
Yes, I do. My general recommendation is to think twice before implementing DTOs and mappers. Don't do this just because you've always done it or everybody else does. Let alone doing it because your boss or your architect told you so. Challenge their ideas. Maybe they're right, maybe not. That's what my previous article is about. I want to make you think.
That said, even I have to admit there are reasonable use-cases for DTOs:
- You won't want to expose database ids over a REST interface. In particular, if your id is a simple number, you must not use it in a public interface. If you do, you'll read in the newspapers tomorrow that a hacker managed to read your entire database by simply enumerating the id.
- Many database tables contain technical data, such as
CREATED_ON
orDELETED
. The consumer of your REST interface isn't interested in such data, so drop it. It's just eating up precious network bandwidth. - Oh, and MapStruct - the framework this article is about - isn't limited to entities and DTO. It's versatile enough to cover a large part of a Backend-for-Frontend layer. And probably many other use-cases. When talking about entities and DTOs, remember it's merely one example out of many.
Define your DTO as a Java record
Java 17 was released in September 2021, so even in the conservative Java world, we're probably allowed to start using it now. A particularly nice feature is the record
, which is basically an immutable data class:
Java's never been simpler! These three lines are a full-blown Java class with many attractive traits:
- It's immutable. You can't modify any of its attributes.
- That, in turn, means there's a constructor covering every attribute. An
@AllArgsConstructor
in Lombok lingo. - The object has an identity. You've probably never heard about object identities before, but it's a big deal for Java framework developers. Let me put it in simple terms: the methods
equals()
,hashCode()
, andtoString()
are there. This doesn't sound like a big deal, but it will spare you a lot of headaches in the future. Ill-implementedequals()
andhashCode()
methods are a source of bugs that are very hard to hunt.
Before record
landed with Java 16, the closest approximation was Lombok's @Value
annotation. Have a look at their documentation to get an idea how much boilerplate code the equivalent plain Java class requires. Most Java projects get away with less boilerplate code, but only because they skip corners.
What about Java Bean Validations annotations?
With hackers lurking around every corner, you're well-advised to validate the parameters of your REST interface. That, in turn, requires you to annotate the attributes of your DTO. Luckily, that's every bit as easy as with traditional classes:
public record ProjectDto( @NotBlank(message = "Your project needs a name.") @Size(min = 3, max = 30) String name, @NotBlank(message = "Tell others about your project.") @Size(min = 1, max = 2000) String description) {}The only challenge is to convince your IDE to format your code nicely. For some reason, the syntax of a record
resembles a constructor instead of resembling a class. The field definitions are between the round parenthesis instead of being between the curly braces. As a consequence, IDEs tend to format your entire record as a one-liner.
Entities can't be records
Awesome. Records are the way to go. Now it's almost time to let MapStruct enter the stage. Here's the entity we want to map. I want to get you hooked, so my entity is almost identical to our DTO:
import lombok.Data; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.RequiredArgsConstructor; @Entity @Data @NoArgsConstructor @RequiredArgsConstructor public class Project { @Id @GeneratedValue private Long id; @NonNull @NotBlank(message = "Your project needs a name.") @Size(min = 3, max = 30) private String name; @NonNull @NotBlank(message = "Tell others about your project.") @Size(min = 1, max = 2000) private String description; }You'll observe the Lombok annotations. Lombok spares you a ton of boilerplate code, so go for it. Even better, if you're unhappy with Lombok for some reason, Lombok ships with its own antidote. There's no such risk as vendor lock-in.
You'll also note that I duplicated the Java Bean Validation annotations from the DTO. That's the only thing about my approach I dislike. However, I suspect every attempt to avoid this duplication results in less readable code. When duplicated code is the alternative to complexity, I prefer duplicated code. Let IntelliJ and Sonarqube complain. There's no rule without exception.
We can't use inheritance to get rid of the duplication for a simple reason: Records can't be entities, as Thorben Janssen explains.
Decorating our entity with Lombok annotations is a close second. Our entity class looks slim and slender without being undernourished.
Mapping all these attributes bidirectionally sounds like a nightmare. Granted, my example has only two attributes, but in a real-world application, that's one or two dozen attributes. Do we have an apprentice or an associate developer to do the tedious work?
Enter MapStruct
No, but we've got a magician. Watch MapStruct perform its magic:
@Mapper(componentModel = "spring") public interface ProjectMapper { ProjectDto entityToDTO(Project project); ListThat's not one, not two, but four mappings in as many lines of code!
You'll observe that our mapper isn't a class. It's an interface. If you're a Spring developer, you already know the drill. Are you familiar with Spring's CrudRepository
? That's an interface, plus a lot of Spring magic, making it an actual class with an implementation. The highlight is that Spring implements the interface for you when booting the application. I've covered CrudRepositories in depth in a pre-pandemic era.
MapStruct does something similar. The difference is it generates source code in your target
folder at compile time. That's even better than the Spring approach. You can examine the source code MapStruct generates. You can debug it. The only disadvantage is you need to rerun your Maven or Gradle scripts after modifying the DTO, the entity, or the mapper.
As promised, I want to get you hooked, so I didn't tweak the mapper. That's where the beauty is. Most of the time, MapStruct does precisely what you want it to do out of the box.
How MapStruct works
I suppose you've already guessed it. MapStruct matches attributes with identical names to each other. Easy!
The only difference between our DTO and our entity is the id
attribute. We've deliberately omitted it because of the hackers. MapStruct doesn't mind. It just maps what can be mapped.
In a nutshell, that's all I want to tell you today. You'll love MapStruct at first glance.
You'll also love Java records. They don't look much different from a Lombok @Value
class, but the record
keyword carries semantics. You know immediately it's a data class.
Combining MapStruct with records is just great. Calling a constructor of a record with dozens of attributes is ugly, and MapStruct hides this ugly part from you. You can see this ugly generated code if you insist, and you will probably see it during a debugging session. But you'll never have to write or maintain it. It's there, but it doesn't hurt.
What else is in store for you
If you're like my co-workers, you know countless corner cases exist. A simple one-to-one mapping isn't enough.
All right. I'll go the extra mile. But not without complaining. Your corner case might be a code smell. Please think twice before configuring MapStruct. I claimed mapping considered harmful" in my previous article, and I'm serious. Trivial mappings are a nuisance, something you something can't avoid. But non-trivial mapping is a slippery path. Such mappings look useful at first, but after a while, they tend to bite you.
That said, I'm ready to admit that MapStruct is incredibly versatile. I won't explain every option in this article. I prefer to keep this article short and provide some pointers instead. Here we go:
- renaming attributes
- mapping attributes with almost - but not quite - compatible types
- mapping lists and maps
- mapping nested objects
- using Java expressions in the mapping
- merge two entities to a single DTO
- mapping streams
There's even more - but I think these pointers are enough to get you hooked and started.
IDE support
There are plugins for IntelliJ plugin and Eclipse.
Bonus section
Sometimes you can get records directly from JPA without having to deal with MapStruct and entities. Thorben Janssen shows three different approaches. Read the full story over there:
@Query(""" SELECT new de.beyondjava.business.projects.ProjectDto( p.name, p.description) FROM Project p WHERE LOWER(p.name) = LOWER(:projectName) """) ListWrapping it up
If you're a long-term reader of this blog, you'll probably know that I used to hate Spring. Later, I hated Java because the ceremonious approach to doing even the simplest things was repelling. In particular, blogs and tutorials about Spring frequently puzzled me in the XML era. You had to understand Spring first to be able to read a Spring tutorial.
It was particularly awful in 2013. That's surprising because I bought a Spring book a few years earlier, and I was fascinated by how simple programming can be. When a framework becomes popular too fast, and zillions of enterprise developers contribute to it, simplicity turns into a complex mess.
Times are a-changing. First, Java evolved. Inventing annotations was a big deal. Bye, bye, XML hell!
The subsequent significant change was Spring Boot. It took a while to get to speed, but nowadays, it's a thing. Spring Boot addresses all the points I used to hate. It's about simplicity, and most of the time, it delivers.
The advent of Java 17 and MapStruct gives us even more options. Add Lombok to the equation, and you end up with straightforward Java code. My demo repository uses all the tools I'm aware of to make Java fun and simple.
As it happens, I'm also familiar with JavaScript and TypeScript. These communities are slightly anarchistic, but they've also found interesting ways to keep their applications simple. So I daresay we Java developers are not there yet, and it's hard to beat the simplicity of an Express server. But we Java developers are close.
There's no excuse to write incomprehensible applications. The technology allowing us to make it simple has landed. It's just us clinging to the old ways. Come on. We're living in a new age. Let's embrace simplicity!
Dig deeper
Tutorialspoint covers a wide range of features of MapStruct
Sourcecode of this article's demo on GitHub
Thorben Janssen on how to use records with JPA
MapStruct Examples - a GitHub repository containing two dozens of examples