feat: add bulk result persistence and macro scene selection

parent 2c825897
package com.aps.common.util;
import org.springframework.stereotype.Service;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;
/** Shared JDBC batch insert helper for large result sets. */
@Service
public class JdbcBatchInsertService {
public static final int DEFAULT_BATCH_SIZE = 500;
private final DataSource dataSource;
public JdbcBatchInsertService(DataSource dataSource) {
this.dataSource = dataSource;
}
/** Executes multiple batch inserts on one connection and one transaction. */
public int executeInTransaction(TransactionWork work) {
try (Connection connection = dataSource.getConnection()) {
boolean previousAutoCommit = connection.getAutoCommit();
connection.setAutoCommit(false);
try {
int count = work.execute(connection);
connection.commit();
connection.setAutoCommit(previousAutoCommit);
return count;
} catch (Exception e) {
connection.rollback();
connection.setAutoCommit(previousAutoCommit);
throw e;
}
} catch (Exception e) {
throw new IllegalStateException("JDBC transaction failed", e);
}
}
public <T> int batchInsert(String sql, List<T> rows, BatchBinder<T> binder) {
return batchInsert(sql, rows, DEFAULT_BATCH_SIZE, binder);
}
public <T> int batchInsert(String sql, List<T> rows, int batchSize, BatchBinder<T> binder) {
if (rows == null || rows.isEmpty()) {
return 0;
}
if (batchSize <= 0) {
throw new IllegalArgumentException("batchSize must be greater than zero");
}
try (Connection connection = dataSource.getConnection()) {
boolean previousAutoCommit = connection.getAutoCommit();
connection.setAutoCommit(false);
try {
int count = batchInsert(connection, sql, rows, batchSize, binder);
connection.commit();
connection.setAutoCommit(previousAutoCommit);
return count;
} catch (Exception e) {
connection.rollback();
connection.setAutoCommit(previousAutoCommit);
throw e;
}
} catch (SQLException e) {
throw new IllegalStateException("JDBC batch insert failed", e);
}
}
public <T> int batchInsert(Connection connection, String sql, List<T> rows,
int batchSize, BatchBinder<T> binder) throws SQLException {
if (rows == null || rows.isEmpty()) {
return 0;
}
if (batchSize <= 0) {
throw new IllegalArgumentException("batchSize must be greater than zero");
}
int pending = 0;
int inserted = 0;
try (PreparedStatement statement = connection.prepareStatement(sql)) {
for (T row : rows) {
binder.bind(statement, row);
statement.addBatch();
pending++;
if (pending == batchSize) {
inserted += countBatch(statement.executeBatch());
pending = 0;
}
}
if (pending > 0) {
inserted += countBatch(statement.executeBatch());
}
}
return inserted;
}
private int countBatch(int[] results) {
int count = 0;
for (int result : results) {
if (result >= 0) {
count += result;
} else {
count++;
}
}
return count;
}
@FunctionalInterface
public interface BatchBinder<T> {
void bind(PreparedStatement statement, T row) throws SQLException;
}
@FunctionalInterface
public interface TransactionWork {
int execute(Connection connection) throws Exception;
}
}
...@@ -13,6 +13,7 @@ import com.aps.macroplanner.data.TestDataBuilder; ...@@ -13,6 +13,7 @@ import com.aps.macroplanner.data.TestDataBuilder;
import com.aps.macroplanner.output.ResultWriter; import com.aps.macroplanner.output.ResultWriter;
import com.aps.macroplanner.output.dto.*; import com.aps.macroplanner.output.dto.*;
import com.aps.service.MacroPlannerResultService; import com.aps.service.MacroPlannerResultService;
import com.aps.service.MpPispipResultPersistenceService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.ortools.Loader; import com.google.ortools.Loader;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
...@@ -60,6 +61,9 @@ import java.util.stream.Collectors; ...@@ -60,6 +61,9 @@ import java.util.stream.Collectors;
@Slf4j @Slf4j
public class MacroPlannerResultController { public class MacroPlannerResultController {
@Autowired
private MpPispipResultPersistenceService mpPispipResultPersistenceService;
@Autowired @Autowired
private MacroPlannerResultService macroPlannerResultService; private MacroPlannerResultService macroPlannerResultService;
...@@ -401,7 +405,11 @@ public class MacroPlannerResultController { ...@@ -401,7 +405,11 @@ public class MacroPlannerResultController {
// 4. 保存结果到 JSON 文件 // 4. 保存结果到 JSON 文件
ResultWriter writer = new ResultWriter(optimizer.getModel(), optimizer.getData(), solveStart); ResultWriter writer = new ResultWriter(optimizer.getModel(), optimizer.getData(), solveStart);
boolean saved = writer.saveResultToFile(sid); OptimizationResult optimizationResult = writer.buildResult();
boolean saved = writer.saveResultToFile(sid, optimizationResult);
if (saved) {
mpPispipResultPersistenceService.save(sid, optimizationResult);
}
result.put("status", saved ? "SUCCESS" : "SAVE_FAILED"); result.put("status", saved ? "SUCCESS" : "SAVE_FAILED");
result.put("solveElapsedMs", solveEnd - solveStart); result.put("solveElapsedMs", solveEnd - solveStart);
......
...@@ -7,7 +7,6 @@ import com.aps.macroplanner.scene.MacroSceneService; ...@@ -7,7 +7,6 @@ import com.aps.macroplanner.scene.MacroSceneService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
...@@ -37,13 +36,20 @@ public class MacroSceneController { ...@@ -37,13 +36,20 @@ public class MacroSceneController {
return R.ok(macroSceneService.listScenes()); return R.ok(macroSceneService.listScenes());
} }
@PostMapping("/select")
@Operation(summary = "选择当前主计划场景")
public R<Boolean> select(@RequestBody java.util.Map<String, String> request) {
macroSceneService.selectScene(request.get("sceneId"), request.get("userId"));
return R.ok(true);
}
@GetMapping("/{sceneId}") @GetMapping("/{sceneId}")
@Operation(summary = "查询主计划场景详情") @Operation(summary = "查询主计划场景详情")
public R<MacroSceneConfig> get(@PathVariable String sceneId) { public R<MacroSceneConfig> get(@PathVariable String sceneId) {
return R.ok(macroSceneService.getScene(sceneId)); return R.ok(macroSceneService.getScene(sceneId));
} }
@DeleteMapping("/{sceneId}") @PostMapping("/delete/{sceneId}")
@Operation(summary = "删除主计划场景", description = "删除场景配置及该场景下复制的数据") @Operation(summary = "删除主计划场景", description = "删除场景配置及该场景下复制的数据")
public R<Boolean> delete(@PathVariable String sceneId) { public R<Boolean> delete(@PathVariable String sceneId) {
macroSceneService.deleteScene(sceneId); macroSceneService.deleteScene(sceneId);
......
...@@ -32,4 +32,6 @@ public class MacroSceneConfig { ...@@ -32,4 +32,6 @@ public class MacroSceneConfig {
private String updateUser; private String updateUser;
@Schema(description = "更新时间") @Schema(description = "更新时间")
private LocalDateTime updateTime; private LocalDateTime updateTime;
@Schema(description = "当前选择该场景的用户 ID,多个用户用逗号分隔")
private String selectedUserIds;
} }
...@@ -102,7 +102,10 @@ public class ResultWriter { ...@@ -102,7 +102,10 @@ public class ResultWriter {
* 将结果保存到 JSON 文件 * 将结果保存到 JSON 文件
*/ */
public boolean saveResultToFile(String sceneId) { public boolean saveResultToFile(String sceneId) {
OptimizationResult result = buildResult(); return saveResultToFile(sceneId, buildResult());
}
public boolean saveResultToFile(String sceneId, OptimizationResult result) {
if (result == null) { if (result == null) {
logger.warn("对象不能为空"); logger.warn("对象不能为空");
return false; return false;
...@@ -239,7 +242,7 @@ public class ResultWriter { ...@@ -239,7 +242,7 @@ public class ResultWriter {
// ==================== 构建 OptimizationResult ==================== // ==================== 构建 OptimizationResult ====================
private OptimizationResult buildResult() { public OptimizationResult buildResult() {
OptimizationResult result = new OptimizationResult(); OptimizationResult result = new OptimizationResult();
// 元数据 // 元数据
......
package com.aps.macroplanner.scene;
import com.aps.entity.MacroSceneConfig;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.sql.DataSource;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@Service
public class MacroSceneSelectionService {
private final JdbcTemplate jdbc;
public MacroSceneSelectionService(DataSource dataSource) {
this.jdbc = new JdbcTemplate(dataSource);
}
@Transactional(rollbackFor = Exception.class)
public String select(String sceneId, String userId) {
return switchSelection(sceneId, userId, false);
}
@Transactional(rollbackFor = Exception.class)
public String completeAndSelect(String sceneId, String userId) {
return switchSelection(sceneId, userId, true);
}
private String switchSelection(String sceneId, String userId, boolean complete) {
validateUserId(userId);
if (sceneId == null || sceneId.trim().isEmpty()) {
throw new IllegalArgumentException("sceneId cannot be blank");
}
String scene = sceneId.trim();
String user = userId.trim();
// Serialize cross-row membership changes across all application instances.
jdbc.execute("LOCK TABLE MP_SCENE_CONFIG IN EXCLUSIVE MODE WAIT 5");
List<MacroSceneConfig> scenes = jdbc.query(
"SELECT SCENE_ID, SCENE_STATUS, SELECTED_USER_IDS FROM MP_SCENE_CONFIG",
new BeanPropertyRowMapper<>(MacroSceneConfig.class));
MacroSceneConfig target = scenes.stream().filter(s -> scene.equals(s.getSceneId()))
.findFirst().orElseThrow(() -> new IllegalArgumentException("scene does not exist: " + scene));
if (!(complete ? "COPYING" : "READY").equals(target.getSceneStatus())) {
throw new IllegalArgumentException("scene is not ready for selection: " + scene);
}
String selected = null;
for (MacroSceneConfig row : scenes) {
String updated = updatedUsers(row.getSelectedUserIds(), user, scene.equals(row.getSceneId()));
if (!Objects.equals(updated, row.getSelectedUserIds())) {
jdbc.update("UPDATE MP_SCENE_CONFIG SET SELECTED_USER_IDS = ? WHERE SCENE_ID = ?",
updated, row.getSceneId());
}
if (scene.equals(row.getSceneId())) selected = updated;
}
if (complete) {
jdbc.update("UPDATE MP_SCENE_CONFIG SET SCENE_STATUS = 'READY', UPDATE_TIME = CURRENT_TIMESTAMP "
+ "WHERE SCENE_ID = ?", scene);
}
return selected;
}
static void validateUserId(String userId) {
if (userId == null || userId.trim().isEmpty() || userId.contains(",")
|| userId.trim().getBytes(StandardCharsets.UTF_8).length > 2000) {
throw new IllegalArgumentException("userId must be a nonblank single ID, at most 2000 bytes");
}
}
static String updatedUsers(String value, String user, boolean selected) {
Set<String> users = new LinkedHashSet<>();
if (value != null) {
for (String id : value.split(",")) {
String normalized = id.trim();
if (!normalized.isEmpty()) users.add(normalized);
}
}
if (selected) users.add(user); else users.remove(user);
String updated = String.join(",", users);
if (updated.getBytes(StandardCharsets.UTF_8).length > 2000) {
throw new IllegalArgumentException("selected user IDs exceed the scene field capacity");
}
return updated.isEmpty() ? null : updated;
}
}
...@@ -49,6 +49,7 @@ public class MacroSceneService { ...@@ -49,6 +49,7 @@ public class MacroSceneService {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final DataSource dataSource; private final DataSource dataSource;
private final MacroSceneSelectionService selectionService;
private final MacroSceneConfigMapper sceneConfigMapper; private final MacroSceneConfigMapper sceneConfigMapper;
private final MacroSceneSequenceMapper sequenceMapper; private final MacroSceneSequenceMapper sequenceMapper;
private final ApsDemandOrderMapper apsDemandOrderMapper; private final ApsDemandOrderMapper apsDemandOrderMapper;
...@@ -76,6 +77,10 @@ public class MacroSceneService { ...@@ -76,6 +77,10 @@ public class MacroSceneService {
return sceneConfigMapper.selectById(requireText(sceneId, "sceneId")); return sceneConfigMapper.selectById(requireText(sceneId, "sceneId"));
} }
public void selectScene(String sceneId, String userId) {
selectionService.select(sceneId, userId);
}
public synchronized MacroSceneConfig createScene(MacroSceneCreateRequest request) { public synchronized MacroSceneConfig createScene(MacroSceneCreateRequest request) {
long createStartNanos = System.nanoTime(); long createStartNanos = System.nanoTime();
if (request == null) { if (request == null) {
...@@ -83,6 +88,7 @@ public class MacroSceneService { ...@@ -83,6 +88,7 @@ public class MacroSceneService {
} }
String sceneName = requireText(request.getSceneName(), "sceneName"); String sceneName = requireText(request.getSceneName(), "sceneName");
String userId = requireText(request.getUserId(), "userId"); String userId = requireText(request.getUserId(), "userId");
MacroSceneSelectionService.validateUserId(userId);
String sourceSceneId = normalize(request.getSourceSceneId()); String sourceSceneId = normalize(request.getSourceSceneId());
if (sourceSceneId != null && sceneConfigMapper.selectById(sourceSceneId) == null) { if (sourceSceneId != null && sceneConfigMapper.selectById(sourceSceneId) == null) {
throw new IllegalArgumentException("source macro scene does not exist: " + sourceSceneId); throw new IllegalArgumentException("source macro scene does not exist: " + sourceSceneId);
...@@ -110,9 +116,10 @@ public class MacroSceneService { ...@@ -110,9 +116,10 @@ public class MacroSceneService {
try { try {
cloneSceneData(sourceSceneId, sceneId); cloneSceneData(sourceSceneId, sceneId);
String selectedUsers = selectionService.completeAndSelect(sceneId, userId);
config.setSceneStatus("READY"); config.setSceneStatus("READY");
config.setUpdateTime(LocalDateTime.now()); config.setUpdateTime(LocalDateTime.now());
sceneConfigMapper.updateById(config); config.setSelectedUserIds(selectedUsers);
long durationMs = elapsedMillis(createStartNanos); long durationMs = elapsedMillis(createStartNanos);
log.info("主计划场景创建完成,场景ID:{},场景名称:{},耗时:{}毫秒", log.info("主计划场景创建完成,场景ID:{},场景名称:{},耗时:{}毫秒",
sceneId, sceneName, durationMs); sceneId, sceneName, durationMs);
......
package com.aps.service;
import com.aps.common.util.JdbcBatchInsertService;
import com.aps.macroplanner.output.dto.OptimizationResult;
import com.aps.macroplanner.output.dto.PispipResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.Types;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Service
@RequiredArgsConstructor
@Slf4j
public class MpPispipResultPersistenceService {
private static final int BATCH_SIZE = 500;
private final DataSource dataSource;
private final JdbcBatchInsertService batch;
public int save(String sceneId, OptimizationResult result) throws Exception {
if (result == null || result.getPispips() == null || result.getPispips().isEmpty()) return 0;
String parent = "INSERT INTO MP_PISPIP_RESULT (PISPIP_ID,SCENE_ID,PRODUCT_ID,PRODUCT_CODE,SP_ID,SP_NAME,PERIOD_INDEX,PERIOD_START_DATE,OPENING_INVENTORY,ENDING_INVENTORY,TARGET_INVENTORY_LEVEL,TARGET_INVENTORY_DAYS,BELOW_TARGET,MIN_INVENTORY_LEVEL,MIN_INVENTORY_DAYS,BELOW_MIN,MAX_INVENTORY_LEVEL,MAX_INVENTORY_DAYS,ABOVE_MAX,PRODUCTION_ARRIVED,IN_TRANSIT_ARRIVAL,TOTAL_INFLOW,SALES_DEMAND_QTY,SALES_FULFILLED_QTY,DEPENDENT_DEMAND_QTY,TOTAL_OUTFLOW,DEMAND_FULFILLMENT,DEMAND_SLACK,PRODUCTION_IN_PROGRESS) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
String prod = "INSERT INTO MP_PISPIP_PRODUCTION (PRODUCTION_ID,PISPIP_ID,SCENE_ID,OPERATION_ID,OPERATION_NAME,UNIT_ID,UNIT_NAME,QUANTITY,LEAD_TIME_DAYS) VALUES (?,?,?,?,?,?,?,?,?)";
String bom = "INSERT INTO MP_PISPIP_BOM (BOM_ID,PRODUCTION_ID,PISPIP_ID,SCENE_ID,INPUT_PRODUCT_ID,INPUT_SP_ID,INPUT_SP_NAME,FACTOR,CONSUMED_QTY) VALUES (?,?,?,?,?,?,?,?,?)";
Map<PispipResult,String> ids = new IdentityHashMap<>();
List<PispipResult.ProductionDetail> details = new ArrayList<>();
List<PispipResult.BomConsumption> boms = new ArrayList<>();
Map<Object,String> detailIds = new IdentityHashMap<>();
int saved = batch.executeInTransaction(c -> {
batch.batchInsert(c, parent, result.getPispips(), BATCH_SIZE, (s,p) -> bindParent(s,p,sceneId,ids));
for (PispipResult p : result.getPispips()) for (PispipResult.ProductionDetail d : p.getProductionDetails()) { details.add(d); detailIds.put(d, UUID.randomUUID().toString()); boms.addAll(d.bomConsumptions); }
batch.batchInsert(c, prod, details, BATCH_SIZE, (s,d) -> { String pid = owner(ids,d); s.setString(1,detailIds.get(d)); s.setString(2,pid); s.setString(3,sceneId); s.setString(4,d.operationId); s.setString(5,d.operationName); s.setString(6,d.unitId); s.setString(7,d.unitName); s.setDouble(8,d.quantity); s.setInt(9,d.leadTimeDays); });
batch.batchInsert(c, bom, boms, BATCH_SIZE, (s,b) -> { String did = detailIds.get(findDetail(details,b)); String pid = owner(ids,findDetail(details,b)); s.setString(1,UUID.randomUUID().toString()); s.setString(2,did); s.setString(3,pid); s.setString(4,sceneId); s.setString(5,b.inputProductId); s.setString(6,b.inputSpId); s.setString(7,b.inputSpName); s.setDouble(8,b.factor); s.setDouble(9,b.consumedQty); });
return ids.size();
});
log.info("Saved PISPIP result: sceneId={}, parents={}, productions={}, boms={}",sceneId,ids.size(),details.size(),boms.size());
return saved;
}
private String owner(Map<PispipResult,String> ids,Object d){ for(Map.Entry<PispipResult,String> e:ids.entrySet()) if(e.getKey().getProductionDetails().contains(d)) return e.getValue(); return null; }
private PispipResult.ProductionDetail findDetail(List<PispipResult.ProductionDetail> ds,PispipResult.BomConsumption b){ for(PispipResult.ProductionDetail d:ds) if(d.bomConsumptions.contains(b)) return d; return null; }
private void bindParent(PreparedStatement s,PispipResult p,String scene,Map<PispipResult,String> ids) throws java.sql.SQLException { ids.put(p,UUID.randomUUID().toString()); int i=1; s.setString(i++,ids.get(p)); s.setString(i++,scene); s.setString(i++,p.getProductId()); s.setString(i++,p.getProductCode()); s.setString(i++,p.getSpId()); s.setString(i++,p.getSpName()); s.setInt(i++,p.getPeriodIndex()); s.setDate(i++,date(p.getPeriodStartDate())); for(Double v:new Double[]{p.getOpeningInventory(),p.getEndingInventory(),p.getTargetInventoryLevel(),p.getTargetInventoryDays(),p.getBelowTarget(),p.getMinInventoryLevel(),p.getMinInventoryDays(),p.getBelowMin(),p.getMaxInventoryLevel(),p.getMaxInventoryDays(),p.getAboveMax(),p.getProductionArrived(),p.getInTransitArrival(),p.getTotalInflow(),p.getSalesDemandQty(),p.getSalesFulfilledQty(),p.getDependentDemandQty(),p.getTotalOutflow(),p.getDemandFulfillment(),p.getDemandSlack(),p.getProductionInProgress()}){if(v==null)s.setNull(i++,Types.NUMERIC);else s.setDouble(i++,v);}}
private Date date(String v){return v==null||v.trim().isEmpty()?null:Date.valueOf(LocalDate.parse(v));}
}
package com.aps.benchmark;
import com.aps.common.util.JdbcBatchInsertService;
import oracle.jdbc.pool.OracleDataSource;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
/** Manual Oracle benchmark for the shared JDBC batch insert service. */
public final class PispipBatchInsertBenchmark {
private static final String PISPIP_SQL =
"INSERT INTO MP_PISPIP_RESULT (PISPIP_ID, SCENE_ID, PRODUCT_ID, PRODUCT_CODE, SP_ID, "
+ "SP_NAME, PERIOD_INDEX, PERIOD_START_DATE, OPENING_INVENTORY, ENDING_INVENTORY, "
+ "TARGET_INVENTORY_LEVEL, TARGET_INVENTORY_DAYS, BELOW_TARGET, MIN_INVENTORY_LEVEL, "
+ "MIN_INVENTORY_DAYS, BELOW_MIN, MAX_INVENTORY_LEVEL, MAX_INVENTORY_DAYS, ABOVE_MAX, "
+ "PRODUCTION_ARRIVED, IN_TRANSIT_ARRIVAL, TOTAL_INFLOW, SALES_DEMAND_QTY, "
+ "SALES_FULFILLED_QTY, DEPENDENT_DEMAND_QTY, TOTAL_OUTFLOW, DEMAND_FULFILLMENT, "
+ "DEMAND_SLACK, PRODUCTION_IN_PROGRESS) VALUES "
+ "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
private static final String PRODUCTION_SQL =
"INSERT INTO MP_PISPIP_PRODUCTION (PRODUCTION_ID, PISPIP_ID, SCENE_ID, OPERATION_ID, "
+ "OPERATION_NAME, UNIT_ID, UNIT_NAME, QUANTITY, LEAD_TIME_DAYS) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
private static final String BOM_SQL =
"INSERT INTO MP_PISPIP_BOM (BOM_ID, PRODUCTION_ID, PISPIP_ID, SCENE_ID, INPUT_PRODUCT_ID, "
+ "INPUT_SP_ID, INPUT_SP_NAME, FACTOR, CONSUMED_QTY) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
private PispipBatchInsertBenchmark() {
}
public static void main(String[] args) throws Exception {
if (args.length < 3) {
throw new IllegalArgumentException("Usage: <jdbcUrl> <username> <password> [rowCount] [batchSize] [keepData]");
}
int rowCount = args.length > 3 ? Integer.parseInt(args[3]) : 100_000;
int batchSize = args.length > 4 ? Integer.parseInt(args[4]) : 500;
boolean keepData = args.length > 5 && Boolean.parseBoolean(args[5]);
String sceneId = "PISPIP_BENCH_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
OracleDataSource dataSource = new OracleDataSource();
dataSource.setURL(args[0]);
dataSource.setUser(args[1]);
dataSource.setPassword(args[2]);
JdbcBatchInsertService batchService = new JdbcBatchInsertService(dataSource);
List<Integer> rows = new ArrayList<>(rowCount);
for (int i = 0; i < rowCount; i++) {
rows.add(i);
}
try (Connection connection = dataSource.getConnection()) {
ensureSchema(connection);
connection.setAutoCommit(false);
long totalStart = System.nanoTime();
long start = System.nanoTime();
int parentInserted = batchService.batchInsert(connection, PISPIP_SQL, rows, batchSize,
(statement, index) -> bindPispip(statement, sceneId, index));
long parentMs = elapsedMillis(start);
start = System.nanoTime();
int productionInserted = batchService.batchInsert(connection, PRODUCTION_SQL, rows, batchSize,
(statement, index) -> bindProduction(statement, sceneId, index));
long productionMs = elapsedMillis(start);
start = System.nanoTime();
int bomInserted = batchService.batchInsert(connection, BOM_SQL, rows, batchSize,
(statement, index) -> bindBom(statement, sceneId, index));
long bomMs = elapsedMillis(start);
start = System.nanoTime();
connection.commit();
long commitMs = elapsedMillis(start);
long totalMs = elapsedMillis(totalStart);
long parentCount = count(connection, "MP_PISPIP_RESULT", sceneId);
long productionCount = count(connection, "MP_PISPIP_PRODUCTION", sceneId);
long bomCount = count(connection, "MP_PISPIP_BOM", sceneId);
long productionOrphans = productionOrphans(connection, sceneId);
long bomOrphans = bomOrphans(connection, sceneId);
System.out.println("BENCHMARK_SCENE_ID=" + sceneId);
System.out.println("ROW_GROUPS=" + rowCount + ", TOTAL_DATABASE_ROWS=" + (rowCount * 3L));
System.out.println("BATCH_SIZE=" + batchSize);
System.out.println("PISPIP_INSERTED=" + parentInserted + ", DB_COUNT=" + parentCount + ", MS=" + parentMs);
System.out.println("PRODUCTION_INSERTED=" + productionInserted + ", DB_COUNT=" + productionCount + ", MS=" + productionMs);
System.out.println("BOM_INSERTED=" + bomInserted + ", DB_COUNT=" + bomCount + ", MS=" + bomMs);
System.out.println("COMMIT_MS=" + commitMs + ", TOTAL_MS=" + totalMs);
System.out.println("ROWS_PER_SECOND=" + Math.round((rowCount * 3000.0) / Math.max(totalMs, 1L)));
System.out.println("PRODUCTION_ORPHANS=" + productionOrphans + ", BOM_ORPHANS=" + bomOrphans);
if (!keepData) {
long cleanupStart = System.nanoTime();
cleanup(connection, sceneId);
connection.commit();
System.out.println("CLEANUP_MS=" + elapsedMillis(cleanupStart));
System.out.println("COUNTS_AFTER_CLEANUP=" + count(connection, "MP_PISPIP_RESULT", sceneId)
+ "," + count(connection, "MP_PISPIP_PRODUCTION", sceneId)
+ "," + count(connection, "MP_PISPIP_BOM", sceneId));
}
}
}
private static void bindPispip(PreparedStatement s, String sceneId, int i) throws SQLException {
s.setString(1, id(sceneId, "P", i));
s.setString(2, sceneId);
s.setString(3, "PRODUCT_" + (i % 10_000));
s.setString(4, "P" + (i % 10_000));
s.setString(5, "SP_" + (i % 100));
s.setString(6, "Stocking point " + (i % 100));
s.setInt(7, i % 52);
s.setDate(8, Date.valueOf(LocalDate.of(2026, 1, 1).plusDays(i % 365)));
for (int column = 9; column <= 29; column++) {
s.setDouble(column, (i % 10_000) + column / 100.0);
}
}
private static void bindProduction(PreparedStatement s, String sceneId, int i) throws SQLException {
s.setString(1, id(sceneId, "D", i));
s.setString(2, id(sceneId, "P", i));
s.setString(3, sceneId);
s.setString(4, "OP_" + i);
s.setString(5, "Operation " + i);
s.setString(6, "UNIT_" + (i % 500));
s.setString(7, "Unit " + (i % 500));
s.setDouble(8, (i % 10_000) + 0.5);
s.setInt(9, i % 30);
}
private static void bindBom(PreparedStatement s, String sceneId, int i) throws SQLException {
s.setString(1, id(sceneId, "B", i));
s.setString(2, id(sceneId, "D", i));
s.setString(3, id(sceneId, "P", i));
s.setString(4, sceneId);
s.setString(5, "INPUT_PRODUCT_" + (i % 20_000));
s.setString(6, "SP_" + (i % 100));
s.setString(7, "Stocking point " + (i % 100));
s.setDouble(8, 0.5 + (i % 10));
s.setDouble(9, i % 10_000);
}
private static String id(String sceneId, String type, int index) {
return sceneId.substring("PISPIP_BENCH_".length()) + type + index;
}
private static long elapsedMillis(long startNanos) {
return (System.nanoTime() - startNanos) / 1_000_000L;
}
private static void ensureSchema(Connection connection) throws Exception {
if (tableExists(connection, "MP_PISPIP_RESULT")
&& tableExists(connection, "MP_PISPIP_PRODUCTION")
&& tableExists(connection, "MP_PISPIP_BOM")) {
return;
}
String script = new String(Files.readAllBytes(Paths.get("sql/20260909_mp_pispip_result.sql")),
StandardCharsets.UTF_8);
script = script.replaceAll("(?m)^\\s*--.*$", "");
try (Statement statement = connection.createStatement()) {
for (String part : script.split(";")) {
String sql = part.trim();
if (sql.isEmpty()) continue;
try {
statement.execute(sql);
} catch (SQLException e) {
if (e.getErrorCode() != 955) throw e;
}
}
}
}
private static boolean tableExists(Connection connection, String tableName) throws SQLException {
try (PreparedStatement s = connection.prepareStatement(
"SELECT COUNT(*) FROM USER_TABLES WHERE TABLE_NAME = ?")) {
s.setString(1, tableName);
try (ResultSet r = s.executeQuery()) {
return r.next() && r.getInt(1) > 0;
}
}
}
private static long count(Connection connection, String table, String sceneId) throws SQLException {
try (PreparedStatement s = connection.prepareStatement(
"SELECT COUNT(*) FROM " + table + " WHERE SCENE_ID = ?")) {
s.setString(1, sceneId);
try (ResultSet r = s.executeQuery()) {
r.next();
return r.getLong(1);
}
}
}
private static long productionOrphans(Connection c, String sceneId) throws SQLException {
return scalar(c, "SELECT COUNT(*) FROM MP_PISPIP_PRODUCTION d LEFT JOIN MP_PISPIP_RESULT p "
+ "ON p.PISPIP_ID=d.PISPIP_ID WHERE d.SCENE_ID=? AND p.PISPIP_ID IS NULL", sceneId);
}
private static long bomOrphans(Connection c, String sceneId) throws SQLException {
return scalar(c, "SELECT COUNT(*) FROM MP_PISPIP_BOM b LEFT JOIN MP_PISPIP_PRODUCTION d "
+ "ON d.PRODUCTION_ID=b.PRODUCTION_ID WHERE b.SCENE_ID=? AND d.PRODUCTION_ID IS NULL", sceneId);
}
private static long scalar(Connection c, String sql, String sceneId) throws SQLException {
try (PreparedStatement s = c.prepareStatement(sql)) {
s.setString(1, sceneId);
try (ResultSet r = s.executeQuery()) {
r.next();
return r.getLong(1);
}
}
}
private static void cleanup(Connection c, String sceneId) throws SQLException {
for (String table : new String[]{"MP_PISPIP_BOM", "MP_PISPIP_PRODUCTION", "MP_PISPIP_RESULT"}) {
try (PreparedStatement s = c.prepareStatement("DELETE FROM " + table + " WHERE SCENE_ID = ?")) {
s.setString(1, sceneId);
s.executeUpdate();
}
}
}
}
package com.aps.macroplanner.scene;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class MacroSceneSelectionServiceTest {
@Test
void appendsWithoutLosingOtherUsers() {
assertEquals("111,222,333", MacroSceneSelectionService.updatedUsers("111,222", "333", true));
}
@Test
void repeatedSelectionIsIdempotent() {
assertEquals("111,222", MacroSceneSelectionService.updatedUsers("111,222", "111", true));
}
@Test
void removesOnlyExactUserAndClearsLastUser() {
assertEquals("1111,222", MacroSceneSelectionService.updatedUsers("111,1111,222", "111", false));
assertNull(MacroSceneSelectionService.updatedUsers("111", "111", false));
}
@Test
void handlesOriginalNullAndDuplicateIds() {
assertEquals("111", MacroSceneSelectionService.updatedUsers(null, "111", true));
assertEquals("111,222", MacroSceneSelectionService.updatedUsers("111,111, 222", "111", true));
}
@Test
void rejectsInvalidUsersAndOverflow() {
assertThrows(IllegalArgumentException.class, () -> MacroSceneSelectionService.validateUserId("111,222"));
assertThrows(IllegalArgumentException.class, () -> MacroSceneSelectionService.validateUserId(" "));
String full = String.join("", java.util.Collections.nCopies(2000, "a"));
assertThrows(IllegalArgumentException.class, () -> MacroSceneSelectionService.updatedUsers(full, "111", true));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment