MapStruct con Spring Boot: la guida completa al mapping tra DTO e Entity

Quando sviluppiamo applicazioni con Spring Boot, ci troviamo spesso a dover convertire oggetti da un tipo all'altro. In particolare, la conversione tra Entity (gli oggetti che rappresentano le tabelle del database) e DTO (Data Transfer Object, gli oggetti che usiamo per comunicare con il mondo esterno) è un'operazione che ripetiamo decine, se non centinaia di volte in un progetto.

Scrivere questo codice di mapping a mano è noioso, ripetitivo e soggetto a errori. È qui che entra in gioco MapStruct, una libreria che genera automaticamente il codice di mapping al momento della compilazione. In questo tutorial vedremo come integrarla nel nostro progetto Spring Boot, partendo dalle basi fino agli scenari più avanzati.


Perché separare Entity e DTO?

Prima di tuffarci in MapStruct, facciamo un passo indietro e chiediamoci: perché dovremmo avere oggetti separati per il database e per le API?

Immaginiamo di avere un'entità User che rappresenta un utente nel nostro database:

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String firstName;
    private String lastName;
    private String email;
    private String passwordHash;
    private LocalDateTime createdAt;
    private LocalDateTime lastLogin;
    private boolean enabled;
    
    @ManyToOne
    private Role role;
    
    @OneToMany(mappedBy = "user")
    private List<Order> orders;
    
    // getter e setter...
}

Se esponessimo direttamente questa entità attraverso le nostre API REST, avremmo diversi problemi:

  1. Sicurezza: esporremmo il campo passwordHash, che dovrebbe rimanere assolutamente privato
  2. Performance: caricheremmo sempre la lista degli ordini, anche quando non serve
  3. Accoppiamento: ogni modifica alla struttura del database si rifletterebbe automaticamente sulle API, rompendo potenzialmente i client esistenti
  4. Flessibilità: non potremmo avere rappresentazioni diverse dello stesso dato per contesti diversi

I DTO risolvono tutti questi problemi. Possiamo creare un UserDto che espone solo ciò che vogliamo:

public class UserDto {
    private Long id;
    private String fullName;  // firstName + lastName combinati
    private String email;
    private String roleName;  // solo il nome del ruolo, non l'intero oggetto
    
    // getter e setter...
}

Ma ora abbiamo un nuovo problema: dobbiamo scrivere il codice che converte da User a UserDto e viceversa. Ed è qui che MapStruct diventa il nostro migliore amico.


Cos'è MapStruct e come funziona

MapStruct è un generatore di codice che crea implementazioni di mapper type-safe al momento della compilazione. A differenza di altre librerie che usano la reflection a runtime (come ModelMapper o Dozer), MapStruct genera codice Java puro che viene compilato insieme al resto dell'applicazione.

Questo approccio offre diversi vantaggi:


Configurazione del progetto

Aggiungere le dipendenze Maven

Iniziamo configurando il nostro progetto Maven. Apriamo il file pom.xml e aggiungiamo le seguenti dipendenze e plugin:

<properties>
    <java.version>17</java.version>
    <mapstruct.version>1.5.5.Final</mapstruct.version>
    <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
</properties>

<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    
    <!-- MapStruct -->
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${mapstruct.version}</version>
    </dependency>
    
    <!-- Lombok (opzionale, ma molto comodo) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.11.0</version>
            <configuration>
                <annotationProcessorPaths>
                    <!-- MapStruct processor -->
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${mapstruct.version}</version>
                    </path>
                    <!-- Lombok processor -->
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <version>${lombok.version}</version>
                    </path>
                    <!-- Binding per far funzionare Lombok con MapStruct -->
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok-mapstruct-binding</artifactId>
                        <version>${lombok-mapstruct-binding.version}</version>
                    </path>
                </annotationProcessorPaths>
                <compilerArgs>
                    <arg>-Amapstruct.defaultComponentModel=spring</arg>
                </compilerArgs>
            </configuration>
        </plugin>
    </plugins>
</build>

