author-avatar
Dominik Martyniak
Java 10 minut

MapStruct w Springu: Skuteczne Mapowanie Obiektów

Czy kiedykolwiek zastanawialiście się, jak zoptymalizować mapowanie obiektów w waszym kodzie? Jeśli tak, to świetnie trafiłeś! Dzisiejszy wpis dotyczyć będzie jednej z najbardziej zaawansowanej i wygodnej biblioteki do mapowania obiektów w świecie Javy - MapStruct.

MapStruct to narzędzie, które znacząco ułatwia proces mapowania pomiędzy obiektami, eliminując potrzebę pisania rutynowego, powtarzalnego kodu. Jednakże, w przeciwieństwie do niektórych innych rozwiązań, MapStruct jest oparty na adnotacjach, co pozwala programistom zachować pełną kontrolę nad procesem mapowania, jednocześnie ograniczając ilość kodu do napisania.

Dlaczego Warto Używać MapStruct część 1 

 

  • Wydajność: MapStruct generuje kod mapujący na etapie kompilacji, co oznacza, że nie ponosimy dodatkowego obciążenia w trakcie działania aplikacji. Jest to szczególnie ważne w przypadku dużych projektów, gdzie efektywne mapowanie obiektów może wpłynąć na ogólną wydajność.
  • Prostota Użycia: Dzięki prostocie MapStructa, programiści mogą szybko nauczyć się go używać i zacząć korzystać z jego zalet bez dużego nakładu pracy.
  • Konfigurowalność: MapStruct pozwala na dostosowanie mapowania obiektów do indywidualnych potrzeb poprzez konfigurację adnotacji i strategii mapowania.

 

Zależności


Aby używać MapStruct w projekcie Gradle, musisz dodać odpowiednie zależności do pliku build.gradle. Oto przykładowa konfiguracja:

 

dependencies {
    implementation 'org.mapstruct:mapstruct:1.5.5.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
}

annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.2.Final' dodaje zależność procesora adnotacji MapStruct, który jest niezbędny do generowania kodu mapowania podczas kompilacji.



Lets code



Słynne hasło "Encja na twarz i pcharz" autorstwa Pawła Szulca z Confitury doskonale oddaje istotę naszego podejścia. Nie pragniemy, aby nasza warstwa bazodanowa, bogata w różnorodne informacje, wychodziła na świat bez odpowiedniego przygotowania. W tym miejscu idealnie sprawdza się użycie modelu DTO (Data Transfer Object) wraz z wykorzystaniem MapStruct.

 

Przyjmując, że mamy już zdefiniowaną encję użytkownika (User) w postaci SQLUser, warto zwrócić uwagę na pewne pola, takie jak email czy password, które nie powinny być przesyłane na wyższe warstwy aplikacji, na przykład w przypadku odczytu przez REST API. Dlatego też warto stworzyć odpowiednie DTO dla naszego użytkownika. Poniżej znajdziesz przykładowy kod DTO dla użytkownika:

 

@Data
@AllArgsConstructor
public class UserDto {
    private String id;
    private String name;
    private String avatar;
    private String description;
}

 

Korzystając z tego DTO, możemy bezpiecznie przekazywać tylko istotne informacje na wyższe warstwy aplikacji, eliminując konieczność przekazywania wrażliwych danych, takich jak email czy hasło. To podejście pomaga utrzymać bezpieczeństwo i prywatność użytkowników, jednocześnie zapewniając niezbędne informacje w bardziej zabezpieczony sposób.

 

Tworzenie Interfejsów Mapperów

 

Zdefiniuj interfejsy mapujące, które będą używane do konwersji obiektów.

 

@Mapper(componentModel = "spring")
public interface UserMapper {
    UserDto mapFromDomain(User user);
}

 

W tym fragmencie kodu definiujemy interfejs mapujący o nazwie UserMapper. Jest to często stosowane podejście przy korzystaniu z MapStruct w kontekście Springa. Adnotacja @Mapper(componentModel = "spring") oznacza, że MapStruct wygeneruje implementację tego interfejsu z wykorzystaniem mechanizmów zarządzania komponentami dostarczanych przez Spring Framework. Dzięki czemu będziemy mogli wstrzyknąć zależność do komponentów.
Wnętrze interfejsu zawiera jedną metodę mapFromDomain, która będzie odpowiedzialna za mapowanie obiektu klasy User na obiekt klasy UserDto. 

 

Warto zauważyć, że MapStruct automatycznie generuje implementację tej metody w trakcie kompilacji, co eliminuje potrzebę ręcznego pisania rutynowego kodu mapującego. To podejście pozwala skupić się na logice biznesowej, zamiast tracić czas na niuanse mapowania obiektów. 

