如何从原始付款记录获取最新 payment status
结论
程序卡住,是因为循环查询得到的是 rs2,读取数据时用的却还是 rs。结果就是 isales_id 和 ref_isales_id 一直没有更新,while 条件始终为真,最终形成死循环。
循环也不该用当前记录的 ref_isales_id != 0 判断是否继续。沿付款链向后查询时,应该在找不到 ref_isales_id = 当前 isales_id 的下一条记录时结束。
如果使用 MySQL 8.0 及以上版本,可以用递归 CTE 一次查出整条付款链。旧版本可以继续使用 Java 循环,但要确保读取 rs2,在没有下一条记录时退出,并处理可能出现的循环引用。
为什么现有循环不会结束
问题出在这里:
if (rs2.next()) {
isales_id = rs.getString("isales_id");
// 其余字段也全部从 rs 读取
ref_isales_id = rs.getString("ref_isales_id");
}
rs2.next() 移动的是第二个结果集,后续读取的却是第一个结果集 rs。第一次查询完成后,rs 中的数据已经固定,循环变量自然不会更新。
即使把这里改成 rs2,原来的循环条件仍然不对:
while (!"0".equals(ref_isales_id))
ref_isales_id 表示当前记录指向的上一条记录,并不代表下一条记录。只要当前记录不是原始记录,这个值通常就不是 "0"。要判断是否还有下一条记录,只能查询:
WHERE ref_isales_id = 当前记录的 isales_id
方案一:使用 MySQL 8.0+ 递归 CTE
假设付款记录是一条单链,也就是每条付款记录最多只有一条后续记录,可以通过一次查询取得付款链中最后一条非全额付款记录:
WITH RECURSIVE payment_chain AS (
SELECT
isales_id,
rce,
total_payment,
pay_status,
pay_amount,
remaining_bal,
it_id,
client_id,
ic_id,
ref_isales_id,
0 AS depth
FROM installation_sales
WHERE isales_id = ?
UNION ALL
SELECT
child.isales_id,
child.rce,
child.total_payment,
child.pay_status,
child.pay_amount,
child.remaining_bal,
child.it_id,
child.client_id,
child.ic_id,
child.ref_isales_id,
parent.depth + 1
FROM installation_sales AS child
JOIN payment_chain AS parent
ON child.ref_isales_id = parent.isales_id
)
SELECT
isales_id,
rce,
total_payment,
pay_status,
pay_amount,
remaining_bal,
it_id,
client_id,
ic_id,
ref_isales_id
FROM payment_chain
WHERE pay_status <> 'full payment'
ORDER BY depth DESC
LIMIT 1;
对应的 Java 代码如下:
String sql =
"WITH RECURSIVE payment_chain AS (" +
" SELECT isales_id, rce, total_payment, pay_status, pay_amount, " +
" remaining_bal, it_id, client_id, ic_id, ref_isales_id, " +
" 0 AS depth " +
" FROM installation_sales " +
" WHERE isales_id = ? " +
" UNION ALL " +
" SELECT child.isales_id, child.rce, child.total_payment, " +
" child.pay_status, child.pay_amount, child.remaining_bal, " +
" child.it_id, child.client_id, child.ic_id, " +
" child.ref_isales_id, parent.depth + 1 " +
" FROM installation_sales child " +
" JOIN payment_chain parent " +
" ON child.ref_isales_id = parent.isales_id " +
") " +
"SELECT isales_id, rce, total_payment, pay_status, pay_amount, " +
" remaining_bal, it_id, client_id, ic_id, ref_isales_id " +
"FROM payment_chain " +
"WHERE pay_status <> 'full payment' " +
"ORDER BY depth DESC " +
"LIMIT 1";
try (Connection conn = MySQLConnect.ConnectDB();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, clickedTable);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
isales_id = rs.getString("isales_id");
rce = rs.getString("rce");
total_payment = rs.getDouble("total_payment");
pay_status = rs.getString("pay_status");
pay_amount1 = rs.getDouble("pay_amount");
remaining_bal = rs.getDouble("remaining_bal");
it_id = rs.getInt("it_id");
client_id = rs.getString("client_id");
ic_id = rs.getString("ic_id");
ref_isales_id = rs.getString("ref_isales_id");
}
}
} catch (SQLException e) {
e.printStackTrace();
}
这里把 pay_status <> 'full payment' 放在递归结束后的最终查询中,是为了先遍历完整的付款链,再选择最后一条符合条件的记录。如果在递归部分提前排除某种状态,该记录之后的节点也可能无法访问。
方案二:修正 Java 循环
如果数据库不支持递归 CTE,可以逐条查询后续记录。循环应在没有查到下一条记录时退出:
String rootSql =
"SELECT isales_id, rce, total_payment, pay_status, pay_amount, " +
" remaining_bal, it_id, client_id, ic_id, ref_isales_id " +
"FROM installation_sales " +
"WHERE isales_id = ?";
String nextSql =
"SELECT isales_id, rce, total_payment, pay_status, pay_amount, " +
" remaining_bal, it_id, client_id, ic_id, ref_isales_id " +
"FROM installation_sales " +
"WHERE ref_isales_id = ? " +
"ORDER BY isales_id DESC " +
"LIMIT 1";
try (Connection conn = MySQLConnect.ConnectDB();
PreparedStatement rootStatement = conn.prepareStatement(rootSql);
PreparedStatement nextStatement = conn.prepareStatement(nextSql)) {
rootStatement.setString(1, clickedTable);
try (ResultSet rootResult = rootStatement.executeQuery()) {
if (!rootResult.next()) {
return;
}
isales_id = rootResult.getString("isales_id");
if (!"full payment".equals(rootResult.getString("pay_status"))) {
copyPaymentData(rootResult);
}
}
Set<String> visitedIds = new HashSet<>();
visitedIds.add(isales_id);
while (true) {
nextStatement.setString(1, isales_id);
try (ResultSet nextResult = nextStatement.executeQuery()) {
if (!nextResult.next()) {
break;
}
String nextId = nextResult.getString("isales_id");
// 防止错误数据形成 A -> B -> A 一类循环引用
if (!visitedIds.add(nextId)) {
throw new SQLException(
"检测到付款记录循环引用,isales_id=" + nextId
);
}
isales_id = nextId;
if (!"full payment".equals(nextResult.getString("pay_status"))) {
copyPaymentData(nextResult);
}
}
}
} catch (SQLException e) {
e.printStackTrace();
}
公共字段的读取逻辑可以抽取成一个方法:
private void copyPaymentData(ResultSet rs) throws SQLException {
isales_id = rs.getString("isales_id");
rce = rs.getString("rce");
total_payment = rs.getDouble("total_payment");
pay_status = rs.getString("pay_status");
pay_amount1 = rs.getDouble("pay_amount");
remaining_bal = rs.getDouble("remaining_bal");
it_id = rs.getInt("it_id");
client_id = rs.getString("client_id");
ic_id = rs.getString("ic_id");
ref_isales_id = rs.getString("ref_isales_id");
}
下面这段代码负责在没有后续付款记录时结束循环:
if (!nextResult.next()) {
break;
}
查到记录后,数据必须从本次查询得到的 nextResult 中读取,不能继续使用第一次查询的 ResultSet。
还需要注意的地方
不要吞掉异常
原代码中的空 catch 会把 SQL 错误和循环读取错误全部隐藏:
catch (Exception e) {
}
至少要记录异常:
catch (SQLException e) {
e.printStackTrace();
}
正式项目可以改用统一的日志记录,并在界面中显示用户能够理解的错误信息。
使用 PreparedStatement
不要直接拼接 clickedTable:
"... WHERE isales_id='" + clickedTable + "'"
应使用参数占位符 ?。这既能避免 SQL 注入,也能减少字符串类型、引号和特殊字符引发的错误。如果 isales_id 是整数,应使用:
ps.setInt(1, Integer.parseInt(clickedTable));
ORDER BY isales_id DESC 不一定代表“最新”
只有当 isales_id 单调递增,而且数值大小确实对应付款顺序时,这种排序才可靠。更稳妥的方式是保存明确的时间列,例如 created_at 或 payment_at:
ORDER BY payment_at DESC, isales_id DESC
LIMIT 1
明确是否允许多个后续记录
如果一个 isales_id 可以被多条记录通过 ref_isales_id 引用,数据结构就是一棵树,而不是单链。此时,LIMIT 1 只会选择排序最靠前的一条分支。业务规则需要明确:究竟是选择付款时间最新的一条,还是遍历所有分支。
如果业务规定每条记录只能有一个直接后继,可以考虑给 ref_isales_id 添加唯一约束。否则,应使用递归 CTE 遍历所有分支,再根据明确的时间字段选择最新记录。
不要在 Swing 事件线程中执行长查询
manageBalancesTableMouseClicked 通常运行在 Swing Event Dispatch Thread 上。数据库查询和多次循环查询会阻塞界面。即使代码没有死循环,用户也会觉得程序“卡住”。可以把查询放进 SwingWorker 等后台任务,执行完成后再更新控件。
数据库还应为关联列建立索引:
CREATE INDEX idx_installation_sales_ref
ON installation_sales (ref_isales_id);
如果经常同时按引用和付款状态查询,也可以结合实际执行计划考虑联合索引。具体采用哪种索引,应根据表的数据量和 EXPLAIN 结果决定。
备注:内容仅供参考。
