2

I have two entities with one to many relationship which are joined together by composite primary keys. Since Spring Data generates wrong count distinct query for oracle database , I have SQL output with cartesian join which leads to repeating row for parent object for every row of child object. I need to find out distinct parent objects based on composite keys and then add each child object for parent in a list and set it as property of parent object

I am able to find out distinct parent object based on composite keys of the parent object. Following is the relevant code

private static <T> Predicate<T> distinctByKeys(Function<? super T, ?>... keyExtractors)
{
final Map<List<?>, Boolean> seen = new ConcurrentHashMap<>();
return t ->
{
final List<?> keys = Arrays.stream(keyExtractors)
.map(ke -> ke.apply(t))
collect(Collectors.toList());

return seen.putIfAbsent(keys, Boolean.TRUE) == null;
};
}

suppose I have the following input

list.add(new Book("Core Java", 200, new ArrayList(){{add("Page 1");}}));
list.add(new Book("Core Java", 200, new ArrayList(){{add("Page 2");}}));
list.add(new Book("Learning Freemarker", 150, new ArrayList(){{add("Page 15");}}));
list.add(new Book("Spring MVC", 300, new ArrayList(){{add("Page 16");}}));
list.add(new Book("Spring MVC", 300, new ArrayList(){{add("Page 17");}}));

I need to produce following output

Core Java,200, [Page 1, Page 2]
Learning Freemarker,150, [Page 15]
Spring MVC,300 , [Page 16, Page 17]

Any help in this regard will be very helpful

Ravindra Ranwala
  • 20,744
  • 6
  • 45
  • 63
Soumya
  • 51
  • 4
  • 1
    Did you try `Collections.groupingBy`? – Dmytro Chasovskyi Jul 26 '19 at 14:03
  • 1
    https://stackoverflow.com/questions/48603421/create-a-nested-parent-child-list-in-java-8 did help me. But the problem still persists as I am returning Spring Data nested projection as a return type in actual case. I guess Projections with nested collection in Spring Data is buggy any way. – Soumya Jul 26 '19 at 14:16
  • 2
    Are you aware that you are creating five obsolete classes? – Holger Jul 26 '19 at 16:18

1 Answers1

2

One option is to use Collectors.toMap() using the title and pages value as key. If you find duplicates you can merge both lists:

Collection<Book> result = list.stream()
        .collect(Collectors.toMap(
                b -> Map.entry(b.getTitle(), b.getPages()),
                b -> new Book(b.getTitle(), b.getPages(), b.getList()),
                (b1, b2) -> new Book(b1.getTitle(), b1.getPages(),
                        Stream.concat(b1.getList().stream(), b2.getList().stream()).collect(Collectors.toList())),
                LinkedHashMap::new))
        .values();

Alternatively you can use Collectors.groupingBy():

List<Book> result = list.stream().collect(
        Collectors.groupingBy(b -> Map.entry(b.getTitle(), b.getPages()), LinkedHashMap::new,
                Collectors.flatMapping(b -> b.getList().stream(), Collectors.toList())))
        .entrySet().stream()
        .map(e -> new Book(e.getKey().getKey(), e.getKey().getValue(), e.getValue()))
        .collect(Collectors.toList());

The second approach creates a Map using title and pages as key and merging the lists of all books using Collectors.flatMapping(). After that you map the entries back to your Book object.

If you can not use Collectors.flatMapping() use Collectors.mapping()` instead:

List<Book> r = list.stream().collect(
        Collectors.groupingBy(b -> Map.entry(b.getTitle(), b.getPages()), LinkedHashMap::new,
                Collectors.mapping(Book::getList, Collectors.toList())))
        .entrySet().stream()
        .map(e -> new Book(e.getKey().getKey(), e.getKey().getValue(), 
                e.getValue().stream().flatMap(Collection::stream).collect(Collectors.toList())))
        .collect(Collectors.toList());

If you can not use Map.entry() use new AbstractMap.SimpleEntry<>() instead.

The result in both cases will be this:

Book[title='Core Java', pages=200, list=[Page 1, Page 2]]
Book[title='Learning Freemarker', pages=150, list=[Page 15]]
Book[title='Spring MVC', pages=300, list=[Page 16, Page 17]]
Samuel Philipp
  • 10,631
  • 12
  • 36
  • 56
  • `Collectors.flatMapping` is only available in Java9 or higher versions. – Ravindra Ranwala Jul 26 '19 at 16:47
  • 1
    @RavindraRanwala take it from the end of [this answer](https://stackoverflow.com/a/39131049/2711488) – Holger Jul 26 '19 at 16:51
  • @RavindraRanwala in that case you can use `Collectors.mapping()`. Updated my answer. – Samuel Philipp Jul 26 '19 at 16:54
  • 2
    Alternatively, you can replace `Collectors.flatMapping(b -> b.getList().stream(), Collectors.toList())` with `Collector.of(ArrayList::new, (l, b) -> l.addAll(b.getList()), (l1,l2)-> { l1.addAll(l2); return l1; })`. But I prefer the backported version of `flatMapping`, which can get redirected to the builtin one in the future easily. – Holger Jul 26 '19 at 17:04
  • Solution works fine as long as Entity objects are returned. But in case return type is Spring Projection with subset of fields the nested collection becomes unmodifiable as Spring projection is Tuple based, hence immutable – Soumya Jul 29 '19 at 07:58