A jak wygląda wygenerowana implementacja mapowania:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-11-09T14:19:15+0100",
    comments = "version: 1.5.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.5.1.jar, environment: Java 17.0.4.1 (Amazon.com Inc.)"
)
@Component
public class UserMapperImpl implements UserMapper {

    @Override
    public UserDto mapFromDomain(User user) {
        if ( user == null ) {
            return null;
        }

        String id = null;
        String name = null;
        String avatar = null;
        String description = null;

        UserDto userDto = new UserDto( id, name, avatar, description );

        return userDto;
    }
}

 

Podsumowując, kod tej klasy jest rezultatem automatycznego generowania MapStruct w trakcie kompilacji, co umożliwia efektywne mapowanie obiektów zgodnie z zdefiniowanymi regułami.

 

Adnotacja @Mapping

Stwórzmy pseudo kod encji SQLPost, która posiada relacje.

 

@Entity
@Table(name = "post")
@AllArgsConstructor
@NoArgsConstructor
public class SQLPost {

    // ... 
    // ...
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id")
    private SQLUser author;
    // ... 
    // ...

}

 

W naszym MapStruct dodajemy odpowiednie mapowania:

 

@Mapper(componentModel = "spring")
public interface PostSQLMapper {

    @Mapping(target = "author", source = "user")
    @Mapping(target = "id", source = "post.id")
    @Mapping(target = "thumbnailUrl", source = "post.thumbnailUrl")
    @Mapping(target = "description", source = "post.description")
    SQLPost mapToEntity(Post post, SQLUser user);

}

 

W powyższym kodzie mamy interfejs PostSQLMapper, który definiuje mapowanie z obiektu klasy Post na obiekt klasy SQLPost. Zauważ, że używamy adnotacji @Mapping, aby precyzyjnie wskazać, które pola mają zostać zmapowane. Na przykład, target = "author", source = "user" oznacza, że pole author w SQLPost zostanie wypełnione wartością z obiektu SQLUser przekazanego z obiektu Post. 

Zauważcie, że w naszym przykładzie wskazujemy, z których pól obiektu źródłowego (source) mają być pobierane dane i przekazywane do odpowiadających im pól obiektu docelowego (target). Wykorzystujemy do tego adnotacje takie jak:

 

@Mapping(target = "thumbnailUrl", source = "post.thumbnailUrl")
@Mapping(target = "description", source = "post.description")

Podobnie możemy określić inne mapowania dla konkretnych pól.

 

Przykład użycia

 

W klasie, gdzie chcesz użyć Mappera, możesz wstrzyknąć go jako zależność Springową :

 

@Service
@AllArgsConstructor
public class PostService {

    private final PostSQLMapper postSQLMapper;

    // Metoda, która wykorzystuje postSQLMapper...
}

 

W powyższym przykładzie PostService wstrzykuje PostSQLMapper za pomocą adnotacji Konstruktora. Dzięki temu możesz używać postSQLMapper do mapowania obiektów Post na SQLPost.

 

Podsumowanie cz. 1

 

Dzięki takiemu podejściu MapStruct umożliwia elastyczne definiowanie mapowań pomiędzy różnymi obiektami, co jest szczególnie przydatne, gdy mamy do czynienia z obiektami złożonymi, np. takimi jak relacje pomiędzy encjami w bazie danych.

Podsumowując, w dzisiejszym wpisie zapoznaliśmy się z przykładem wykorzystania MapStruct do mapowania pól obiektów, zwłaszcza w kontekście tworzenia obiektu wyjściowego na bazie dwóch innych obiektów. Zauważyliśmy, że choć MapStruct potrafi automatycznie mapować pola o identycznych nazwach, czasem potrzebujemy precyzyjnej kontroli nad procesem mapowania.

Przeanalizowaliśmy przykład klasy SQLPost, reprezentującej post z relacją do klasy SQLUser, i zastosowaliśmy MapStruct, aby precyzyjnie wskazać, które pola chcemy zmapować. Takie podejście pozwala nam elastycznie dostosowywać mapowania do naszych konkretnych potrzeb.

Zapraszam do kolejnego wpisu, gdzie zagłębimy się w bardziej zaawansowane metody korzystania z MapStructa. Odkryjemy jego potencjał w obszarach, takich jak mapowanie niestandardowych typów, użycie adnotacji do bardziej skomplikowanych sytuacji oraz optymalizacje kodu. Bądźcie gotowi na kolejny etap z MapStructem!

 

3
[spring boot, java, mapstruct]

Więcej od Dominik Martyniak

Więcej artykułów