Nota importante: l'ordine degli annotation processor è fondamentale! Lombok deve essere processato prima di MapStruct, altrimenti MapStruct non vedrà i getter e setter generati da Lombok.

L'argomento -Amapstruct.defaultComponentModel=spring dice a MapStruct di generare i mapper come bean Spring, così potremo iniettarli con @Autowired.

Configurazione con Gradle

Se preferite Gradle, ecco la configurazione equivalente per il file build.gradle:

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.0'
    id 'io.spring.dependency-management' version '1.1.4'
}

ext {
    mapstructVersion = '1.5.5.Final'
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    
    implementation "org.mapstruct:mapstruct:${mapstructVersion}"
    
    compileOnly 'org.projectlombok:lombok'
    
    annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
    annotationProcessor 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
}

compileJava {
    options.compilerArgs += ['-Amapstruct.defaultComponentModel=spring']
}

Il primo mapper: da Entity a DTO

Ora che abbiamo configurato il progetto, creiamo il nostro primo mapper. Partiamo con un esempio semplice: un'entità Product e il suo DTO.

L'Entity

@Entity
@Table(name = "products")
@Data  // Lombok genera getter, setter, equals, hashCode e toString
@NoArgsConstructor
@AllArgsConstructor
public class Product {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    private String description;
    
    private BigDecimal price;
    
    private Integer stockQuantity;
    
    private LocalDateTime createdAt;
    
    private LocalDateTime updatedAt;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id")
    private Category category;
}

Il DTO

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ProductDto {
    
    private Long id;
    
    private String name;
    
    private String description;
    
    private BigDecimal price;
    
    private Integer stockQuantity;
}

Il Mapper

Ed ecco il mapper che converte tra i due:

@Mapper
public interface ProductMapper {
    
    ProductDto toDto(Product product);
    
    Product toEntity(ProductDto dto);
    
    List<ProductDto> toDtoList(List<Product> products);
}

Questo è tutto! MapStruct genererà automaticamente l'implementazione durante la compilazione. Non dobbiamo scrivere una sola riga di codice di conversione.

Dopo la compilazione (mvn compile o gradle build), MapStruct genera una classe ProductMapperImpl che assomiglia a questa:

@Component
public class ProductMapperImpl implements ProductMapper {
    
    @Override
    public ProductDto toDto(Product product) {
        if (product == null) {
            return null;
        }
        
        ProductDto productDto = new ProductDto();
        productDto.setId(product.getId());
        productDto.setName(product.getName());
        productDto.setDescription(product.getDescription());
        productDto.setPrice(product.getPrice());
        productDto.setStockQuantity(product.getStockQuantity());
        
        return productDto;
    }
    
    @Override
    public Product toEntity(ProductDto dto) {
        if (dto == null) {
            return null;
        }
        
        Product product = new Product();
        product.setId(dto.getId());
        product.setName(dto.getName());
        product.setDescription(dto.getDescription());
        product.setPrice(dto.getPrice());
        product.setStockQuantity(dto.getStockQuantity());
        
        return product;
    }
    
    @Override
    public List<ProductDto> toDtoList(List<Product> products) {
        if (products == null) {
            return null;
        }
        
        List<ProductDto> list = new ArrayList<>(products.size());
        for (Product product : products) {
            list.add(toDto(product));
        }
        
        return list;
    }
}

Notate l'annotazione @Component: grazie alla configurazione defaultComponentModel=spring, il mapper è un bean Spring e possiamo iniettarlo ovunque ne abbiamo bisogno.


Utilizzare il mapper in un Service

Vediamo come utilizzare il mapper all'interno di un service Spring:

@Service
@RequiredArgsConstructor  // Lombok genera il costruttore con i campi final
public class ProductService {
    
    private final ProductRepository productRepository;
    private final ProductMapper productMapper;
    
    public List<ProductDto> findAll() {
        List<Product> products = productRepository.findAll();
        return productMapper.toDtoList(products);
    }
    
