[英]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. 在此示例中,我列出了从数据库返回的
TeamRow
对象。 I can easily group these using Collectors.groupingBy
: 我可以使用
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. 但是,我实际上想要实现的是将
TeamRow
转换为Team
。 So that the id and name are only represented once and the players are stored in a List in the Team
object. 这样,id和name只代表一次,而玩家则存储在
Team
对象的List中。 I can achieve this by adding a forEach
over the map as shown below. 我可以通过在地图上添加一个
forEach
来实现此目标,如下所示。
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
?? 与添加后续
forEach
带来任何好处吗? Eg: 例如:
List<Team> teams = dbTeams.stream().collect(Collectors.groupingBy(TeamRow::getId, ???), ???).???;
Conversion using forEach: 使用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. 这样,在分配
List<Team> teams
之前,您永远不会有可访问的可变集合。
You may use toMap
collector with custom merge function. 您可以使用带有自定义合并功能的
toMap
收集器。 It's probably a good idea to add merge
method to the Team
class: 将
merge
方法添加到Team
类中可能是一个好主意:
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. 使用
collectingAndThen()
,可以使用将映射项映射到Team
的函数。 l.get(0)
should not fail as there is always at least one entry in the list. l.get(0)
应该不会失败,因为列表中始终至少有一个条目。 I am not sure if this is more concise, but at least it does not use foreach
. 我不确定这是否更简洁,但至少它不使用
foreach
。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.