Daniel
Daniel

Reputation: 486

How to map parent and children using Mapstruct in Spring Boot?

I have parent (product) and children(book,furniture), and would like to map product entity to product DTO. As you can see, the product is mapped and stored in single table in database. How can I map the parent, product, that has extra details of its child?

I have look at this, this and this to get some idea but no luck

Entity

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

Service

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

Edited

I get the following result after mapping the product entity to product dto. It does not bind the data and does not include its children attributes. Does the above mapper section correct?

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

The result however should be like bellow:

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

Upvotes: 10

Views: 19305

Answers (2)

Tasos P.
Tasos P.

Reputation: 4124

TL;DR

There is no clean way to do this. The reason lies in Java's compile-time method selection. But there is a somewhat clean way of using the visitor pattern.

Why it's not working

While you iterate the list of entities, which contains different types (Product, Book, Furniture), you need to call a different mapping method for each type (i.e. a different MapStruct mapper).

Unless you go with the instanceof way, as Amir suggests, and explicitly select the mapper, you need to use method overloading to invoke different mapping methods per entity class. The problem is that Java will select the overloaded method at compile-time and at that point, the compiler only sees a list of Product objects (the ones returned by your repository method). It doesn't really matter if MapStruct or Spring or your own custom code tries to do that. This is also why your ProductMapper is always invoked: it's the only type visible at compile-time.

Using the visitor pattern

Since we need to pick the right mapper manually, we get to choose which way is cleaner or more maintainable. This is definitely opinioned.

My suggestion is to use the visitor pattern (actually a variation of it) in the following way:

Introduce a new interface for your entities that need to be mapped:

public interface MappableEntity {

    public ProductDto map(EntityMapper mapper);
}

Your entities will need to implement this interface, for example:

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 is the visitor interface:

public interface EntityMapper {

    ProductDto map(Product entity);

    BookDto map(Book entity);

    FurnitureDto map(Furniture entity);

    // Add your next entity here
}

Finally, you need the 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

}

Your service method will then look like:

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;

You can safely ignore Mappers.getMapper() calls: since the issue is not related to Spring, I created a working example on GitHub using MapStruct's factories for simplicity. You will just inject your mappers using CDI.

Upvotes: 6

Amir Schnell
Amir Schnell

Reputation: 651

This is exactly the same scenario as described in this issue.
Unfortunately this issue is currently still open and nobody has provided solution so far. So I think what you want is not possible.

The only way to solve your problem is to change the code within your service.

OPne thing you could do, as mentioned in the issue, is to test every Product object with 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;
}

You could also make your ProductMapper extend BookMapper and FurnitureMapper, so you don't have to inject those.

Upvotes: 1

Related Questions