Как построить агрегаты из таблицы, содержащей SQL и NoSQL информацию на stream api?
я имею следующую таблицу, для примера. В ней 5 заявок orderId, каждая состоит из некоторого количества действий actionId где и отображен ход выполнения заявок:
|ordr| data |
| -- | ----------------------------------------------------------------------------------------- |
| 1 | {"actionId":1,"curNum":1,"maxNum":3,"status":"READY","templateId":4,"errMsg":null} |
| 1 | {"actionId":1,"curNum":1,"maxNum":3,"status":"SUCCESS","templateId":4,"errMsg":null} |
| 1 | {"actionId":2,"curNum":2,"maxNum":3,"status":"READY","templateId":4,"errMsg":null} |
| 1 | {"actionId":2,"curNum":2,"maxNum":3,"status":"SUCCESS","templateId":4,"errMsg":null} |
| 1 | {"actionId":3,"curNum":3,"maxNum":3,"status":"READY","templateId":4,"errMsg":null} |
| 1 | {"actionId":3,"curNum":3,"maxNum":3,"status":"SUCCESS","templateId":4,"errMsg":null} |
| 2 | {"actionId":4,"curNum":1,"maxNum":3,"status":"READY","templateId":5,"errMsg":null} |
| 2 | {"actionId":4,"curNum":1,"maxNum":3,"status":"SUCCESS","templateId":5,"errMsg":null} |
| 2 | {"actionId":5,"curNum":2,"maxNum":3,"status":"READY","templateId":5,"errMsg":null} |
| 2 | {"actionId":5,"curNum":2,"maxNum":3,"status":"SUCCESS","templateId":5,"errMsg":null} |
| 2 | {"actionId":6,"curNum":3,"maxNum":3,"status":"READY","templateId":5,"errMsg":null} |
| 2 | {"actionId":6,"curNum":3,"maxNum":3,"status":"SUCCESS","templateId":5,"errMsg":null} |
| 3 | {"actionId":7,"curNum":1,"maxNum":2,"status":"READY","templateId":6,"errMsg":null} |
| 3 | {"actionId":7,"curNum":1,"maxNum":2,"status":"FAIL","templateId":6,"errMsg":"BAD REQUEST"}|
| 4 | {"actionId":8,"curNum":1,"maxNum":2,"status":"UNKNOWN","templateId":7,"errMsg":null} |
| 4 | {"actionId":8,"curNum":1,"maxNum":2,"status":"UNKNOWN","templateId":7,"errMsg":null} |
| 5 | {"actionId":7,"curNum":1,"maxNum":2,"status":"READY","templateId":6,"errMsg":null} |
| 5 | {"actionId":7,"curNum":1,"maxNum":2,"status":"FAIL","templateId":6,"errMsg":"BAD REQUEST"}|
Как при помощи Stream Api получить отчет следующего вида (отобразил в таблице для наглядности):
| TemplateId | TotalRunOrder |SuccessRunOrder | FailRunOrder |FailOrdersInfo |
| ---------- | -------------- | -------------- | --------------|----------------|
| 4 | 1 | 1 | 0 | null |
| 5 | 1 | 1 | 0 | null |
| 6 | 2 | 0 | 2 | [{"OrderId":3,"ErrMsg":"BAD REQUEST"},{"OrderId":5,"ErrMsg":"BAD REQUEST"}]|
| 7 | 0 | 0 | 0 | null |
Пояснения, как считаются поля отчета:
TemplateId- берем из начальной таблицы из поляdataTotalRunOrder- если в рамках однойOrderIdесть хотя бы один статус"READY"SuccessRunOrder- ищем строку в рамках однойOrderId, гдеcurNum=maxNumи если в ней статус"SUCCESS"то считаем заявку успешнойFailRunOrder- если в рамках однойOrderIdесть хотя бы один статус"FAIL"то считаем заявку зафейленнойFailOrdersInfo- выводим инфу в виде массива, в котором показываем какиеOrderIdсвалились и показываемErrMsgтекст ошибки
Ответы (2 шт):
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
import static java.util.stream.Collectors.groupingBy;
public class Aaa {
public static void main(String[] args) {
var input = Stream.of(
new Data(1, 1, 3, "READY", 4, 1),
new Data(1, 1, 3, "SUCCESS", 4, 1),
new Data(2, 2, 3, "READY", 4, 1),
new Data(2, 2, 3, "SUCCESS", 4, 1),
new Data(3, 3, 3, "READY", 4, 1),
new Data(3, 3, 3, "SUCCESS", 4, 1),
new Data(4, 1, 3, "READY", 5, 2),
new Data(4, 1, 3, "SUCCESS", 5, 2),
new Data(5, 2, 3, "READY", 5, 2),
new Data(5, 2, 3, "SUCCESS", 5, 2),
new Data(6, 3, 3, "READY", 5, 2),
new Data(7, 3, 3, "SUCCESS", 5, 2)
);
Map<Integer, List<Data>> groupByTemplate = input.collect(groupingBy(Data::templateId));
Stream<Result> resultStream = groupByTemplate.entrySet().stream().map(e -> {
var totalRunOrder = e.getValue().stream()
.filter(d -> Objects.equals(d.status, "READY"))
.map(Data::ordr)
.distinct()
.count();
var successRunOrder = e.getValue().stream()
.filter(d -> Objects.equals(d.status, "SUCCESS") && d.curNum == d.maxNum)
.map(Data::ordr)
.distinct()
.count();
return new Result(e.getKey(), totalRunOrder, successRunOrder);
});
resultStream.forEach(System.out::println);
}
record Data(int actionId, int curNum, int maxNum, String status, int templateId, int ordr){}
record Result(int TemplateId, long totalRunOrd, long successRunOrder){}
}
В консоли получаем:
- Result[TemplateId=4, totalRunOrd=1, successRunOrder=1]
- Result[TemplateId=5, totalRunOrd=1, successRunOrder=1]
Остальные можно по аналогии сделать.
Для начала потребуется создать некий POJO/DTO класс, соответствующий JSON в поле data + поле orderId:
@Data
class RowEntry {
private int orderId;
private MyData data;
}
@Data
class MyData {
private int actionId;
private int curNum;
private int maxNum;
private int templateId;
private String status; // или какой-то enum
private String errMsg;
}
Данные вычитываются из таблицы и преобразовываются любым удобным способом типа JdbcTemplate + RowMapper, на выходе получается коллекция / список RowEntry:
public static List<RowEntry> readData() {
// ...
}
При первом проходе строится мапа Map<Integer, List<RowEntry>> с ключами templateId, и при втором проходе считается соответствующая статистика для каждого списка RowEntry:
public static List<Report> report(List<RowEntry> rows) {
return rows.stream() // Stream<RowEntry>
.collect(Collectors.groupingBy(
row -> row.getData().getTemplateId()
)) // Map<Integer, List<RowEntry>>
.entrySet()
.stream() // Stream<Map.Entry<Integer, List<RowEntry>>>
.map(Report::new)
.collect(Collectors.toList());
}
Остаётся определить класс Report, представляющий агрегированную статистику для списка ордеров/строк.
@Data class Report {
int templateId;
int totalRun;
int successRun;
int failRun;
List<FailInfo> failInfo;
}
@Data
@AllArgsConstructor
class FailInfo {
int orderId;
String errMsg;
}
Подсчёт статистики для входного параметра лучше/эффективнее реализовать в классическом стиле за один проход:
public Report(Map.Entry<Integer, List<RowEntry>> data) {
this.templateId = data.getKey();
// группировка по номеру ордера
Map<Integer, List<RowEntry>> mapStats = data.getValue() // List<RowEntry>
.stream()
.collect(Collectors.groupingBy(
RowEntry::getOrderId
));
totalRun = 0;
successRun = 0;
failRun = 0;
failInfo = null;
for (Map.Entry<Integer, List<RowEntry>> byOrder : mapStats.entrySet()) {
boolean hasReady = false;
boolean hasSuccess = false;
boolean hasFail = false;
List<FailInfo> fails = new ArrayList<>();
for (RowEntry re : byOrder.values()) {
String status = re.getData().getStatus();
if ("READY".equals(status)) {
hasReady = true;
} else if ("SUCCESS".equals(status) && re.getData().getCurNum() == re.getData().getMaxNum()) {
hasSuccess = true;
} else if ("FAIL".equals(status)) {
hasFail = true;
fails.add(new FailInfo(byOrder.getKey(), re.getData().getErrMsg()));
}
}
if (hasReady) totalRun++;
if (hasSuccess) successRun++;
if (hasFail) {
failRun++;
if (!fails.isEmpty()) {
if (failInfo == null) {
failInfo = new ArrayList<>(fails);
} else {
failInfo.addAll(fails);
}
}
}
}
}