Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Transform a flat list to domain objects with child objects using java streams

Tags:

I have incoming objects with a flat de-normalized structure which I instantiated from a JDBC resultset. The incoming objects mirror the resultset, there's loads of repeated data so I want to convert the data into a list of parent objects with nested child collections, i.e. an object graph, or normalized list.

The incoming object's class looks like this:

class IncomingFlatItem {
    String clientCode;
    String clientName;
    String emailAddress;
    boolean emailHtml;
    String reportCode;
    String reportLanguage;
}

So the incoming data contains multiple objects for each client, which I'd like to aggregate into one client object, which contains a list of email address objects for the client, and a list of report objects.

So the Client object would look like this:

class Client {
    String clientCode;
    String clientName;
    Set<EmailAddress> emailAddresses;
    Set<Report> reports;
}

Strangely I can't find an existing answer for this. I am looking at nesting streams or chaining streams but I'd like to find the most elegant approach and I definitely want to avoid a for-loop.

like image 828
Adam Avatar asked Jun 14 '19 12:06

Adam


2 Answers

You can use this:

List<Client> clients = items.stream()
        .collect(Collectors.groupingBy(i -> Arrays.asList(i.getClientCode(), i.getClientName())))
        .entrySet().stream()
        .map(e -> new Client(e.getKey().get(0), e.getKey().get(1),
                e.getValue().stream().map(i -> new EmailAddress(i.getEmailAddress(), i.isEmailHtml())).collect(Collectors.toSet()),
                e.getValue().stream().map(i -> new Report(i.getReportCode(), i.getReportLanguage())).collect(Collectors.toSet())))
        .collect(Collectors.toList());

At the beginning you group your items by clientCode and clientName. After that you map the results to your Client object.

Make sure the .equals() and hashCode() methods are implemented for EmailAddress and Report to ensure they are distinct in the set.

like image 151
Samuel Philipp Avatar answered Nov 03 '22 01:11

Samuel Philipp


One thing you can do is use constructor parameters and a fluent API to your advantage. Thinking "nested" flows and the stream API (with dynamic data) can get complex very quickly.

This just uses a fluent API to simplify things (you can use a proper builder pattern instead)

class Client {
    String clientCode;
    String clientName;
    Set<EmailAddress> emailAddresses = new HashSet<>();
    Set<Report> reports = new HashSet<>();

    public Client(String clientCode, String clientName) {
        super();
        this.clientCode = clientCode;
        this.clientName = clientName;
    }

    public Client emailAddresses(String address, boolean html) {
        this.emailAddresses = 
             Collections.singleton(new EmailAddress(address, html));
        return this;
    }

    public Client reports(String... reports) {
        this.reports = Arrays.stream(reports)
                        .map(Report::new)
                        .collect(Collectors.toSet());
        return this;
    }

    public Client merge(Client other) {
        this.emailAddresses.addAll(other.emailAddresses);
        this.reports.addAll(other.reports);

        if (null == this.clientName)
            this.clientName = other.clientName;
        if (null == this.clientCode)
            this.clientCode = other.clientCode;

        return this;
    }
}

class EmailAddress {
    public EmailAddress(String e, boolean html) {

    }
}

class Report {
    public Report(String r) {

    }
}

And...

Collection<Client> clients = incomingFlatItemsCollection.stream()
        .map(flatItem -> new Client(flatItem.clientCode, flatItem.clientName)
                          .emailAddresses(flatItem.emailAddress, flatItem.emailHtml)
                          .reports(flatItem.reportCode, flatItem.reportLanguage))
        .collect(Collectors.groupingBy(Client::getClientCode,
                Collectors.reducing(new Client(null, null), Client::merge)))
        .values();

Or you can also just use mapping functions that convert IncomingFlatItem objects to Client.

like image 38
ernest_k Avatar answered Nov 02 '22 23:11

ernest_k