Как сопоставить родителя и детей с помощью Mapstruct в Spring Boot?

Dec 01 2020

У меня есть родитель (продукт) и дети (книга, мебель), и я хочу сопоставить объект продукта с DTO продукта. Как видите, продукт отображается и хранится в одной таблице в базе данных. Как я могу сопоставить родительский продукт, у которого есть дополнительные сведения о его дочернем элементе?

Я посмотрел на это , это и это, чтобы получить представление, но не повезло

сущность

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

обслуживание

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

Отредактировано

После сопоставления сущности продукта и продукта dto я получаю следующий результат. Он не связывает данные и не включает дочерние атрибуты. Правильный ли приведенный выше раздел картографа ?

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

Однако результат должен быть таким, как показано ниже:

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

Ответы

2 TasosP. Dec 10 2020 at 05:01

TL; DR

Нет чистого способа сделать это. Причина кроется в выборе метода Java во время компиляции. Но есть несколько более понятный способ использования шаблона посетителя.

Почему не работает

Во время итерации списка объектов, который содержит разные типы (продукт, книга, мебель), вам необходимо вызвать другой метод сопоставления для каждого типа (т. Е. Другой сопоставитель MapStruct).

Если вы не пойдете по instanceofпути, как предлагает Амир, и явно не выберете средство сопоставления, вам необходимо использовать перегрузку методов для вызова разных методов сопоставления для каждого класса сущности. Проблема в том, что Java выберет перегруженный метод во время компиляции, и в этот момент компилятор видит только список Productобъектов (тех, которые возвращены вашим методом репозитория). На самом деле не имеет значения, пытается ли это сделать MapStruct, Spring или ваш собственный код. Вот почему ProductMapperвсегда вызывается your : это единственный тип, видимый во время компиляции.

Использование шаблона посетителя

Поскольку нам нужно выбрать правильный маппер вручную , мы можем выбрать, какой способ будет более чистым или более удобным в обслуживании. Это определенно считается общепринятым.

Я предлагаю использовать шаблон посетителя (на самом деле его вариант) следующим образом:

Представьте новый интерфейс для ваших сущностей, которые необходимо сопоставить:

public interface MappableEntity {

    public ProductDto map(EntityMapper mapper);
}

Вашим объектам потребуется реализовать этот интерфейс, например:

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 - это интерфейс посетителя:

public interface EntityMapper {

    ProductDto map(Product entity);

    BookDto map(Book entity);

    FurnitureDto map(Furniture entity);

    // Add your next entity here
}

Наконец, вам понадобится 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

}

Тогда ваш метод обслуживания будет выглядеть так:

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;

Вы можете спокойно игнорировать Mappers.getMapper()вызовы: поскольку проблема не связана со Spring, я создал рабочий пример на GitHub, используя фабрики MapStruct для простоты. Вы просто внедрите свои картографы с помощью CDI.

1 AmirSchnell Dec 09 2020 at 16:44

Это точно такой же сценарий, как описано в этом выпуске .
К сожалению, эта проблема в настоящее время все еще открыта, и пока никто не предложил решения. Поэтому я думаю, что то, что вы хотите, невозможно.

Единственный способ решить вашу проблему - изменить код в вашем сервисе.

Как упоминалось в проблеме, вы могли бы протестировать каждый Productобъект с помощью 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;
}

Вы также можете ProductMapperрасширить BookMapperи FurnitureMapper, чтобы не вводить их.