存儲過程能將業務邏輯下沉數據庫層,減少網絡往返、利用數據庫優化器、防 SQL 注入。本文給出 **完整可運行示例**:建表、創建帶輸出參數的存儲過程、JSP 調用代碼 (含 PreparedStatement/CallableStatement 兩種方式)、結果處理。
CREATE TABLE BookUser (
UserID INT IDENTITY(1,1) PRIMARY KEY,
UserName VARCHAR(50) NOT NULL,
Title NVARCHAR(50) NOT NULL,
Guid UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
BirthDate DATETIME NOT NULL,
Description NTEXT NOT NULL,
Photo IMAGE NULL,
Other VARCHAR(50) NULL DEFAULT '默認值'
);
CREATE PROCEDURE InsertUser
@UserName VARCHAR(50),
@Title VARCHAR(255),
@Guid UNIQUEIDENTIFIER,
@BirthDate DATETIME,
@Description NTEXT,
@Photo IMAGE,
@Other NVARCHAR(50),
@UserID INT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
-- 業務唯一性檢查
IF EXISTS (SELECT 1 FROM BookUser WHERE UserName = @UserName)
BEGIN
RETURN 0; -- 返回碼 0 表示用戶名已存在
END
INSERT INTO BookUser (UserName, Title, Guid, BirthDate, Description, Photo, Other)
VALUES (@UserName, @Title, @Guid, @BirthDate, @Description, @Photo, @Other);
SET @UserID = SCOPE_IDENTITY(); -- 獲取剛插入的自增 ID
RETURN 1; -- 成功
END;
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="java.sql.*, java.util.*, javax.sql.*, javax.naming.*" %>
<!DOCTYPE html>
<html>
<head>
<title>調用存儲過程插入用戶</title>
</head>
<body>
<h2>註冊新用戶</h2>
<%
// 1. 獲取表單參數
String userName = request.getParameter("userName");
String title = request.getParameter("title");
String birthDateStr = request.getParameter("birthDate"); // 格式 yyyy-MM-dd
String description = request.getParameter("description");
String other = request.getParameter("other");
// 2. 基本校驗
boolean hasError = false;
String errorMsg = "";
if (userName == null || userName.trim().isEmpty()) {
hasError = true; errorMsg = "用戶名不能為空";
}
// 3. 無錯誤時執行存儲過程
Integer newUserId = null;
int returnCode = -1;
if (!hasError) {
Connection conn = null;
CallableStatement cstmt = null;
try {
// --- 方式 A: DataSource + JNDI (生產推薦) ---
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/SqlServerDS");
conn = ds.getConnection();
// --- 方式 B: 直連 (測試用,請替換為您的連接信息) ---
// Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
// conn = DriverManager.getConnection(
// "jdbc:sqlserver://localhost:1433;databaseName=testdb;encrypt=false",
// "sa", "password");
conn.setAutoCommit(false); // 事務控制
// 4. 準備 CallableStatement
String sql = "{call InsertUser(?, ?, ?, ?, ?, ?, ?, ?)}";
cstmt = conn.prepareCall(sql);
// 5. 綁定輸入參數 (按存儲過程定義順序)
cstmt.setString(1, userName);
cstmt.setString(2, title);
cstmt.setString(3, UUID.randomUUID().toString()); // Guid
cstmt.setDate(4, java.sql.Date.valueOf(birthDateStr)); // BirthDate
cstmt.setString(5, description);
cstmt.setNull(6, Types.BLOB); // Photo 暫存 NULL
cstmt.setString(7, other != null ? other : "默認值");
// 6. 註冊輸出參數 (第 8 參數)
cstmt.registerOutParameter(8, Types.INTEGER);
// 7. 執行
cstmt.execute();
// 8. 獲取輸出參數與返回碼
newUserId = cstmt.getInt(8);
returnCode = cstmt.getInt(1); // JDBC 規範:getInt(1) 獲取 RETURN 值 (視驅動而定,建議用 OUTPUT 參數)
conn.commit();
} catch (Exception e) {
hasError = true;
errorMsg = "數據庫錯誤: " + e.getMessage();
if (conn != null) try { conn.rollback(); } catch (SQLException ignored) {}
e.printStackTrace(); // 生產用 Logger
} finally {
// 9. 關閉資源
if (cstmt != null) try { cstmt.close(); } catch (SQLException ignored) {}
if (conn != null) try { conn.close(); } catch (SQLException ignored) {}
}
}
%>
<% if (hasError) { %>
<div style="color:red"><%= errorMsg %></div>
<% } else if (newUserId != null) { %>
<div style="color:green">
註冊成功!新用戶 ID: <%= newUserId %>, 返回碼: <%= returnCode %>
</div>
<% } %>
<hr/>
<form method="post" action="">
用戶名: <input type="text" name="userName" required/><br/>
標題: <input type="text" name="title"/><br/>
出生日期: <input type="date" name="birthDate" required/><br/>
描述: <textarea name="description"></textarea><br/>
其他: <input type="text" name="other"/><br/>
<input type="submit" value="提交調用存儲過程"/>
</form>
</body>
</html>
| 點 | 說明 |
|---|---|
CallableStatement | JDBC 標準調用存儲過程接口,支援輸入/輸出/輸入輸出參數。 |
{call ...} 語法 | JDBC Escape 語法,跨數據庫可移植 (Oracle/MySQL/DB2 同樣適用)。 |
registerOutParameter(index, Type) | **必須**在 execute() 前註冊輸出參數類型,否則取值報錯。 |
SCOPE_IDENTITY() | SQL Server 獲取當前作用域最後自增值,**優於 @@IDENTITY** (不受觸發器影響)。 |
| 返回碼 vs 輸出參數 | RETURN 碼僅 INT,且部分驅動獲取麻煩,**推薦統一用 OUTPUT 參數**傳回業務數據。 |
| 事務控制 | 存儲過程內隱式事務,但 Java 端可顯式 setAutoCommit(false) 組合多操作。 |
| BLOB/IMAGE 參數 | cstmt.setBlob(index, inputStream) 或 setBinaryStream,大對象建議分流存儲。 |
setDate(index, java.sql.Date.valueOf("yyyy-MM-dd"))。@RetCode INT OUTPUT。EXECUTE 權限,Schema 正確 (dbo)。@Procedure / <select statementType="CALLABLE">,避免手寫 JDBC。