    public ProductDto findById(Long id) {
        Product product = productRepository.findById(id)
            .orElseThrow(() -> new EntityNotFoundException("Product not found"));
        return productMapper.toDto(product);
    }
    
    public ProductDto create(ProductDto dto) {
        Product product = productMapper.toEntity(dto);
        product.setCreatedAt(LocalDateTime.now());
        Product saved = productRepository.save(product);
        return productMapper.toDto(saved);
    }
    
    public ProductDto update(Long id, ProductDto dto) {
        Product existing = productRepository.findById(id)
            .orElseThrow(() -> new EntityNotFoundException("Product not found"));
        
        // Aggiorniamo solo i campi che ci interessano
        existing.setName(dto.getName());
        existing.setDescription(dto.getDescription());
        existing.setPrice(dto.getPrice());
        existing.setStockQuantity(dto.getStockQuantity());
        existing.setUpdatedAt(LocalDateTime.now());
        
        Product saved = productRepository.save(existing);
        return productMapper.toDto(saved);
    }
}

Mapping di campi con nomi diversi

Non sempre i campi dell'entity e del DTO hanno lo stesso nome. MapStruct ci permette di specificare il mapping esplicito con l'annotazione @Mapping.

Immaginiamo di voler rinominare alcuni campi nel DTO:

@Data
public class ProductDto {
    private Long id;
    private String productName;      // invece di "name"
    private String productDescription;  // invece di "description"
    private BigDecimal unitPrice;    // invece di "price"
    private Integer quantity;        // invece di "stockQuantity"
}

Il mapper diventa:

@Mapper
public interface ProductMapper {
    
    @Mapping(source = "name", target = "productName")
    @Mapping(source = "description", target = "productDescription")
    @Mapping(source = "price", target = "unitPrice")
    @Mapping(source = "stockQuantity", target = "quantity")
    ProductDto toDto(Product product);
    
    @Mapping(source = "productName", target = "name")
    @Mapping(source = "productDescription", target = "description")
    @Mapping(source = "unitPrice", target = "price")
    @Mapping(source = "quantity", target = "stockQuantity")
    Product toEntity(ProductDto dto);
}

Oppure, in modo più compatto con @Mappings:

@Mapper
public interface ProductMapper {
    
    @Mappings({
        @Mapping(source = "name", target = "productName"),
        @Mapping(source = "description", target = "productDescription"),
        @Mapping(source = "price", target = "unitPrice"),
        @Mapping(source = "stockQuantity", target = "quantity")
    })
    ProductDto toDto(Product product);
}

Mapping di oggetti annidati

Uno dei casi più comuni è il mapping di relazioni. Torniamo al nostro Product che ha una relazione con Category:

@Entity
@Data
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    private String description;
    
    @OneToMany(mappedBy = "category")
    private List<Product> products;
}

Vogliamo che il nostro ProductDto includa il nome della categoria:

@Data
public class ProductDto {
    private Long id;
    private String name;
    private String description;
    private BigDecimal price;
    private String categoryName;  // Solo il nome della categoria
}

MapStruct può navigare automaticamente le proprietà annidate usando la notazione punto:

@Mapper
public interface ProductMapper {
    
    @Mapping(source = "category.name", target = "categoryName")
    ProductDto toDto(Product product);
}

Se invece vogliamo includere un oggetto DTO annidato, creiamo un CategoryDto:

@Data
public class CategoryDto {
    private Long id;
    private String name;
}

@Data
public class ProductDto {
    private Long id;
    private String name;
    private String description;
    private BigDecimal price;
    private CategoryDto category;  // DTO annidato
}

E modifichiamo il mapper per usare un altro mapper:

@Mapper(uses = CategoryMapper.class)
public interface ProductMapper {
    ProductDto toDto(Product product);
}

@Mapper
public interface CategoryMapper {
    CategoryDto toDto(Category category);
    Category toEntity(CategoryDto dto);
}

MapStruct userà automaticamente CategoryMapper per convertire la categoria quando converte il prodotto.


Ignorare campi specifici

A volte non vogliamo mappare certi campi. Possiamo dire a MapStruct di ignorarli:

