Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
H
HYH.APSJ
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
佟礼
HYH.APSJ
Commits
7d6de509
Commit
7d6de509
authored
Sep 10, 2026
by
DESKTOP-VKRD9QF\Administration
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add bulk result persistence and macro scene selection
parent
2c825897
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
568 additions
and
6 deletions
+568
-6
JdbcBatchInsertService.java
...main/java/com/aps/common/util/JdbcBatchInsertService.java
+122
-0
MacroPlannerResultController.java
...java/com/aps/controller/MacroPlannerResultController.java
+9
-1
MacroSceneController.java
src/main/java/com/aps/controller/MacroSceneController.java
+8
-2
MacroSceneConfig.java
src/main/java/com/aps/entity/MacroSceneConfig.java
+2
-0
ResultWriter.java
src/main/java/com/aps/macroplanner/output/ResultWriter.java
+5
-2
MacroSceneSelectionService.java
...om/aps/macroplanner/scene/MacroSceneSelectionService.java
+89
-0
MacroSceneService.java
...in/java/com/aps/macroplanner/scene/MacroSceneService.java
+8
-1
MpPispipResultPersistenceService.java
...ava/com/aps/service/MpPispipResultPersistenceService.java
+53
-0
PispipBatchInsertBenchmark.java
...st/java/com/aps/benchmark/PispipBatchInsertBenchmark.java
+236
-0
MacroSceneSelectionServiceTest.java
...ps/macroplanner/scene/MacroSceneSelectionServiceTest.java
+36
-0
No files found.
src/main/java/com/aps/common/util/JdbcBatchInsertService.java
0 → 100644
View file @
7d6de509
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
;
}
}
src/main/java/com/aps/controller/MacroPlannerResultController.java
View file @
7d6de509
...
@@ -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
);
...
...
src/main/java/com/aps/controller/MacroSceneController.java
View file @
7d6de509
...
@@ -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
);
...
...
src/main/java/com/aps/entity/MacroSceneConfig.java
View file @
7d6de509
...
@@ -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
;
}
}
src/main/java/com/aps/macroplanner/output/ResultWriter.java
View file @
7d6de509
...
@@ -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 ====================
p
rivate
OptimizationResult
buildResult
()
{
p
ublic
OptimizationResult
buildResult
()
{
OptimizationResult
result
=
new
OptimizationResult
();
OptimizationResult
result
=
new
OptimizationResult
();
// 元数据
// 元数据
...
...
src/main/java/com/aps/macroplanner/scene/MacroSceneSelectionService.java
0 → 100644
View file @
7d6de509
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
;
}
}
src/main/java/com/aps/macroplanner/scene/MacroSceneService.java
View file @
7d6de509
...
@@ -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
);
...
...
src/main/java/com/aps/service/MpPispipResultPersistenceService.java
0 → 100644
View file @
7d6de509
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
));}
}
src/test/java/com/aps/benchmark/PispipBatchInsertBenchmark.java
0 → 100644
View file @
7d6de509
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
();
}
}
}
}
src/test/java/com/aps/macroplanner/scene/MacroSceneSelectionServiceTest.java
0 → 100644
View file @
7d6de509
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
));
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment