Archange Archange - 6 months ago 22
Java Question

Java 8 Streams : build multilevel / composite objets

I'm starting using java 8 strream API. Still novice.
I would like to convert a list of "sql result set" to domain objects, i.e composite structure.

Domain objects : a user has a collection of permissions, each permission has a collection of year of applications.
For example, John has 2 permissions (MODERATOR and DEV).
its moderator permission is only applicable for 2014 and 2015
its dev permission of only applicable for 2014.

class User {
// some primtives attributes
List<Permission> permission;
}

class Permission {
// some primitives attributes
List<Integer> years;
}


Now I make a query and got a list of flat results, something like :

[1, "moderator", 2014]
[1, "moderator", 2015]
[1, "dev", 2014]
[2, "dev", 2010]
[2, "dev", 2011]
[2, "dev", 2012]


The 1 and 2 are userId.

I tried various construction but at the end it's more complex than fluent. And didn't work :)

I read in a Java 8 book that it's "simple" to build dompain objects with collectors.

I cried a little when I read that :'(

I tried

sb.collect(
collectingAndThen(
groupingBy(
Mybean::getUserId,
collectingAndThen(
groupingBy(Monbean::getPermissionId, mapping(convertPermission, toList())),
finisherFonction)
),
convertUser)
);


and got one hell of generics compilation failure.


  • what's the best way to construct multi level composite domain objects using java 8 streams ?

  • is collectionAndThen / finisher a good idea ?

  • or do you use only groupingBy followed by a mapping function ?

  • do you transform the classifier to an object (sort of first level mapping function ?)



Because at the end I want to get rid of the Map and got a List result (I think i can add a map call on the entrySet to finish the transformation).

thanks in advance for your help

++

Answer

Let me offer you a few options and you decide which looks most clear to you. I am assuming that User constructor is User(int userId, List<Permission> permissions) and Permission constructor is Permission(String permissionId, List<Integer> years)

Option 1: The direct approach. Group by userid, construct a list of permissions for each userid and make User objects. Personally, I find this much nesting in collectors to be hard to follow.

List<User> users = beans.stream()
    .collect(
        groupingBy(
            MyBean::getUserid,
            collectingAndThen(
                groupingBy(
                    MyBean::getPermission,
                    mapping(MyBean::getYear, toList())
                ),
                t -> t.entrySet().stream()
                    .map(e -> new Permission(e.getKey(), e.getValue()))
                    .collect(toList())
            )
        )
    ).entrySet().stream()
    .map(e -> new User(e.getKey(), e.getValue()))
    .collect(toList());

Option 2: Same as above but make the permission collector separately for clarity.

Collector<MyBean, ?, List<Permission>> collectPermissions = collectingAndThen(
    groupingBy(MyBean::getPermission, mapping(MyBean::getYear, toList())),
    t -> t.entrySet().stream()
        .map(e -> new Permission(e.getKey(), e.getValue()))
        .collect(toList())
);

List<User> users = beans.stream()
    .collect(groupingBy(MyBean::getUserid, collectPermissions))
    .entrySet().stream()
    .map(e -> new User(e.getKey(), e.getValue()))
    .collect(toList());

Option 3: First roll the beans into a map of userid to map of permissionid to list of years (Map<Integer, Map<String, List<Integer>>). Then construct the domain objects out of the map

List<User> users = beans.stream().collect(
    groupingBy(
        MyBean::getUserid,
        groupingBy(
            MyBean::getPermission,
            mapping(MyBean::getYear, toList())
        )
    )
).entrySet().stream()
    .map(u -> new User(
            u.getKey(),
            u.getValue().entrySet().stream()
                .map(p -> new Permission(p.getKey(), p.getValue()))
                .collect(toList())
        )
    ).collect(toList());