简体   繁体   中英

How to use Java 8 Streams groupingBy to map multiple database records to a single object with a List<String> property for one column

My problem essentially comes down to this simplified example. I have data coming back from a DB which has some duplicate information in the rows.

In this example I have a list of TeamRow objects that come back from the DB. I can easily group these using Collectors.groupingBy :

public class TeamRow {
    private int id;
    private String name;
    private String player;

    public TeamRow(int id, String name, String player) {
        this.id = id;
        this.name = name;
        this.player = player;
    }

    public int getId() {return id;}
    public String getName() { return name; }
    public String getPlayer() {return player;}
}

public class Team {
    private int id;
    private String name;
    private List<String> players;

    public Team(int id, String name, List<String> players) {
        this.id = id;
        this.name = name;
        this.players = new ArrayList<String>(players);
    }
}

List<TeamRow> dbTeams = new ArrayList<TeamRow>();
dbTeams.add(new TeamRow(1, "Team1", "Jonny"));
dbTeams.add(new TeamRow(1, "Team1", "Rob"));
dbTeams.add(new TeamRow(1, "Team1", "Carlos"));
dbTeams.add(new TeamRow(2, "Team2", "Shane"));
dbTeams.add(new TeamRow(2, "Team2", "Lucas"));
dbTeams.add(new TeamRow(3, "Team3", "Geraint"));
dbTeams.add(new TeamRow(3, "Team3", "Rocky"));
dbTeams.add(new TeamRow(3, "Team3", "Wayne"));
dbTeams.add(new TeamRow(3, "Team3", "Dwayne"));
dbTeams.add(new TeamRow(3, "Team3", "Lester"));

Map<Integer, List<TeamRow>> myMap = dbTeams.stream().collect(Collectors.groupingBy(TeamRow::getId));

However, what I'm actually trying to achieve is to convert the TeamRow s to Team s. So that the id and name are only represented once and the players are stored in a List in the Team object. I can achieve this by adding a forEach over the map as shown below.

But I've been trying to figure out if there is a way I can achieve the same result by adding some sort of mapper or downstream collector. Would this even offer any benefit over adding a subsequent forEach ?? Eg:

List<Team> teams = dbTeams.stream().collect(Collectors.groupingBy(TeamRow::getId, ???), ???).???; 

Conversion using forEach:

List<Team> teams = new ArrayList<>();
myMap.forEach((id, teamRows) -> {
    if (teamRows.size() > 0) {
        TeamRow tr = teamRows.get(0);
        List<String> players = teamRows.stream().map(TeamRow::getPlayer).collect(Collectors.toList());
        teams.add(new Team(id, tr.getName(), players));
    }
});

Previously I said I would do it by creating an atomic transformer function like this:

Function<TeamRow, Team> getTeamRowTransformer() {
    final Map<Integer, Team> map = new ConcurrentHashMap<Integer, Team>();
    return (teamRow)->{
        Team result = map.computeIfAbsent(teamRow.getId(), id->new Team(id, teamRow.getName(), Collections.emptyList()));
        result.players.add(teamRow.getPlayer());
        return result;
    };
}

It handles the mapping and your stream code becomes one very legible step:

Set<Team> finalTeams = dbTeams.stream()
    .map(getTeamRowTransformer())
    .collect(Collectors.toSet());

HOWEVER, I realized, you can also do this:

    List<Team> teams = dbTeams.stream()
        .map(tr->new Team(tr.getId(), tr.getName(), Arrays.asList(tr.getPlayer())))
        .collect(Collectors.collectingAndThen(
                    Collectors.groupingBy(t->t.id, 
                        Collectors.reducing((Team a, Team b)->{
                            a.players.addAll(b.players); 
                            return (Team)a;
                        })
                    ), m->m.values().stream()
                    .filter(Optional::isPresent)
                    .map(Optional::get)
                    .collect(Collectors.toList())
                )
        );

This way you never have an accessible mutable collection until List<Team> teams is assigned.

You may use toMap collector with custom merge function. It's probably a good idea to add merge method to the Team class:

public class Team {
    private final int id;
    private final String name;
    private final List<String> players;

    public Team(int id, String name, List<String> players) {
        this.id = id;
        this.name = name;
        this.players = new ArrayList<>(players);
    }

    // merges other team into this team, returning this team
    public Team merge(Team other) {
        assert id == other.id; // remove asserts if you don't like them
        assert name.equals(other.name);
        players.addAll(other.players);
        return this;
    }
}

Now you can solve your problem this way:

Collection<Team> teams = dbTeams.stream()
        .map(tr -> new Team(tr.id, tr.name, Arrays.asList(tr.player)))
        .collect(Collectors.toMap(t -> t.id, t -> t, Team::merge)).values();

You could try something like

    List<Team> teamList = dbTeams.stream().collect(Collectors.collectingAndThen(Collectors.groupingBy(TeamRow::getId),
            (m -> m.entrySet().stream().map(
            e -> {
                List<TeamRow> l = e.getValue();
                return new Team(l.get(0).getId(), l.get(0).getName(), l.stream().map(TeamRow::getPlayer).collect(Collectors.toList()));

            }
    ).collect(Collectors.toList()))));

Using collectingAndThen() you can use a function which maps the entries of the map to Team s. l.get(0) should not fail as there is always at least one entry in the list. I am not sure if this is more concise, but at least it does not use foreach .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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