I have a db table that repeats lots of data and is represented by the class FooEntity. Foo should actually have a 1-N relationship with Bar, but they're all stored in the same table replicating foo data. I can't change the table as it's already being used.
class FooEntity {
private Long id;
private Long keyValue;
private String fooField; //other foo fields, duplicated
private String barField; //other bar fields
}
I need to query data from this table and map them to a collection of DTOs that follows the 1-N relationship, eg
class FooDTO {
private String fooField; //other foo fields
private List<BarDTO> bars;
}
class BarDTO {
private Long id;
private String barField; //other bar fields
}
I also need to group this collection of FooDTO by their keyValue, putting them in a Map<Long, List<FooDTO>>.
I have found a solution but I'm not sure it's a good one, is there a better way? Especially one that doesn't keep opening and closing streams and doesn't need 3 dtos, as I think it can be done with 2. Should I use different collectors, or do these operations in a different order?
Here's my implementation
class FooKeyDTO {
String fooField; //other foo fields
}
class FooDTO extends FooKeyDTO {
private List<BarDTO> bars;
public FooDTO(FooKeyDTO foo, List<BarDTO> bars) {
this.fooField = foo.fooField; //other foo fields
this.bars = bars;
}
}
class BarDTO {
private Long id;
private String barField; //other bar fields
}
Map<Long, List<FooDTO>> value = jpaRepository
.findByMyField(myField) //Set<FooEntity>
.stream()
.collect(Collectors.groupingBy(s -> s.getKeyValue())) //Map<Long, List<FooEntity>>
.entrySet()
.stream()
.collect(Collectors.toMap(
Entry::getKey,
foo -> foo.getValue() //List<FooEntity>
.stream()
.collect(Collectors.groupingBy(
k -> toFooKeyDTO(k), //FooKeyDTO
Collectors.mapping(v -> toBarDTO(v), Collectors.toList()))) //List<BarDTO>
.entrySet() //entries of Map<FooKeyDTO, List<BarDTO>>
.stream()
.map(f -> new FooDTO(f.getKey(), f.getValue()))
.toList()
));
The query and the entity to dto converting methods are pretty simple and I don't think they're relevant to the question (I used a jpa repository and a model mapper to convert).
An example: From these entities
{
"id": 1,
"keyValue": 100,
"fooField": "foo1",
"barField": "bar1"
},
{
"id": 2,
"keyValue": 100,
"fooField": "foo1",
"barField": "bar2"
},
{
"id": 3,
"keyValue": 100,
"fooField": "foo2",
"barField": "bar3"
},
{
"id": 4,
"keyValue": 100,
"fooField": "foo2",
"barField": "bar4"
},
{
"id": 5,
"keyValue": 100,
"fooField": "foo2",
"barField": "bar5"
},
{
"id": 6,
"keyValue": 200,
"fooField": "foo3",
"barField": "bar6"
},
{
"id": 7,
"keyValue": 200,
"fooField": "foo3",
"barField": "bar7"
}
to this map
{
"100": [
{
"fooField": "foo1",
"bars": [
{
"id": 1,
"barField": "bar1"
},
{
"id": 2,
"barField": "bar2"
}
]
},
{
"fooField": "foo2",
"bars": [
{
"id": 3,
"barField": "bar3"
},
{
"id": 4,
"barField": "bar4"
},
{
"id": 5,
"barField": "bar5"
}
]
}
],
"200": [
{
"fooField": "foo3",
"bars": [
{
"id": 6,
"barField": "bar6"
},
{
"id": 7,
"barField": "bar7"
}
]
}
]
}
Here is one way to do it although I can't say it is better than yours. I am using records. Your classes with constructors and getters would also work.
record FooEntity(Long getId, Long getKeyValue, String getFooField, // other foo
// fields,
// duplicated
String getBarField) {
}
record BarDTO(Long getId, String getBarField // other bar fields
) {
}
record FooDTO(String getFooField, // other foo fields
List<BarDTO> getBars) {
}
A List of your FooEntity class
List<FooEntity> list = List.of(new FooEntity(1L, 100L, "foo1", "bar1"),
new FooEntity(2L, 100L, "foo1", "bar2"),
new FooEntity(3L, 100L, "foo2", "bar3"),
new FooEntity(4L, 100L, "foo2", "bar4"),
new FooEntity(5L, 100L, "foo2", "bar5"),
new FooEntity(6L, 200L, "foo3", "bar6"),
new FooEntity(7L, 200L, "foo3", "bar7"));
In a two stage process, this builds a map of maps using computeIfAbsent which creates the entry if the key doesn't exist and returns the new or existing value.
keyValue
fooField
FooDTO
Map<Long, Map<String, FooDTO>> mapTemp = new HashMap<>();
for (FooEntity fe : list) {
mapTemp.computeIfAbsent(fe.getKeyValue(), v -> new HashMap<>())
.computeIfAbsent(fe.getFooField(),
v -> new FooDTO(fe.getFooField(), // FooDTO instance
new ArrayList<>()))
.getBars() // get the list from FooDTO
// and add a new BarDTO instance
.add(new BarDTO(fe.getId(),
fe.getBarField()));
}
This prints the map
mapTemp.entrySet().forEach(e -> {
System.out.println(e.getKey());
e.getValue().entrySet().forEach(ee -> {
System.out.println(" " + ee.getKey());
ee.getValue().getBars()
.forEach(v -> System.out.println(" " + v));
});
});
prints
100
foo1
BarDTO[getId=1, getBarField=bar1]
BarDTO[getId=2, getBarField=bar2]
foo2
BarDTO[getId=3, getBarField=bar3]
BarDTO[getId=4, getBarField=bar4]
BarDTO[getId=5, getBarField=bar5]
200
foo3
BarDTO[getId=6, getBarField=bar6]
BarDTO[getId=7, getBarField=bar7]
It's possible the above will be sufficient for your use. However, to convert that to the desired map, the following will work.
Map<Long, List<FooDTO>> result = mapTemp.entrySet().stream()
.collect(Collectors.toMap(
e -> e.getKey(),
e -> e.getValue().entrySet()
.stream()
.map(ee -> ee.getValue()).toList()));
And it can be printed with the following, the output looking exactly like the above.
result.entrySet().forEach(e -> {
System.out.println(e.getKey());
e.getValue().forEach(ob -> {
System.out.println(" " + ob.getFooField());
ob.getBars().forEach(b -> System.out.println(" " + b));
});
});