简体   繁体   English

使用java流将平面列表转换为具有子对象的域对象

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

I have incoming objects with a flat de-normalized structure which I instantiated from a JDBC resultset. 我有一个带有平面反规范化结构的传入对象,我从JDBC结果集中实例化。 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, ie 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: 所以Client对象看起来像这样:

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. 我正在寻找嵌套流或链接流,但我想找到最优雅的方法,我绝对想避免一个for循环。

One thing you can do is use constructor parameters and a fluent API to your advantage. 您可以做的一件事是使用构造函数参数和流畅的API。 Thinking "nested" flows and the stream API (with dynamic data) can get complex very quickly. 思考“嵌套”流和流API(使用动态数据)可以非常快速地变得复杂。

This just uses a fluent API to simplify things (you can use a proper builder pattern instead) 这只是使用流畅的API来简化事情(您可以使用适当的构建器模式)

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 . 或者您也可以使用将IncomingFlatItem对象转换为Client映射函数。

You can do something on the lines of using mapping function to convert List<IncomingFlatItem> to Set<Reports/EmailAddress> as: 您可以使用映射函数将List<IncomingFlatItem>转换为Set<Reports/EmailAddress>如下所示:

Function<List<IncomingFlatItem>, Set<EmailAddress>> inferEmailAddress =
        incomingFlatItems -> incomingFlatItems.stream()
                .map(obj -> new EmailAddress(obj.getEmailAddress(), 
                                             obj.isEmailHtml()))
                .collect(Collectors.toSet());

Function<List<IncomingFlatItem>, Set<Report>> inferReports =
        incomingFlatItems -> incomingFlatItems.stream()
                .map(obj -> new Report(obj.getReportCode(), 
                                       obj.getReportLanguage()))
                .collect(Collectors.toSet());

and further using groupingBy and mapping the entries to List<Client> as: 并进一步使用groupingBy并将条目映射到List<Client>

List<Client> transformIntoGroupedNormalisedContent(
                  List<IncomingFlatItem> incomingFlatItemList) {
    return incomingFlatItemList.stream()
            .collect(Collectors.groupingBy(inc ->
                    Arrays.asList(inc.getClientCode(), inc.getClientName())))
            .entrySet()
            .stream()
            .map(e -> new Client(e.getKey().get(0), 
                                 e.getKey().get(1),
                                 inferEmailAddress.apply(e.getValue()), 
                                 inferReports.apply(e.getValue())))
            .collect(Collectors.toList());
}

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 . 在开始时,您通过clientCodeclientName对项目进行clientName After that you map the results to your Client object. 之后,将结果映射到Client对象。

Make sure the .equals() and hashCode() methods are implemented for EmailAddress and Report to ensure they are distinct in the set. 确保为EmailAddressReport实现.equals()hashCode()方法,以确保它们在集合中是不同的。

Thanks to all the answerers who mentioned Collectors.groupingBy() . 感谢所有提到Collectors.groupingBy()的回答者。 This was key to setting up a stream where I could use reduce() . 这是设置我可以使用reduce()的流的关键。 I had erroneously believed I should be able to use reduce on its own to solve the problem, without groupingBy . 我错误地认为我应该能够自己使用reduce来解决问题,而不需要groupingBy

Thanks also to the suggestion to create a fluent API. 还要感谢创建一个流畅的API的建议。 I added IncomingFlatItem.getEmailAddress() and IncomingFlatItem.getReport() to fluently grab the domain objects from IncomingFlatItem - and also a method to convert the whole flat item to a proper domain object with its email and report nested already: 我添加了IncomingFlatItem.getEmailAddress()IncomingFlatItem.getReport()来流利地从IncomingFlatItem获取域对象 - 还有一个方法将整个平面项目转换为适当的域对象,其电子邮件和报告已嵌套:

public Client getClient() {
    Client client = new Client();
    client.setClientCode(clientCode);
    client.setClientName(clientName);
    client.setEmailAddresses(new ArrayList());
    client.getEmailAddresses().add(this.getEmailAddress());
    client.setReports(new ArrayList<>());
    client.getReports().add(this.getReport());
    return client;
}

I also created business ID-based .equals() and .hashCode() methods on Client , EmailAddress and Report as recommended by @SamuelPhilip 我还根据.equals()建议在ClientEmailAddressReport上创建了基于业务ID的.equals().hashCode()方法。