@Mapper
public interface ProductMapper {
    
    @Mapping(target = "createdAt", ignore = true)
    @Mapping(target = "updatedAt", ignore = true)
    @Mapping(target = "category", ignore = true)
    Product toEntity(ProductDto dto);
}

Questo è utile quando certi campi devono essere gestiti manualmente (come i timestamp) o quando non vogliamo sovrascrivere relazioni esistenti.


Mapping con valori di default e costanti

MapStruct permette di specificare valori di default o costanti:

@Mapper
public interface ProductMapper {
    
    // Se stockQuantity è null, usa 0
    @Mapping(target = "stockQuantity", defaultValue = "0")
    ProductDto toDto(Product product);
    
    // Imposta sempre "ACTIVE" come stato
    @Mapping(target = "status", constant = "ACTIVE")
    Product toEntity(ProductDto dto);
}

Mapping con espressioni Java

Per logiche di mapping più complesse, possiamo usare espressioni Java:

@Mapper(imports = {LocalDateTime.class, UUID.class})
public interface ProductMapper {
    
    @Mapping(target = "createdAt", expression = "java(LocalDateTime.now())")
    @Mapping(target = "sku", expression = "java(UUID.randomUUID().toString())")
    Product toEntity(ProductDto dto);
}

Notate l'attributo imports nell'annotazione @Mapper: serve per importare le classi usate nelle espressioni.


Metodi di mapping personalizzati con @AfterMapping e @BeforeMapping

Per logiche ancora più complesse, MapStruct offre i metodi @BeforeMapping e @AfterMapping:

@Mapper
public abstract class ProductMapper {
    
    public abstract ProductDto toDto(Product product);
    
    public abstract Product toEntity(ProductDto dto);
    
    @AfterMapping
    protected void enrichDto(Product product, @MappingTarget ProductDto dto) {
        // Logica personalizzata dopo il mapping
        if (product.getPrice() != null && product.getStockQuantity() != null) {
            dto.setTotalValue(product.getPrice()
                .multiply(BigDecimal.valueOf(product.getStockQuantity())));
        }
    }
    
    @BeforeMapping
    protected void validateProduct(ProductDto dto) {
        // Validazione prima del mapping
        if (dto.getPrice() != null && dto.getPrice().compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Price cannot be negative");
        }
    }
}

Notate che per usare @BeforeMapping e @AfterMapping, il mapper deve essere una classe astratta invece di un'interfaccia.


Update di entità esistenti

Un pattern comune è aggiornare un'entità esistente con i dati di un DTO, senza crearne una nuova. MapStruct supporta questo scenario con @MappingTarget:

@Mapper
public interface ProductMapper {
    
    ProductDto toDto(Product product);
    
    @Mapping(target = "id", ignore = true)
    @Mapping(target = "createdAt", ignore = true)
    void updateEntityFromDto(ProductDto dto, @MappingTarget Product product);
}

E nel service:

@Service
@RequiredArgsConstructor
public class ProductService {
    
    private final ProductRepository productRepository;
    private final ProductMapper productMapper;
    
    public ProductDto update(Long id, ProductDto dto) {
        Product existing = productRepository.findById(id)
            .orElseThrow(() -> new EntityNotFoundException("Product not found"));
        
        // Aggiorna l'entità esistente con i dati del DTO
        productMapper.updateEntityFromDto(dto, existing);
        existing.setUpdatedAt(LocalDateTime.now());
        
        Product saved = productRepository.save(existing);
        return productMapper.toDto(saved);
    }
}

Mapping di collezioni e mappe

MapStruct gestisce automaticamente le collezioni:

@Mapper
public interface ProductMapper {
    
    ProductDto toDto(Product product);
    
    Product toEntity(ProductDto dto);
    
    List<ProductDto> toDtoList(List<Product> products);
    
    Set<ProductDto> toDtoSet(Set<Product> products);
    
    Map<Long, ProductDto> toDtoMap(Map<Long, Product> products);
}

