Jak zmapować rodzica i dzieci za pomocą Mapstruct w Spring Boot?

Dec 01 2020

Mam rodzica (produkt) i dzieci (książka, meble) i chciałbym zmapować jednostkę produktu do produktu DTO. Jak widać, produkt jest mapowany i przechowywany w jednej tabeli w bazie danych. Jak mogę zmapować rodzica, produkt, który ma dodatkowe szczegóły dotyczące swojego dziecka?

Rzuciłem okiem na to , to i to, aby mieć jakiś pomysł, ale bez powodzenia

Jednostka

@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);
}

Usługa

@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;
    }
}

Edytowano

Otrzymuję następujący wynik po zmapowaniu jednostki produktu do produktu dto. Nie wiąże danych i nie zawiera atrybutów potomnych. Czy powyższa sekcja mapowania jest poprawna?

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

Wynik powinien jednak wyglądać jak poniżej:

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

Odpowiedzi

2 TasosP. Dec 10 2020 at 05:01

TL; DR

Nie ma na to czystego sposobu. Przyczyna tkwi w wyborze metody kompilacji języka Java. Ale jest dość czysty sposób wykorzystania wzorca odwiedzających.

Dlaczego to nie działa

Podczas iteracji listy obiektów, która zawiera różne typy (produkt, książka, meble), musisz wywołać inną metodę mapowania dla każdego typu (tj. Inny mapownik MapStruct).

Jeśli nie pójdziesz instanceofdrogą, jak sugeruje Amir, i jawnie wybierzesz program odwzorowujący, musisz użyć przeciążenia metod, aby wywołać różne metody mapowania na klasę jednostki. Problem polega na tym, że Java wybierze przeciążoną metodę w czasie kompilacji iw tym momencie kompilator widzi tylko listę Productobiektów (te zwrócone przez metodę repozytorium). Naprawdę nie ma znaczenia, czy próbuje to zrobić MapStruct, Spring lub Twój własny kod niestandardowy. Dlatego też ProductMapperzawsze wywoływana jest twoja : jest to jedyny typ widoczny w czasie kompilacji.

Korzystanie ze wzorca gości

Ponieważ musimy ręcznie wybrać odpowiedniego mapera , możemy wybrać, który sposób jest czystszy lub łatwiejszy w utrzymaniu. To jest zdecydowanie opiniowane.

Moją sugestią jest użycie wzorca gości (właściwie jego odmiany) w następujący sposób:

Wprowadź nowy interfejs dla swoich encji, które wymagają mapowania:

public interface MappableEntity {

    public ProductDto map(EntityMapper mapper);
}

Twoje podmioty będą musiały wdrożyć ten interfejs, na przykład:

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 to interfejs gościa:

public interface EntityMapper {

    ProductDto map(Product entity);

    BookDto map(Book entity);

    FurnitureDto map(Furniture entity);

    // Add your next entity here
}

Wreszcie, potrzebujesz 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

}

Twoja metoda obsługi będzie wtedy wyglądać następująco:

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;

Można bezpiecznie zignorować Mappers.getMapper()połączeń: ponieważ problem nie jest związany z wiosną, stworzyłem przykład pracuje na GitHub użyciu fabryk MapStruct dla prostoty. Po prostu wstrzykniesz swoje mapery za pomocą CDI.

1 AmirSchnell Dec 09 2020 at 16:44

To jest dokładnie ten sam scenariusz, który opisano w tym wydaniu .
Niestety ta kwestia jest obecnie nadal otwarta i nikt do tej pory nie przedstawił rozwiązania. Więc myślę, że to, czego chcesz, nie jest możliwe.

Jedynym sposobem rozwiązania problemu jest zmiana kodu w usłudze.

OP Jedną rzeczą, którą możesz zrobić, jak wspomniano w numerze, jest przetestowanie każdego Productobiektu za pomocą 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;
}

Można również tworzyć ProductMapperprzedłużyć BookMapperi FurnitureMappertak nie trzeba wstrzyknąć ci.