Home > database >  Collectors mapping() reducing() - folding stream element having One-To-Many relationship with Java
Collectors mapping() reducing() - folding stream element having One-To-Many relationship with Java

Time:08-20

The classes that I have the following domain classes:

@AllArgsConstructor
@Getter
@ToString
public static class Customer {
    private String id;
    private String name;
}

@Getter
public static class Order {
    private String id;
    private Set<OrderContent> orderContent = new HashSet<>();
    private Customer customer;
    
    public Order(String id, Customer customer) {
        this.id = id;
        this.customer = customer;
    }
    
    public Order addOrderContent(Product product, int amount) {
        orderContent.add(new OrderContent(product, amount));
        return this;
    }
}

@AllArgsConstructor
@Getter
public static class Product {
    private String name;
    private BigDecimal price;
}

@AllArgsConstructor
@Getter
public static class OrderContent {
    private Product product;
    private Integer quantity;
}

So each Order is associated with a Customer and one-to-many association with OrderContent. Each OrderContent has properties: product and quantity.

I would like to return the most valuable customer, which is the customer that has the highest value of all placed orders.

Let's consider the example below:

List<Order> orders = new ArrayList<>();

Customer customer1 = new Customer("1", "Bill");
Customer customer2 = new Customer("2", "John");

Product device1 = new Product("Device 1", new BigDecimal("30.00"));
Product device2 = new Product("Device 2", new BigDecimal("50.00"));
Product device3 = new Product("Device 3", new BigDecimal("110.00"));

orders.add(new Order("1", customer1).addOrderContent(device1 , 1));
orders.add(new Order("2", customer1).addOrderContent(device2 , 1));
orders.add(new Order("3", customer2).addOrderContent(device3, 1));

The code below fail to compile. The problem is located inside the collector, because the types of arguments inside mapping() does not match. How to implement this logic properly?

My code:

Customer customer = orders.stream()
    .collect(Collectors.groupingBy(Order::getCustomer,
        Collectors.mapping((Order order) -> order.getOrderContent().stream()
            .map(orderContent -> orderContent.getProduct().getPrice()
                .multiply(new BigDecimal(orderContent.getQuantity()))),
            Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))
    ))
    .entrySet().stream()
    .max(Comparator.comparing(Map.Entry::getValue))
    .map(Map.Entry::getKey).orElse(null);

CodePudding user response:

You were almost there.

The only thing required is to handle properly one-to-many transformation - an order into prices of products within this order.

Java 9

Instead of collector mapping() to transform Order object into a sequence of OrderContent objects, you can use collector flatMapping(), available since JDK version 9, as the downstream of groupingBy().

And then we can turn each OrderContent into BigDecimal by multiplying price and amount, and obtain the total amount spent for every Customer by using reducing() exactly as you did.

Customer mostValCust = orders.stream()
    .collect(Collectors.groupingBy(Order::getCustomer,
        Collectors.flatMapping((Order order) -> order.getOrderContent().stream()
            .map(orderContent -> orderContent.getProduct().getPrice()
                .multiply(BigDecimal.valueOf(orderContent.getQuantity()))),
        Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))
    ))
    .entrySet().stream()
    .max(Map.Entry.comparingByValue())
    .map(Map.Entry::getKey)
    .orElse(null);

Java 8

One of the ways to achieve it with Java 8 is to move the logic for accumulating the overall price of each Order into the collector reducing():

Customer mostValCust = orders.stream()
    .collect(Collectors.groupingBy(Order::getCustomer,
        Collectors.reducing(
            BigDecimal.ZERO,
            order -> order.getOrderContent().stream()
                .map(orderContent -> orderContent.getProduct().getPrice()
                    .multiply(BigDecimal.valueOf(orderContent.getQuantity())))
                .reduce(BigDecimal.ZERO,BigDecimal::add),
        BigDecimal::add)
    ))
    .entrySet().stream()
    .max(Map.Entry.comparingByValue())
    .map(Map.Entry::getKey)
    .orElse(null);
  • Related