MapStruct itera automaticamente sulla collezione e applica il metodo di mapping appropriato a ogni elemento.


Gestione dei null

MapStruct gestisce i null in modo intelligente. Di default:

Possiamo personalizzare questo comportamento:

@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface ProductMapper {
    
    // Se una proprietà del DTO è null, non sovrascrive quella dell'entity
    void updateEntityFromDto(ProductDto dto, @MappingTarget Product product);
}

Oppure a livello di singola proprietà:

@Mapper
public interface ProductMapper {
    
    @Mapping(target = "description", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT)
    ProductDto toDto(Product product);
}

Conversioni di tipo automatiche

MapStruct converte automaticamente tra tipi compatibili:

Per esempio:

@Entity
public class Product {
    private LocalDateTime createdAt;
    private ProductStatus status;  // enum
}

@Data
public class ProductDto {
    private String createdAt;  // MapStruct converte automaticamente
    private String status;     // MapStruct converte automaticamente
}

Per formattare le date, usiamo l'attributo dateFormat:

@Mapper
public interface ProductMapper {
    
    @Mapping(target = "createdAt", dateFormat = "dd/MM/yyyy HH:mm:ss")
    ProductDto toDto(Product product);
}

Mapping di enum

Per il mapping tra enum diversi o tra enum e stringhe:

public enum ProductStatus {
    ACTIVE, INACTIVE, DISCONTINUED
}

public enum ProductStatusDto {
    AVAILABLE, UNAVAILABLE, REMOVED
}

@Mapper
public interface ProductMapper {
    
    @ValueMapping(source = "ACTIVE", target = "AVAILABLE")
    @ValueMapping(source = "INACTIVE", target = "UNAVAILABLE")
    @ValueMapping(source = "DISCONTINUED", target = "REMOVED")
    ProductStatusDto toStatusDto(ProductStatus status);
    
    // Per valori non mappati, usa un default
    @ValueMapping(source = MappingConstants.ANY_REMAINING, target = "UNAVAILABLE")
    ProductStatusDto toStatusDtoWithDefault(ProductStatus status);
}

Composizione di mapper

In progetti grandi, è utile comporre mapper più piccoli e riutilizzabili:

// Mapper base per gli audit field
@Mapper
public interface AuditMapper {
    
    @Mapping(target = "createdAt", source = "createdAt")
    @Mapping(target = "updatedAt", source = "updatedAt")
    @Mapping(target = "createdBy", source = "createdBy")
    void mapAuditFields(Auditable source, @MappingTarget AuditableDto target);
}

// Mapper di prodotto che usa AuditMapper
@Mapper(uses = {CategoryMapper.class, AuditMapper.class})
public interface ProductMapper {
    ProductDto toDto(Product product);
}

Configurazione condivisa con @MapperConfig

Per evitare di ripetere la stessa configurazione su ogni mapper, usiamo @MapperConfig:

@MapperConfig(
    componentModel = "spring",
    nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
    nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS
)
public interface MapperConfiguration {
}

// Ora ogni mapper può ereditare questa configurazione
@Mapper(config = MapperConfiguration.class)
public interface ProductMapper {
    ProductDto toDto(Product product);
}

@Mapper(config = MapperConfiguration.class)
public interface CategoryMapper {
    CategoryDto toDto(Category category);
}

Iniezione di servizi Spring nei mapper

A volte abbiamo bisogno di chiamare servizi Spring durante il mapping. Possiamo farlo usando classi astratte:

@Mapper(componentModel = "spring")
public abstract class ProductMapper {
    
    @Autowired
    protected PriceCalculationService priceService;
    
    @Autowired
    protected CategoryRepository categoryRepository;
    
    @Mapping(target = "finalPrice", expression = "java(calculateFinalPrice(product))")
    @Mapping(target = "category", source = "categoryId")
    public abstract ProductDto toDto(Product product);
    
    protected BigDecimal calculateFinalPrice(Product product) {
        return priceService.calculateWithDiscount(product.getPrice(), product.getId());
    }
    