Lastly for the domain objects, I created .addReport(Report r) and .addEmail(EmailAddress e) on my Client class, which would add the child object to Client if not already present. 最后,对于域对象,我在我的Client类上创建了.addReport(Report r).addEmail(EmailAddress e) ,如果尚未存在,则会将子对象添加到Client I ditched the Set collection type for List because the domain model standard is List and Sets would have meant lots of conversions to Lists . 我放弃了ListSet集合类型,因为域模型标准是ListSets意味着很多转换到Lists

So with that, the stream code and lambdas look succinct. 因此,流代码和lambdas看起来简洁。

There are 3 steps: 有3个步骤:

  1. map IncomingFlatItems to Clients IncomingFlatItems映射到Clients
  2. group the Clients into a map by client (relying heavily on Client.equals() ) ClientsClients分组到一个映射中(严重依赖于Client.equals()
  3. reduce each group to one Client 将每个组缩减为一个Client

So this is the functional algorithm: 所以这是功能算法:

List<Client> unflatten(List<IncomingFlatItem> flatItems) {
    return flatItems.parallelStream()
            .map(IncomingFlatItem::getClient)
            .collect(Collectors.groupingByConcurrent(client -> client))
            .entrySet().parallelStream()
            .map(kvp -> kvp.getValue()
                    .stream()
                    .reduce(new Client(), 
                            (client1, client2) -> {
                                    client1.getReports()
                                            .forEach(client2::addReport);
                                    client1.getEmailAddresses()
                                            .forEach(client2::addEmail);
                                    return client2;
                    }))
            .collect(Collectors.toList());
}

I took a long time due to going off on a tangent before I really understood reduce - I found a solution which passed my tests while using .stream() but totally failed with .parallelStream() hence its usage here. 在我真正了解reduce之前,我花了很长时间才开始切线 - 我找到了一个解决方案,它在使用.stream()时通过了我的测试但完全失败了.parallelStream()因此在这里使用它。 I had to use CopyOnWriteArrayList as well otherwise it would fall over randomly with ConcurrentModificationExceptions 我不得不使用CopyOnWriteArrayList ,否则会随机使用ConcurrentModificationExceptions

If you don't like to iterate over entry sets (don't want to handle Map.Entry ) or prefer a different solution without groupingBy , you can also use toMap with a merge function to aggregate your values. 如果您不想迭代条目集(不想处理Map.Entry )或者更喜欢不使用groupingBy的其他解决方案,您还可以使用带有合并功能的toMap来聚合您的值。 This approach works nicely because Client can hold the initial single item and the accumulated collection of all EmailAddress (Note: I used a utility function com.google.common.collectSets.union for conciseness, but you can just work with eg HashSet). 这种方法很有效,因为Client可以保存所有EmailAddress的初始单项和累积集合(注意:我使用实用程序函数com.google.common.collectSets.union来简洁,但您可以使用例如HashSet)。

The following code demonstrates how to do it (add Reports in the same manner as EmailAddress, and add the other fields you want). 以下代码演示了如何执行此操作(以与EmailAddress相同的方式添加Reports,并添加所需的其他字段)。 I left the merge function inline and did not add an AllArgsConstructor, but feel free to refactor. 我将合并函数保留为内联,并没有添加AllArgsConstructor,但可以随意重构。

static Client mapFlatItemToClient(final IncomingFlatItem item) {
    final Client client = new Client();
    client.clientCode = item.clientCode;
    client.emailAddresses = Collections.singleton(mapFlatItemToEmail(item));
    return client;
}

static EmailAddress mapFlatItemToEmail(final IncomingFlatItem item) {
    final EmailAddress address = new EmailAddress();
    address.emailAddress = item.emailAddress;
    return address;
}

public static void example() {
    final List<IncomingFlatItem> items = new ArrayList<>();

    // Aggregated Client Info by Client Code
    final Map<String, Client> intermediateResult = items.stream()
            .collect(
                    Collectors.<IncomingFlatItem, String, Client> toMap(
                            flat -> flat.clientCode,
                            flat -> mapFlatItemToClient(flat),
                            (lhs, rhs) -> {
                                final Client client = new Client();
                                client.clientCode = lhs.clientCode;
                                client.emailAddresses = Sets.union(lhs.emailAddresses, rhs.emailAddresses);
                                return client;
                            }));

    final Collection<Client> aggregatedValues = intermediateResult.values();
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM