Oracle 8i/9i 時代,Oracle Web Server (OWS) + PL/SQL Cartridge (mod_plsql) 讓資料庫直接輸出 HTML,無需中間層 Java/.NET。核心在於 HTP (Hypertext Procedures)、HTF (Hypertext Functions) 套件,以及 OWA_UTIL、OWA_COOKIE、OWA_SEC 等內建工具包。本文以「學生成績查詢」為例,完整演示從資料庫表到動態網頁的端到端開發流程。
Browser (HTTP Request)
↓
Oracle HTTP Server (Apache + mod_plsql)
↓ 認證/會話管理/連接池
Oracle Database (PL/SQL Procedure)
↓ HTP/HTF 輸出 HTML
Browser (HTTP Response: HTML)
URL 映射規則:https://host:port/pls/DAD/PROCEDURE_NAME?PARAM=VALUE
DAD (Database Access Descriptor):連接池、認證、Session 管理配置單元。PROCEDURE_NAME:PL/SQL 存儲過程/函數入口點。PARAM:自動綁定為過程參數 (名稱大小寫不敏感)。| 套件 | 風格 | 用法 | 適用場景 |
|---|---|---|---|
| HTP | 程序 (Procedure) | HTP.PRINT('<h1>Title</h1>'); 直接寫入 HTTP 回應緩衝區 | 大段 HTML 輸出、邏輯控制流中穿插輸出 |
| HTF | 函數 (Function) | v_html := HTF.ANCHOR('url','Link Text'); 回傳 HTML 字串 | 組裝 HTML 片段、函數式風格、賦值給變量 |
兩者功能對等,HTP.xxx 對應 HTF.xxx,常用:HTMLOPEN/HTMLCLOSE, HEADOPEN/HEADCLOSE, TITLE, BODYOPEN/BODYCLOSE, TABLEOPEN/TABLECLOSE, TABLEROWOPEN/TABLEROWCLOSE, TABLEHEADER, TABLEDATA, FORMOPEN/FORMCLOSE, FORMTEXT, FORMSUBMIT, ANCHOR, IMG, PRINT/PRN (PRN 不換行)。
CREATE TABLE student_scores (
student_id NUMBER PRIMARY KEY,
student_name VARCHAR2(50),
subject VARCHAR2(30),
score NUMBER(5,2),
exam_date DATE
);
-- 測試資料
INSERT INTO student_scores VALUES (1001, '張三', '數學', 92.5, DATE '2024-01-15');
INSERT INTO student_scores VALUES (1001, '張三', '英文', 88.0, DATE '2024-01-15');
INSERT INTO student_scores VALUES (1002, '李四', '數學', 76.0, DATE '2024-01-15');
-- ...
CREATE OR REPLACE PROCEDURE score_query_form IS
BEGIN
HTP.HTMLOPEN;
HTP.HEADOPEN;
HTP.TITLE('學生成績查詢系統');
HTP.HEADCLOSE;
HTP.BODYOPEN(cattributes => 'bgcolor="#F0F8FF"');
HTP.CENTEROPEN;
HTP.HEADER(1, '學生成績查詢');
HTP.CENTERCLOSE;
HTP.FORMOPEN(url => 'score_query_result', method => 'POST');
HTP.TABLEOPEN(cattributes => 'border="1" align="center" cellpadding="5"');
HTP.TABLEROWOPEN;
HTP.TABLEHEADER('輸入學號:');
HTP.TABLEDATA(HTF.FORMTEXT(cname => 'p_student_id', csize => 10, cmaxlength => 10));
HTP.TABLEROWCLOSE;
HTP.TABLEROWOPEN;
HTP.TABLEDATA(HTF.FORMSUBMIT(cvalue => '查詢', cattributes => 'style="font-weight:bold"'), cattributes => 'colspan="2" align="center"');
HTP.TABLEROWCLOSE;
HTP.TABLECLOSE;
HTP.FORMCLOSE;
HTP.BODYCLOSE;
HTP.HTMLCLOSE;
END score_query_form;
/
CREATE OR REPLACE PROCEDURE score_query_result (p_student_id IN NUMBER) IS
CURSOR c_scores IS
SELECT subject, score, exam_date
FROM student_scores
WHERE student_id = p_student_id
ORDER BY exam_date DESC, subject;
v_student_name VARCHAR2(50);
v_row_count NUMBER := 0;
BEGIN
-- 獲取學生姓名 (單獨查詢或 JOIN)
SELECT student_name INTO v_student_name
FROM student_scores
WHERE student_id = p_student_id
AND ROWNUM = 1;
HTP.HTMLOPEN;
HTP.HEADOPEN;
HTP.TITLE(v_student_name || ' 的成績單');
HTP.HEADCLOSE;
HTP.BODYOPEN(cattributes => 'bgcolor="#FFFFFF"');
HTP.CENTEROPEN;
HTP.HEADER(2, v_student_name || ' (學號: ' || p_student_id || ') 的成績查詢結果');
HTP.CENTERCLOSE;
HTP.TABLEOPEN(cattributes => 'border="1" align="center" cellpadding="5" cellspacing="0" width="60%"');
HTP.TABLEROWOPEN;
HTP.TABLEHEADER('科目', cattributes => 'bgcolor="#4472C4" style="color:white"');
HTP.TABLEHEADER('分數', cattributes => 'bgcolor="#4472C4" style="color:white"');
HTP.TABLEHEADER('考試日期', cattributes => 'bgcolor="#4472C4" style="color:white"');
HTP.TABLEROWCLOSE;
FOR r IN c_scores LOOP
v_row_count := v_row_count + 1;
HTP.TABLEROWOPEN(cattributes => CASE MOD(v_row_count,2) WHEN 0 THEN 'bgcolor="#E2EFDA"' ELSE 'bgcolor="#FFFFFF"' END);
HTP.TABLEDATA(r.subject, cattributes => 'align="center"');
HTP.TABLEDATA(r.score, cattributes => 'align="center"');
HTP.TABLEDATA(TO_CHAR(r.exam_date, 'YYYY-MM-DD'), cattributes => 'align="center"');
HTP.TABLEROWCLOSE;
END LOOP;
IF v_row_count = 0 THEN
HTP.TABLEROWOPEN;
HTP.TABLEDATA('查無資料', cattributes => 'colspan="3" align="center" style="color:red"');
HTP.TABLEROWCLOSE;
END IF;
HTP.TABLECLOSE;
HTP.PRINT('
' || HTF.ANCHOR('score_query_form', '返回重新查詢') || ' ');
HTP.BODYCLOSE;
HTP.HTMLCLOSE;
EXCEPTION
WHEN NO_DATA_FOUND THEN
HTP.PRINT('學號 ' || p_student_id || ' 不存在
');
HTP.PRINT('' || HTF.ANCHOR('score_query_form', '返回重新查詢') || ' ');
WHEN OTHERS THEN
HTP.PRINT('系統錯誤: ' || SQLERRM || '
');
-- 記錄錯誤日誌
INSERT INTO error_log (proc_name, error_msg, log_time) VALUES ('SCORE_QUERY_RESULT', SQLERRM, SYSDATE);
END score_query_result;
/
SQL> @score_procs.sql (無錯誤)。$ORACLE_HOME/Apache/modplsql/cfg/dads.conf):
SetHandler pls_handler
Order deny,allow
Allow from all
PlsqlDatabaseUsername scott
PlsqlDatabasePassword tiger
PlsqlDatabaseConnectString orcl:1521:ORCL
PlsqlAuthenticationMode Basic
PlsqlDefaultPage score_query_form
PlsqlDocumentTablename scott.wwv_flow_documents
PlsqlDocumentPath docs
PlsqlDocumentProcedure wwv_flow_doc_manager.process_download
opmnctl restartproc type=ohs。https://host:port/pls/score_dad/score_query_form。| 主題 | 解決方案 | 關鍵套件/技術 |
|---|---|---|
| Session 狀態 | OWA_UTIL.GET_CGI_ENV('HTTP_COOKIE') 解析、OWA_COOKIE.SEND/GET、PL/SQL Package 變量 (Session 級) | OWA_COOKIE, OWA_UTIL, Package State |
| 認證授權 | DAD 配置 PlsqlAuthenticationMode Basic/Custom/Owa、OWA_CUSTOM.AUTHORIZE 函數 | mod_plsql DAD, OWA_CUSTOM |
| 防 XSS/注入 | HTP/HTF 自動 HTML 轉義 (PRINT/PRN)、綁定變量參數化、OWA_UTIL.ESCAPE_URL | HTP/HTF, Bind Variables |
| 效能優化 | mod_plsql 連接池 (PlsqlMaxRequestsPerSession, PlsqlIdleSessionCleanupInterval)、緩存輸出 (OWA_CACHE)、靜態資源剝離至 Web Server | mod_plsql config, OWA_CACHE |
| 檔案上傳/下載 | OWA_UTIL.GET_OWA_SERVICE_PATH、WP_G_DOCLOAD.DOWNLOAD_FILE、Document Table | WP_G_DOCLOAD, Document Table |
遷移路徑:PL/SQL 過程 → REST API (ORDS / APEX REST / 自建 Spring Boot/Node.js) → 前端獨立部署。ORDS (Oracle REST Data Services) 可自動將 PL/SQL 過程發布為 REST 端點,平滑遷移。
HTP/HTF + mod_plsql 是 Oracle 將資料庫變身為應用伺服器的輝嘗試,曾支撐無數 9i/10g 時代的內部系統。理解其「URL→DAD→PL/SQL→HTP→HTML」流程、掌握 HTP.TABLE* 動態表格生成、OWA_* 狀態管理、mod_plsql 連接池調優,對維護存量系統、規劃遷移路徑仍具高價值。新架構請擁抱 資料庫專注資料、中間層專注業務、前端專注體驗 的關注點分離原則。