    protected Category mapCategory(Long categoryId) {
        if (categoryId == null) {
            return null;
        }
        return categoryRepository.findById(categoryId).orElse(null);
    }
}

Testing dei mapper

Testare i mapper è semplice perché sono bean Spring standard:

@SpringBootTest
class ProductMapperTest {
    
    @Autowired
    private ProductMapper productMapper;
    
    @Test
    void shouldMapProductToDto() {
        // Given
        Product product = new Product();
        product.setId(1L);
        product.setName("Test Product");
        product.setPrice(new BigDecimal("99.99"));
        product.setStockQuantity(10);
        
        // When
        ProductDto dto = productMapper.toDto(product);
        
        // Then
        assertThat(dto.getId()).isEqualTo(1L);
        assertThat(dto.getName()).isEqualTo("Test Product");
        assertThat(dto.getPrice()).isEqualByComparingTo("99.99");
        assertThat(dto.getStockQuantity()).isEqualTo(10);
    }
    
    @Test
    void shouldMapDtoToProduct() {
        // Given
        ProductDto dto = new ProductDto();
        dto.setName("New Product");
        dto.setPrice(new BigDecimal("149.99"));
        
        // When
        Product product = productMapper.toEntity(dto);
        
        // Then
        assertThat(product.getName()).isEqualTo("New Product");
        assertThat(product.getPrice()).isEqualByComparingTo("149.99");
    }
    
    @Test
    void shouldUpdateExistingProduct() {
        // Given
        Product existing = new Product();
        existing.setId(1L);
        existing.setName("Old Name");
        existing.setCreatedAt(LocalDateTime.now().minusDays(30));
        
        ProductDto dto = new ProductDto();
        dto.setName("New Name");
        dto.setPrice(new BigDecimal("199.99"));
        
        // When
        productMapper.updateEntityFromDto(dto, existing);
        
        // Then
        assertThat(existing.getId()).isEqualTo(1L); // non modificato
        assertThat(existing.getName()).isEqualTo("New Name"); // aggiornato
        assertThat(existing.getCreatedAt()).isNotNull(); // non modificato
    }
}

Best Practices

Concludiamo con alcune best practice per l'uso di MapStruct:

1. Un mapper per bounded context

Non create un unico mapper gigante. Create mapper separati per ogni area funzionale della vostra applicazione.

2. DTO specifici per caso d'uso

Non abbiate paura di creare DTO diversi per operazioni diverse. Un ProductSummaryDto per le liste, un ProductDetailDto per il dettaglio, un ProductCreateDto per la creazione.

3. Validate i DTO, non le Entity

La validazione dei dati in input dovrebbe avvenire sui DTO, usando le annotazioni di Bean Validation (@NotNull, @Size, ecc.).

4. Non mappate le relazioni bidirezionali

Se avete relazioni bidirezionali (es. ProductCategory), mappate solo una direzione per evitare ricorsioni infinite.

5. Usate @MappingTarget per gli update

Invece di creare nuove entity dal DTO, aggiornate quelle esistenti. Questo preserva campi come l'ID e i timestamp di creazione.

6. Controllate il codice generato

Ogni tanto date un'occhiata al codice generato nella cartella target/generated-sources. Vi aiuterà a capire cosa fa MapStruct e a diagnosticare problemi.

7. Gestite esplicitamente i campi non mappati

Usate @Mapping(target = "campo", ignore = true) invece di lasciare warning in compilazione. Questo documenta le vostre intenzioni.


Conclusioni

MapStruct è uno strumento potente che elimina il codice boilerplate di mapping mantenendo performance eccellenti e type-safety. L'integrazione con Spring Boot è seamless grazie al component model spring, e la possibilità di vedere il codice generato rende il debugging semplice.

Iniziate con mapping semplici e progressivamente esplorate le funzionalità avanzate man mano che ne avete bisogno. La curva di apprendimento è dolce, e una volta padroneggiato, MapStruct diventerà uno strumento indispensabile nel vostro toolkit di sviluppo.

Risorse utili: