簡介
本文為 DB2 提供一個 SOAP 驅動程序示例,以便使用 SOAP API 隱式地執行 DB2 存儲過程,而不需要創建任何顯式的映射。我相信下一代數據庫驅動程序會讓數據庫連接拋棄 ODBC/JDBC 等低層 API,轉而使用 SOAP 和 REST 等高層 API,這會使數據庫成為 SOA 環境中的直接參與者。
為了突出這個體系結構中與 XML 數據模型相關的方面,示例程序的流程盡可能保持簡單。GUI 也保持展示功能所需的最基本形式。
我使用 Open Travel Alliance XML 模式創建旅館示例數據和搜索旅館的 SOAP 調用。使用 PayPal API 處理來自應用服務器的信用卡交易。
本文後面提供了源代碼,可以下載並編譯這些源代碼。您需要安裝 DB2 9 並在 Tomcat 類路徑中包含 DB2 JCC 和 XML jar 文件。如果希望測試信用卡交易,就需要安裝 PayPal Java API 並在 Tomcat 類路徑中包含相關的 jar 文件。還必須在 PayPal 沙箱中創建一個帳戶並獲得 API 憑證,詳細說明參見 PayPal Integration Center。然後可以修改 article4.Java 文件的 setupPaypal() 函數中的憑證信息。
場景
在這個場景中,一位客戶要通過 Web 預訂旅館房間。他首先要登錄,獲得他的個人信息。然後,指定一個城市,獲取這個城市中的旅館及其房間價格列表。最後,選擇一家旅館並預訂一個房間。
圖 1. 特性級體系結構
客戶機上的客戶操作導致 Web 浏覽器向應用服務器發出 REST 調用。然後,應用服務器:
使用 JDBC 直接連接內部數據庫,獲取客戶的個人信息。
對另一個數據庫執行 SOAP 調用,這個數據庫在公司防火牆內,但是位於客房預訂部門的內部防火牆後面。
對一個外部信用卡交易服務提供商(比如 PayPal)執行 REST 調用。
圖 2. 設計級體系結構
細節
為了了解在預訂過程的每個步驟幕後發生的情況,我們來看看信息流和相關代碼。
步驟 1
客戶在旅行代理商的 Web 站點上輸入他的姓名並獲得他的個人信息。為了簡單,這個示例不要求輸入密碼,並假設客戶個人信息已經在代理商數據庫中存在。
圖 3. 登錄和獲得個人信息的命令和數據流
客戶個人信息是一個 XML 文檔,存儲在數據庫中的 XML 列中。信用卡信息也是一個 XML 文檔,但是為了安全,它被加密並存儲為二進制格式。
清單 1. 創建 customers 表並插入記錄CREATE TABLE CUSTOMERS (CUSTID CHARACTER (64) NOT NULL,
CC VARCHAR(1024) for bit data not null, INFO XML NOT NULL )
insert into CUSTOMERS values('hardeep',
encrypt('<CC type="visa" expirydate="12/2009" number="4721930402892796" cvv="808">
<name>hardeep singh</name></CC>' , 'passWord'),
'<Customer customerid="hardeep" firstname="hardeep" lastname="singh"/>');
圖 4. 登錄
當單擊 Login 按鈕時,在客戶機中調用 Javascript 函數 getCustomerInfo()。這個函數生成執行應用服務器中的 customerinfo 服務所需的 REST 調用。
清單 2. 用來獲取客戶個人信息的客戶機調用var cid=document.getElementById("userid").value;
var addr=servletpath+"?cmd=customerinfo&msg="+cid;
var xmlhttpObj= new XMLHttpRequest();
XMLhttpObj.open('GET', addr, true);
xmlhttpObj.onreadystatechange = function() { getCustomerInfoCallback(XMLhttpObj); };
XMLhttpObj.send("");
應用服務器對本地數據庫執行一個 SQL 查詢,從 customers 表的 info 列中選擇客戶個人信息。
清單 3. 應用服務器查詢數據庫來獲取客戶個人信息Connection conn= DriverManager.getConnection("jdbc:db2:article4");
Statement stmt = conn.createStatement();
stmt.setMaxRows(1);
ResultSet rs= stmt.executeQuery(
"select info from customers where custid='"+msg+"'");
if(rs.next ()) retValue=rs.getString(1);
stmt.close();
conn.close();
將數據庫查詢所產生的客戶數據以 XML 數據的形式發送回客戶機。
清單 4. 在 HTTP 報頭中返回的數據類型設置為 XML_res.setContentType("text/XML");
_res.setHeader("Cache-Control", "no-cache");
_res.getWriter().write(retValue);
當客戶機從應用服務器接收到客戶數據時,它調用 getCustomerInfoCallback 函數,這個函數使用 XMLParse 包裝器類將客戶 XML 數據解析為 DOM 樹並保存在一個全局變量中。然後改變用戶界面,讓客戶能夠輸入城市的編碼。
清單 5. 解析 XML 數據並改變用戶界面customerinfo= new xmlparse(xmlhttp.responseXML, false);
var hstr='<table cellSpacing="0" width="100%" cellPadding="2" border="0" align="left">';
hstr+='<tr><td align="right">City Code:</td><td><INPUT type="text" id="citycode"
SIZE=15 MAXLENGTH=50 value="msy" tabindex="1">';
hstr+='<td><INPUT type=button value="submit" >';
document.getElementById("canvas").innerHtml=hstr;
步驟 2
客戶現在要搜索旅行目的地城市中的旅館。
圖 5. 列出旅館及其房間價格的命令和數據流
用戶輸入城市編碼並單擊提交按鈕,從而調用客戶機 Javascript 中的 getRates() 函數。
圖 6. 搜索與城市編碼對應的旅館
getRates 函數使用 OTA_HotelAvailRQ XML 模式生成請求旅館信息所需的應用服務器調用。
清單 6. 使用 OTA_HotelAvailRQ 模式創建的搜索旅館消息var citycode=document.getElementById("citycode").value
var req='OTA_HotelSearchRQ XMLns="http://www.opentravel.org/OTA/2003/05"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.opentravel.org/OTA/2003/05 OTA_HotelSearchRQ.xsd"
EchoToken="HL" Target="Production" Version="1.003" PrimaryLangID="EN-US"
ResponseType="PropertyList">'+
'<POS><Source AirlineVendorID="FG" PseudoCityCode="MIA" ISOCountry="US"
ISOCurrency="USD" AgentSine="A4444BM" AgentDutyCode="FR"></Source>'+
'<Source><RequestorID Type="5" ID="12345675" ID_Context="IATA"/></Source></POS>'+
'<Criteria><Criterion><RefPoint></RefPoint><CodeRef LocationCode="23"
CodeContext="OTA-REF code list"/>'+
'<HotelRef HotelCityCode="'+citycode+'"/><Radius Distance="2" DistanceMeasure="MILES"/>'+
'<RoomAmenity RoomAmenity="74"/><RoomAmenity RoomAmenity="123"/></Criterion>
</Criteria></OTA_HotelSearchRQ>';
使用 AJax API 將旅館清單調用以 POST 請求的形式發送到應用服務器。 清單 7. 發送到應用服務器的搜索旅館消息var msg='<request cmd="hotelrates">'+req+'</request>';
var xmlhttpObj= new XMLHttpRequest();
XMLhttpObj.open('POST', servletpath, true);
xmlhttpObj.onreadystatechange = function() { getRatescallback(XMLhttpObj); };
xmlhttpObj.setRequestHeader('content-type', 'text/XML');
XMLhttpObj.send(msg);
應用服務器創建一個 SOAP 調用,調用 article4 數據庫中的 getHotelRates 存儲過程。這個數據庫在部門內部防火牆的後面運行,位置是 http://localhost:8080/article4。從客戶機接收的 OTA 搜索旅館請求(msg)作為參數傳遞給這個存儲過程。
注意:SOAP 消息的 SOAPAction 屬性設置為數據庫名。
清單 8. 應用服務器對 DB2 getHotelRates 存儲過程執行一個 SOAP 調用String body="<db:getHotelRates XMLns:db='http://ibm.com/db2/soap'>"+
"<db:arg>"+msg+"</db:arg></db:getHotelRates>";
return sendURLMessage("http://localhost:8080/article4/db2soapdriver",
body,"http://ibm.com/db2/soap#article4");
盡管有用來創建 SOAP 消息的 API,但是本文只使用基本的 URL 調用代碼,以此說明 SOAP 調用僅僅是一種特殊的 HTTP POST 調用,其消息體符合一個標准化的 XML 模式。
注意:SOAPAction 設置為目標數據庫名。
清單 9. 應用服務器對 DB2 getHotelRates 存儲過程執行一個 SOAP 調用URL u = new URL(url);
URLConnection uc = u.openConnection();
HttpURLConnection connection = (HttpURLConnection) uc;
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("SOAPAction", database_name);
OutputStream out = connection.getOutputStream();
Writer wout = new OutputStreamWriter(out);
wout.write("<?XML version='1.0'?>\r\n");
>
wout.write("XMLns:SOAP-ENV=");
wout.write("'http://schemas.XMLsoap.org/soap/envelope/' ");
wout.write("XMLns:SOAP-ENC=");
wout.write("'http://schemas.XMLsoap.org/soap/encoding/' " );
wout.write("SOAP-ENV:encodingStyle=");
wout.write("'http://schemas.XMLsoap.org/soap/encoding/' ");
wout.write("XMLns:xsi=");
wout.write("'http://www.w3.org/2001/XMLSchema-instance'> ");
wout.write(" <SOAP-ENV:Body>");
wout.write(msg);
wout.write(" </SOAP-ENV:Body>");
wout.write("</SOAP-ENV:Envelope>\r\n");
因為應用服務器僅僅創建 SOAP 包裝器並執行一個 URL 調用,所以如果 AJax 的安全限制允許的話,也可以從客戶機直接執行 SOAP 調用。盡管在 Web 客戶機中使用 SOAP 驅動程序直接調用數據庫是可能的,但是由於安全原因這種方式並不合適,應該改進 SOAP 驅動程序來防止這種做法。
getHotelRates 存儲過程接受一個 XML 參數,其中包含 OTA 請求。XQuery 從輸入的 XML 中提取出 HotelCityCode,並用它搜索和列出包含匹配的 HotelCityCode 屬性的所有旅館。
清單 10. getHotelRates 存儲過程CREATE PROCEDURE getHotelRates( IN request XML )
DYNAMIC RESULT SETS 1
LANGUAGE SQL
BEGIN
DECLARE c_cur CURSOR WITH RETURN FOR
Select XMLQuery('declare namespace ns1 = "http://www.opentravel.org/OTA/2003/05";
$info//ns1:HotelDescriptiveContents' passing info as "info")
from hotel
where XMLexists('declare namespace ns1 = "http://www.opentravel.org/OTA/2003/05";
$info//ns1:HotelDescriptiveContents[@HotelCityCode=$req//ns1:HotelRef/@HotelCityCode]'
passing request as "req", info as "info" );
OPEN c_cur;
END
然後,將從 db2soapdriver 返回給應用服務器的 SOAP 響應發送回客戶機,而不做任何修改。這再次說明,在 XML 模型編程方式中,數據庫成了重要的參與者,而且在許多情況下應用服務器僅僅作為交換信息的中介。
當客戶機從應用服務器接收到響應時,它調用 getRatescallback。使用 DOM 解析器解析返回的 SOAP 響應。DOM 解析器會處理 SOAP 響應中的名稱空間。
因為應用服務器僅僅創建 SOAP 包裝器並執行一個 URL 調用,所以如果 AJax 的安全限制允許的話,也可以從客戶機直接執行 SOAP 調用。盡管在 Web 客戶機中使用 SOAP 驅動程序直接調用數據庫是可能的,但是由於安全原因這種方式並不合適,應該改進 SOAP 驅動程序來防止這種做法。
getHotelRates 存儲過程接受一個 XML 參數,其中包含 OTA 請求。XQuery 從輸入的 XML 中提取出 HotelCityCode,並用它搜索和列出包含匹配的 HotelCityCode 屬性的所有旅館。
清單 10. getHotelRates 存儲過程CREATE PROCEDURE getHotelRates( IN request XML )
DYNAMIC RESULT SETS 1
LANGUAGE SQL
BEGIN
DECLARE c_cur CURSOR WITH RETURN FOR
Select XMLQuery('declare namespace ns1 = "http://www.opentravel.org/OTA/2003/05";
$info//ns1:HotelDescriptiveContents' passing info as "info")
from hotel
where XMLexists('declare namespace ns1 = "http://www.opentravel.org/OTA/2003/05";
$info//ns1:HotelDescriptiveContents[@HotelCityCode=$req//ns1:HotelRef/@HotelCityCode]'
passing request as "req", info as "info" );
OPEN c_cur;
END
然後,將從 db2soapdriver 返回給應用服務器的 SOAP 響應發送回客戶機,而不做任何修改。這再次說明,在 XML 模型編程方式中,數據庫成了重要的參與者,而且在許多情況下應用服務器僅僅作為交換信息的中介。
當客戶機從應用服務器接收到響應時,它調用 getRatescallback。使用 DOM 解析器解析返回的 SOAP 響應。DOM 解析器會處理 SOAP 響應中的名稱空間。 清單 11. 客戶機解析來自應用服務器的 SOAP 響應soapxml= new xmlparse(xmlhttp.responseXML, false);
soapxml.XMLRoot.setProperty("SelectionNamespaces",
"XMLns:xsl='http://www.w3.org/1999/XSL/Transform'
xmlns:SOAP-ENV='http://schemas.XMLsoap.org/soap/envelope/'
XMLns:db='http://ibm.com/db2/soap'");
var hstr='<table cellSpacing="0" width="100%" cellPadding="2" border="1" align="left">';
hstr+="<tr><td>name<td>rate<td>rooms<td>";
將數據庫結果中的每一行提取到另一個 DOM 樹中,然後使用 XPath 提取相關信息。為客戶機創建一個新視圖,顯示返回的所有旅館的列表。注意,XPath 調用中使用了一個名稱空間別名。
清單 12. 客戶機從 SOAP 體中提取數據庫結果集soapXML.find("//SOAP-ENV:Body//db:row",null,true);
for(i=0;soapXML.currentFind.length>i;i++)
{
var result=soapXML.getValue("db:col/text()",i);
rateslist=new XMLparse(result,true);
rateslist.XMLRoot.setProperty("SelectionNamespaces",
"XMLns:xsl='http://www.w3.org/1999/XSL/Transform'
XMLns:x='http://www.opentravel.org/OTA/2003/05' ");
var id=rateslist.getValue("//x:HotelDescriptiveContents/@HotelCode",null);
var name=rateslist.getValue("//x:HotelName/@HotelShortName",null);
var rooms=rateslist.getValue("//x:GuestRoomInfo/@Quantity",null);
var charge=rateslist.getValue("//x:Charge/@Amount",null);
hstr+="<tr><td>"+name+"<td>"+charge+"<td>"+rooms+"<td>
<input type='button' onClick=\"Javascript:bookRoom('"+id+"','"+charge+"');
\" value='select'/>";
}
document.getElementById("canvas").innerHtml=hstr;
步驟 3
在最後一步中,客戶選擇一家旅館並預訂房間。從數據庫中保存的客戶個人信息中獲得信用卡信息。
圖 7. 預訂房間的命令和數據流
客戶現在可以在列表中選擇一家旅館,從而在這家旅館預訂房間。
圖 8. 從旅館列表中選擇旅館
當客戶單擊 Select 時,調用客戶機 Javascript 中的 bookroom 函數。使用 AJax API 將一個 XML 消息(請求預訂房間)以 POST 請求的形式發送到應用服務器。這個請求包含旅館 ID、客戶名、要預訂的房間數量和客戶信用卡需要支付的數額。
清單 13. 客戶機向應用服務器發送 REST 調用來預定房間function bookRoom(hotelid,amount)
{
var cid=document.getElementById("userid").value;
var msg='<request cmd="bookroom"><message><ccinfo units="1" invoice=""
amount="'+amount+'"/><username>'+cid+'</username>
<hotelid>'+hotelid+'</hotelid></message></request>';
var xmlhttpObj= new XMLHttpRequest();
XMLhttpObj.open('POST', servletpath, true);
xmlhttpObj.onreadystatechange = function() { bookRoomcallback(XMLhttpObj); };
xmlhttpObj.setRequestHeader('content-type', 'text/XML');
XMLhttpObj.send(msg);
}
應用服務器解析收到的消息並從其中提取出客戶 ID。然後,調用本地數據庫,從 customers 表中獲取信用卡信息和客戶個人信息。
注意:信用卡信息存儲在 customers 表的一個加密列中。盡管在這個示例中密碼是硬編碼的,但是在真實的場景中密碼可能是客戶用來登錄的密碼。
清單 14. 應用服務器向數據庫查詢加密的信用卡信息XMLParse msgxml=new XMLParse(msg);
String userid=msgXML.getValue("//username/text()");
Connection conn= DriverManager.getConnection("jdbc:db2:article4");
Statement stmt = conn.createStatement();
stmt.setMaxRows(1);
ResultSet rs= stmt.executeQuery("select info,
decrypt_char(CC,'passWord') from customers where custid='"+userid+"'");
if(rs.next ())
{
String custinfo=rs.getString(1);
String CCInfo=rs.getString(2);
接下來,應用服務器創建一個 SOAP 調用,調用在部門內部防火牆後面運行 article4 數據庫中的 bookaroom 存儲過程。旅館 ID 和客戶個人信息作為參數傳遞給這個存儲過程。
清單 15. 應用服務器對 bookaroom 存儲過程執行一個 SOAP 調用String hotelid=msgXML.getValue("//hotelid/text()");
String body="<db:bookaroom XMLns:db='http://ibm.com/db2/soap'>"+
"<db:arg>"+hotelid+"</db:arg>"+
"<db:arg>"+custinfo+"</db:arg>"+
"<db:arg></db:arg>"+
"</db:bookaroom>";
String soapstr=sendURLMessage("http://localhost:8080/article4/db2soapdriver",
body,"http://ibm.com/db2/soap#article4");