Преобразовать плоский список в доменные объекты с дочерними объектами, используя потоки Java - PullRequest
5 голосов
/ 14 июня 2019

У меня есть входящие объекты с плоской ненормализованной структурой, которую я создал из результирующего набора JDBC.Входящие объекты зеркально отражают набор результатов, есть множество повторных данных, поэтому я хочу преобразовать данные в список родительских объектов с вложенными дочерними коллекциями, то есть граф объектов или нормализованный список.

Класс входящего объекта выглядит следующим образом:

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

Таким образом, входящие данные содержат несколько объектов для каждого клиента, которые я хотел бы объединить в один объект клиента, который содержит списокобъектов адреса электронной почты для клиента и список объектов отчета.

Таким образом, объект Client будет выглядеть следующим образом:

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

Странно, я не могу найти существующий ответ для этого.Я смотрю на вложенные потоки или цепочки потоков, но я бы хотел найти наиболее элегантный подход, и я определенно хочу избежать цикла for.

Ответы [ 5 ]

1 голос
/ 18 июня 2019

Спасибо всем ответчикам, которые упомянули Collectors.groupingBy().Это было ключом к настройке потока, где я мог бы использовать reduce().Я ошибочно полагал, что смогу использовать reduce сам по себе для решения проблемы, без groupingBy.

Спасибо также за предложение создать свободный API.Я добавил 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;
}

Я также создал методы .equals() и .hashCode() на основе бизнес-идентификаторов для Client, EmailAddress и Report в соответствии с рекомендациями @SamuelPhilip

Наконец, для объектов домена я создал .addReport(Report r) и .addEmail(EmailAddress e) в моем Client классе, который добавил бы дочерний объект к Client, если его еще нет.Я исключил тип коллекции Set для List, потому что стандарт модели домена - List, а Sets означало бы много преобразований в Lists.

Итак, код потока и лямбды выглядят лаконично.

Существует 3 шага:

  1. map IncomingFlatItems to Clients
  2. группировка Clients в карту по клиенту (в значительной степени полагаясь на * 1041)*)
  3. уменьшить каждую группу до одной Client

Так что это функциональный алгоритм:

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

У меня ушло много времени из-за выходана касательной, прежде чем я действительно понял reduce - я нашел решение, которое прошло мои тесты при использовании .stream(), но полностью провалилось с .parallelStream(), следовательно, его использование здесь.Я должен был использовать CopyOnWriteArrayList, иначе он случайно упадет с ConcurrentModificationExceptions

1 голос
/ 14 июня 2019

Вы можете сделать что-то в строках использования функции отображения для преобразования 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());

и далее, используя 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());
}
1 голос
/ 14 июня 2019

Вы можете использовать это:

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

Вначале вы группируете свои предметы по clientCode и clientName.После этого вы сопоставляете результаты с вашим Client объектом.

Убедитесь, что методы .equals() и hashCode() реализованы для EmailAddress и Report, чтобы гарантировать, что они различны в наборе.

1 голос
/ 14 июня 2019

Одна вещь, которую вы можете сделать, это использовать параметры конструктора и свободный API в ваших интересах. Мышление «вложенных» потоков и API потоков (с динамическими данными) могут очень быстро усложниться.

Это просто использует свободный 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) {

    }
}

И ...

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

Или вы можете просто использовать функции отображения, которые преобразуют IncomingFlatItem объекты в Client.

0 голосов
/ 14 июня 2019

Если вам не нравится перебирать наборы записей (не хотите обрабатывать Map.Entry) или предпочитаете другое решение без groupingBy, вы также можете использовать toMap с функцией слияния для агрегирования ваших значений , Этот подход хорошо работает, потому что Client может содержать начальный отдельный элемент и накопленную коллекцию всех EmailAddress (Примечание: я использовал вспомогательную функцию com.google.common.collectSets.union для краткости, но вы можете просто работать, например, с HashSet).

Следующий код демонстрирует, как это сделать (добавить отчеты таким же образом, как EmailAddress, и добавить другие поля, которые вы хотите). Я оставил встроенную функцию слияния и не добавил 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();
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...