JDBC ResultSet 綁定底層資料庫連接與網路游標,一旦 Statement 或 Connection 關閉,ResultSet 即失效。因此**絕不能將 ResultSet 傳遞至 JSP 視圖層**。必須在 DAO/Service 層將查詢結果轉換為獨立於連接的數據結構,再傳給 JSP 渲染。本文對比三種主流傳遞模式。
public class EmployeeVO implements Serializable {
private Long empId;
private String empCode;
private String realName;
private String deptName;
private Date hireDate;
// 標準 Getter/Setter、toString、equals/hashCode
}
public List<EmployeeVO> findByOrgId(Long orgId) {
String sql = "SELECT e.emp_id, e.emp_code, e.real_name, d.dept_name, e.hire_date " +
"FROM t_employee e JOIN t_dept d ON e.dept_id = d.dept_id " +
"WHERE e.organ_id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setLong(1, orgId);
try (ResultSet rs = pstmt.executeQuery()) {
List<EmployeeVO> list = new ArrayList<>();
while (rs.next()) {
EmployeeVO vo = new EmployeeVO();
vo.setEmpId(rs.getLong("emp_id"));
vo.setEmpCode(rs.getString("emp_code"));
vo.setRealName(rs.getString("real_name"));
vo.setDeptName(rs.getString("dept_name"));
vo.setHireDate(rs.getDate("hire_date"));
list.add(vo);
}
return list;
}
}
}
<%@ taglib prefix="c" uri="https://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="https://java.sun.com/jsp/jstl/fmt" %>
<table>
<thead><tr><th>工號</th><th>姓名</th><th>部門</th><th>入職日期</th></tr></thead>
<tbody>
<c:forEach items="${employeeList}" var="emp">
<tr>
<td>${emp.empCode}</td>
<td>${emp.realName}</td>
<td>${emp.deptName}</td>
<td><fmt:formatDate value="${emp.hireDate}" pattern="yyyy-MM-dd"/></td>
</tr>
</c:forEach>
</tbody>
</table>
| 優點 | 缺點 |
|---|---|
| 類型安全,IDE 自動提示、重構友好 業務語義清晰,便於驗證、格式化邏輯封裝 適合穩定、長期維護的核心業務實體 |
每個查詢需定義專屬 VO 類,類爆炸 手動映射代碼繁瑣(可用 BeanUtils/MapStruct 自動化) 彈性差:SELECT 列變動需同步修改 VO |
public List<Map<String, Object>> findDynamic(String sql, Object... params) {
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
for (int i = 0; i < params.length; i++) pstmt.setObject(i+1, params[i]);
try (ResultSet rs = pstmt.executeQuery()) {
ResultSetMetaData meta = rs.getMetaData();
int colCount = meta.getColumnCount();
List<Map<String, Object>> list = new ArrayList<>();
while (rs.next()) {
Map<String, Object> row = new LinkedHashMap<>(); // 保持列順序
for (int i = 1; i <= colCount; i++) {
String colName = meta.getColumnLabel(i); // 別名優先
row.put(colName, rs.getObject(i));
}
list.add(row);
}
return list;
}
}
}
<c:forEach items="${rowList}" var="row">
<tr>
<c:forEach items="${row}" var="entry">
<td>${entry.value}</td>
</c:forEach>
</tr>
</c:forEach>
| 優點 | 缺點 |
|---|---|
| 零領域類定義,極致彈性 通用 DAO 方法支援任意 SQL 適合報表、臨時查詢、動態列表 |
無類型安全,鍵名拼寫錯誤運行時才發現 EL 訪問需用 ${row['COL_NAME']},大小寫敏感日期/數字格式化需在 JSP 中處理,邏輯分散 序列化體積大(含鍵名重複) |
// 使用 Jackson / Gson
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule()); // 支援 Java 8 日期
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
String json = mapper.writeValueAsString(rowList); // List
<script>
const data = ${jsonData}; // 直接注入 JS 變量
// Vue/React/原生 JS 渲染
new Vue({
el: '#app',
data: { rows: data },
template: '<table><tr v-for="r in rows"><td>{{r.EMP_CODE}}</td>...</tr></table>'
});
</script>
| 優點 | 缺點 |
|---|---|
| 前後端契約標準化,支援異構客戶端 天然支援巢狀結構、複雜類型 前端渲染分擔伺服器壓力,首屏可直出 SEO |
傳統 JSP 專案引入前端框架增加複雜度 雙模板維護成本高(JSP + JS 模板) 需處理 XSS 風險(JSON 注入 script 標籤) |
public class PageResult<T> {
private List<T> records;
private long total;
private int pageNum, pageSize;
private int totalPages;
// 構造器、Getter、工廠方法 of(list, total, pageNum, pageSize)
}
@Mapper(componentModel = "spring")
public interface EmployeeMapper {
@Mapping(source = "empId", target = "empId")
EmployeeVO toVO(EmployeePO po); // PO 為持久層實體
List<EmployeeVO> toVOList(List<EmployeePO> pos);
}
public abstract class BaseDao {
protected <T> List<T> queryForList(String sql, RowMapper<T> mapper, Object... params) {
// 統一連接、預編譯、參數綁定、結果映射、資源關閉、異常轉換
}
}
@FunctionalInterface
public interface RowMapper<T> {
T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
JSP 頁面查詢顯示的本質是**「如何將資料庫元組安全、高效地轉換為視圖可消費的數據結構」**。
選擇適合團隊技術棧與業務階段的模式,並建立統一規範與基礎設施,才是長久之計。