Come mappare genitori e figli usando Mapstruct in Spring Boot?

Dec 01 2020

Ho padre (prodotto) e figli (libro, mobili) e vorrei mappare l'entità del prodotto al DTO del prodotto. Come puoi vedere, il prodotto è mappato e archiviato in un'unica tabella nel database. Come posso mappare il genitore, il prodotto, che ha dettagli extra di suo figlio?

Ho guardato questo , questo e questo per avere un'idea ma senza fortuna

Entità

@Entity
@Table(name = "product")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class Product {
  @Id
  private long id;
  private String productName;
}

@Entity
@DiscriminatorValue("Book")
public class Book extends Product { 
  private String author;
  ...
}
@Entity
@DiscriminatorValue("Furniture")
public class Furniture extends Product {
  String color;
  ...
}

DTO

public class ProductDto {
  private long id;
  private String productName;
  ...
}

public class BookDto extends ProductDto {
  private String author;
  ...
}
public class FurnitureDto extends ProductDto {
   String color;
   ... 
}

Mapper

@Mapper(uses = {BookMapper.class,FurnitureMapper.class})
public interface ProductMapper {
    
    ProductDto productToProductDto(Product product);
    Product productDtoToProduct(ProductDto productDto);
}

@Mapper
public interface BookMapper {
    BookDto bookToBookDto(Book book);
    Book bookDtoToBook(BookDto bookDto);
}

@Mapper
public interface FurnitureMapper {
    FurnitureDto furnitureToFurnitureDto(Furniture furniture);
    Furniture furnitureDtoToFurniture(FurnitureDto furnitureDto);
}

Servizio

@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    ProductRepository productRepository;
    @Autowired
    ProductMapper productMapper;

    @Override
    public List<ProductDto> getAllProducts() {
        List<ProductDto> listOfProducts = new ArrayList<>();
        productRepository.findAll().forEach(i -> 
        listOfProducts.add(productMapper.productToProductDto(i)));
        return listOfProducts;
    }
}

Modificato

Ottengo il seguente risultato dopo aver mappato l'entità del prodotto sul prodotto dto. Non associa i dati e non include i relativi attributi figli. La sezione del mappatore sopra è corretta?

[
    {
        "id": 0,
        "productName": null
    },
    {
        "id": 0,
        "productName": null
    },
    ...
]

Il risultato tuttavia dovrebbe essere come il seguente:

[
    {
        "id": 11,
        "productName": ABC,
        "author":"James"
    },
    {
        "id": 22,
        "productName": XYZ,
        "color":"Oak"
    },
    ...
]

Risposte

2 TasosP. Dec 10 2020 at 05:01

TL; DR

Non esiste un modo pulito per farlo. Il motivo risiede nella selezione del metodo in fase di compilazione di Java. Ma c'è un modo un po 'pulito di usare il pattern del visitatore.

Perché non funziona

Mentre iterate l'elenco delle entità, che contiene diversi tipi (Prodotto, Libro, Arredo), è necessario chiamare un diverso metodo di mappatura per ogni tipo (cioè un diverso mappatore MapStruct).

A meno che tu non segua la instanceofstrada, come suggerisce Amir, e selezioni esplicitamente il mappatore, devi usare il sovraccarico del metodo per invocare diversi metodi di mappatura per classe di entità. Il problema è che Java selezionerà il metodo sovraccarico in fase di compilazione e, a quel punto, il compilatore vede solo un elenco di Productoggetti (quelli restituiti dal metodo del repository). Non importa se MapStruct o Spring o il tuo codice personalizzato cercano di farlo. Questo è anche il motivo per cui your ProductMapperè sempre invocato: è l'unico tipo visibile in fase di compilazione.

Utilizzando il pattern del visitatore

Poiché dobbiamo scegliere manualmente il mappatore giusto , possiamo scegliere in che modo è più pulito o più manutenibile. Questo è decisamente un parere.

Il mio suggerimento è di utilizzare il pattern del visitatore (in realtà una sua variazione) nel modo seguente:

Introduci una nuova interfaccia per le tue entità che devono essere mappate:

public interface MappableEntity {

    public ProductDto map(EntityMapper mapper);
}

Le tue entità dovranno implementare questa interfaccia, ad esempio:

public class Book extends Product implements MappableEntity {
//...
@Override
    public ProductDto map(EntityMapper mapper) {
        return mapper.map(this);//This is the magic part. We choose which method to call because the parameter is this, which is a Book!
    }
}

EntityMapper è l'interfaccia del visitatore:

public interface EntityMapper {

    ProductDto map(Product entity);

    BookDto map(Book entity);

    FurnitureDto map(Furniture entity);

    // Add your next entity here
}

Infine, hai bisogno del MasterMapper:

// Not a class name I'm proud of
public class MasterMapper implements EntityMapper {

    // Inject your mappers here

    @Override
    public ProductDto map(Product product) {
        ProductMapper productMapper = Mappers.getMapper(ProductMapper.class);
        return productMapper.map(product);
    }

    @Override
    public BookDto map(Book product) {
        BookMapper productMapper = Mappers.getMapper(BookMapper.class);
        return productMapper.map(product);
    }

    @Override
    public FurnitureDto map(Furniture product) {
        FurnitureMapper productMapper = Mappers.getMapper(FurnitureMapper.class);
        return productMapper.map(product);
    }

    // Add your next mapper here

}

Il tuo metodo di servizio sarà quindi:

MasterMapper mm = new MasterMapper();
List<Product> listOfEntities = productRepository.findAll();
List<ProductDto> listOfProducts = new ArrayList<>(listOfEntities.size());
listOfEntities.forEach(i -> {
        if (i instanceof MappableEntity) {
            MappableEntity me = i;
            ProductDto dto = me.map(mm);
            listOfProducts.add(dto);
        } else {
            // Throw an AssertionError during development (who would run server VMs with -ea ?!?!)
            assert false : "Can't properly map " + i.getClass() + " as it's not implementing MappableEntity";
            // Use default mapper as a fallback
            final ProductDto defaultDto = Mappers.getMapper(ProductMapper.class).map(i);
            listOfProducts.add(defaultDto);
        }
     });
    return listOfProducts;

Puoi tranquillamente ignorare le Mappers.getMapper()chiamate: poiché il problema non è correlato a Spring, ho creato un esempio funzionante su GitHub utilizzando le fabbriche di MapStruct per semplicità. Inietterai semplicemente i tuoi mappatori usando CDI.

1 AmirSchnell Dec 09 2020 at 16:44

Questo è esattamente lo stesso scenario descritto in questo numero .
Purtroppo questo problema è attualmente ancora aperto e nessuno ha fornito una soluzione finora. Quindi penso che quello che vuoi non sia possibile.

L'unico modo per risolvere il tuo problema è modificare il codice all'interno del tuo servizio.

OPUna cosa che potresti fare, come menzionato nel numero, è testare ogni Productoggetto con instanceof:

@Autowired
BookMapper bookMapper;
@Autowired
FurnitureMapper furnitureMapper;

public List<ProductDto> getAllProducts() {
    List<ProductDto> listOfProducts = new ArrayList<>();
    List<Product> all = productRepository.findAll();
    for(Product product : all) {
        if(product instanceof Book) {
            listOfProducts.add(bookMapper.bookToBookDto((Book)product));
        } else if(product instanceof Furniture) {
            listOfProducts.add(furnitureMapper.furnitureToFurnitureDto((Furniture)product));
        }
    }
    return listOfProducts;
}

Potresti anche ProductMapperestendere BookMappere FurnitureMapper, quindi non devi iniettarli.