http://www.cnblogs.com/Mazifu/archive/2005/07/02/185122.html
Rick Strahl写的关于使用VFP构建基于Web的DataService,很好的为我们阐述了整个DataService的数据流程,不管你是使用何种方法构建Webservice的,这篇文章在架构上都有指导意义.花了整整两天翻译,希望与大家共享,水平有限,不当之处请大家指出.
使用 Visual FoxPro提供一个基于互联网的数据服务
Source Code:
http://www.west-wind.com/presentations/foxwebDataService/FoxWebDataService.zip
你有没有想过在你的程序中建立这样一种远程数据访问机制:你将不仅可以从本地网络中访问数据,而且可以从Web上轻易的访问数据。你仅仅需要做的是指向一个URL,并且执行一些Sql语句,那么客户端中就可以得到你需要的数据了,这是不是很Cool呢?在以下这篇文章中,Rick将会为我们展示怎样利用Visual FoxPro做到这一切的。
如果你正在使用任何一种类库,你也许知道其中关于数据访问的那些类。他们都不约而同的都提供了访问不同数据源的方法,一个标准的数据访问层会提供针对某一类型的数据源执行各种SQL语句的功能。数据访问层总是使用不同的数据连接器(data connector)来访问数据源,有时使用VFP的SQL命令或任何DML类型的SQL命令,有时使用SQLPassthrough或OleDb或CursorAdapter等较低级别的数据连接器。
在这篇文章中,我编写了一些有关数据访问的类,以便提供一个可以和Web XML Service进行通信的接口。这个项目分为两部分:1.一个类似代理的客户端,他负责通讯中请求(Request)的编码与响应(Response)的解码。2.一个服务器端,他负责通讯中请求的解码,逻辑的执行与响应的编码。所有的消息(Message)我们都使用XML格式。这个项目需要你的Web服务器可以执行Visual FoxPro代码,类似于Asp+COM,Web connection,Active FoxPro Page,FoxISAPI。我将在一开始使用Web Connection(因为他比较简单易用),最后我将使用ASP+COM的方式来实现。
Web based SQL access
第一步我们需要建立Sql 代理与服务器端。我们这样做的目的是让Sql代理可以以XML的方式传递Sql命令和一些指令给服务器端上的程序进行处理,注意,Sql代理发送的请求(request)与服务器端的响应(Response)都是以XML形式传递的。
我现在要建立一个简单的类,其中只有一个方法(Method)—Execute()用来传送Sql命令。这个方法与VFP中的SQL Passthrough的SQLExecute十分类似,并且他们都返回同一类型的值。
如果你考虑使用VFP来实现这个类将会简单的多,因为VFP提供了许多工具可以帮助我们。在VFP中我们有一个动态执行引擎(Macros和Eval)或SQL Passthrough来执行我们的命令 ,我们还有CUSORTOXML和XMLTOCURSOR等XML转换工具。我们唯一需要做的就是规定一种XML消息格式, 用以在Client与Server之间传递。
在Client端只有一个用来处理XML消息的代理类,他将你的Sql命令转换为XML,并以请求的形式发给Server进行处理。Server处理你发来的SQL命令,并将结果以XML的形式发回给Client。Client将发回的XML解码为游标(cursor),错误信息或返回值。图一为我们解释了这个过程:
图一:wwwHTTPSql类将Sql命令传递给Web服务器,由Web 服务器上的wwwHTTPSqlServer进行处理并将结果返回给Client。
Client与Server之间的通信是通过wwwHTTPSql类与wwwHTTPSqlServer类来实现的。wwwHttpSqlServer类可以集成在任何支持VFP的Web程序中,比如Web Connection ,ASP,FoxISAPI,Active FoxPro Page,ActiveVFP等.但你必须保证wwwHttpSqlServer在Web 服务器上运行,以便Client进行连接。
XML通过字符或DOM节点的形式传递,所以他十分适合在不同的环境中工作。图一向我们展示了wwwHTTPSql与wwwHTTPSqlServer类之间的关系。
在我深入讲解之前,让我们快速浏览一下Client上的代码,Client使用wwwHttpSql类来和特定Url上的Web Server进行连接并获取数据的。
Listing 1: Running a remote SQL Query with wwHTTPSql
DO wwHTTPSQL && Load Libs
oHSQL = CREATEOBJECT("wwHTTPSQL")
oHSQL.cServerUrl = "http://www.west-wind.com/wconnect/wwHttpSql.http"
oHSQL.nConnectTimeout = 10
*** Specify the result cursor name
oHSQL.cSQLCursor = "TDevelopers"
*** Plain SQL statements
lnCount = oHSQL.Execute("select * from wwDevRegistry")
IF oHSQL.lError
? oHSQL.cErrorMsg
ELSE
BROWSE
ENDIF
这段程序中你需要注意的是:1.你需要访问的Web Server(在这里我们使用Web Connection Server)的Url的设定 2:你传递的Sql命令。这些都是最基本的设定,因为wwwHttpSql继承自wwwHttp类,所以我们还可以设定一些诸如验证,连接等功能。
Client可以通过nResultModel属性设置返回数据的方式,默认(nResultModel=0)是返回VFP游标,nResultModel=2表示以XML的形式返回,此时cResponserXML的值为XML。nTransportMode属性让你选择数据传送的方式, nTransportMode=1表示使用VFP的CURSORTOXML命令,nTransportMode=0表示使用XML格式,nTransportMode=2表示使用二进制格式(Binary)(与VFP Cursor比较起来,如果数据量较大的话,使用二进制格式会更有效率)。简而言之,你可以适当的配置这些属性,使你在任何情况下更有效的处理数据。
Execute方法用以执行Sql命令,wwwHttpSql类将会知道返回的是游标或XML消息或任何其他值,他还会捕捉错误与处理返回的XML响应。Client 与Server端其实只是处理XML消息,所以我们只需要任何方法拼凑出合适的经过验证的XML消息发给已知URL的Web Server,而没必要一定使用VFP,比如我们可以在.Net中将将Dataset解析为XML。
Client端生成如下的XML消息发送给Server。
<wwhttpsql>
<sql>select * from wwDevRegistry</sql>
<sqlcursor>TDevelopers</sqlcursor>
<transportmode>1</transportmode>
</wwhttpsql>
当client上执行Execute()命令时,会执行以下步骤:
- 根据你在程序中设置的属性,生成如上的XML。
- XML被发送到指定的Url。
- Server端处理请求并返回XML形式的结果。一般来说,结果都是以XML形式返回的,除非发生了硬件错误,而软件错误也会以XML形式返回。
- Client收到响应信息并进行解析。
- 首先会验证响应信息是否为XML形式的,如果不是,则产生一个错误(Error),请求失败。
- 如果响应信息中包含错误的XML节,则请求失败,将错误信息赋值给IError和CErrorMSg。
- 如果响应的XML信息中包含一个返回值,那我们将他赋值给VReturnValue。
- 如果在响应的XML信息中包含一个游标并且nResultMode=0,我们将游标赋值给cSQLCursor属性。
如果你回想一下这个过程,你就会发现,在VFP和wwXML class类的帮助下,我们只需要很少的代码就可以完成上述功能,下面就是wwwHttpSql的核心代码,我们来看一下:
Listing 2: The core code of the wwHTTPSql client class
***********************************************************
* wwHTTPSQL :: CreateRequestXML
****************************************
FUNCTION CreateRequestXML()
LOCAL lcXML
loXML = THIS.oXML
lcXML = ;
"<wwhttpsql>" + CRLF + ;
loXML.AddElement("sql",THIS.cSQL,1) + ;
loXML.AddElement("sqlcursor",THIS.cSQLCursor,1) + ;
IIF(!EMPTY(THIS.cSQLConnectString),;
loXML.AddElement("connectstring",THIS.cSQLConnectString,1),[]) +;
IIF(!EMPTY(THIS.cSkipFieldsForUpdates),loXML.AddElement("skipfieldsforupdates",;
THIS.cSkipFieldsForUpdates,1) +CRLF,[]) + ;
IIF(THIS.nTransportMode # 0,;
loXML.AddElement("transportmode",THIS.nTransportMode,1),[]) +;
IIF(THIS.nSchema = 0,loXML.AddElement("noschema",1),[]) +;
IIF(!EMPTY(THIS.cSQLParameters),CHR(9) + "<sqlparameters>" + CRLF + ;
THIS.cSQLParameters + ;
CHR(9) + "</sqlparameters>" + CRLF,"")
IF THIS.lUTF8
lcXML = lcXML + loXML.AddElement("utf8","1",1)
ENDIF
lcXML = lcXML + "</wwhttpsql>"
THIS.cRequestXML = lcXML
RETURN lcXML
**********************************************************************
* wwHTTPSQL :: Execute
****************************************
FUNCTION Execute(lcSQL)
LOCAL lnSize, lnBuffer, lnResult, llNoResultSet, lcXML
lcSQL=IIF(VARTYPE(lcSQL)="C",lcSQL,THIS.cSQL)
THIS.cSQL = lcSQL
THIS.lError = .F.
THIS.cErrorMsg = ""
IF !INLIST(LOWER(lcSQL),"select","create","execute")
llNoResultSet = .T.
ELSE
llNoResultSet = .F.
ENDIF
*** Create the XML to send to the server
lcXML = THIS.CreateRequestXML()
THIS.nHTTPPostMode = 4 && Raw XML
THIS.AddPostKey("",lcXML)
THIS.cResponseXML = THIS.HTTPGet(THIS.cServerUrl,;
THIS.cUserName,THIS.cPassword)
*** Clear the entire buffer
THIS.AddPostKey("RESET")
THIS.AddSqlParameter()
IF THIS.nError # 0
THIS.lError = .T.
RETURN -1
ENDIF
THIS.nResultSize = LEN(THIS.cResponseXML)
IF EMPTY(THIS.cResponseXML)
THIS.cErrorMsg = "No data was returned from this request."
THIS.nError = -1
THIS.lError = .T.
RETURN -1
ENDIF
RETURN this.ParseResponseXml()
************************************************************************
* wwHttpSql :: ParseResponseXml
****************************************
FUNCTION ParseResponseXml()
LOCAL lcFileName, loDOM, loRetVal, cResult, ;
loError, loSchema, loXML
loXML = this.oXml
loDOM = loXML.LoadXML(THIS.cResponseXML)
THIS.oDOM = loDOM
*** Check for valid XML
IF ISNULL(loDom)
THIS.cErrorMsg = "Invalid XML returned from server" +;
loXML.cErrorMsg
THIS.nError = -1
THIS.lError = .T.
RETURN -1
ENDIF
*** Check for return value
loRetVal = loDom.documentElement.selectSingleNode("returnvalue")
IF !ISNULL(loRetval)
THIS.vReturnValue = loRetVal.childnodes(0).Text
ENDIF
*** Check for results that don't return a cursor
lcResult = Extract(THIS.cResponseXML,"<result>","</result>")
IF lcResult = "OK"
RETURN 0
ENDIF
*** Check for server errors returned to the client
loError = loDom.documentElement.selectSingleNode("error")
IF !ISNULL(loError)
THIS.cErrorMsg = loError.selectSingleNode("errormessage").text
THIS.nError = -1
THIS.lError = .T.
RETURN -1
ENDIF
*** OK we have an embedded cursor
*** Force new table instead of appending
IF USED(THIS.cSQLCursor)
SELE (THIS.cSQLCursor)
USE
ENDIF
IF "<VFPData>" $ LEFT(THIS.cResponseXML,100)
*** Use VFP 7's XMLTOCURSOR natively (faster)
XMLTOCURSOR(THIS.cResponseXML,THIS.cSQLCursor)
ELSE
*** Otherwise use wwXML
loSchema = loDom.documentElement.selectSingleNode("Schema")
IF !ISNULL(loSchema)
IF THIS.nResultMode=0
loXML.XMLToCursor(loDOM,THIS.cSQLCursor)
IF loXML.lError
THIS.cErrorMsg = "XML conversion failed: " +loXML.cErrorMsg
RETURN -1
ENDIF
ENDIF
ELSE
*** No cursor to return
RETURN 0
ENDIF
ENDIF
RETURN RECCOUNT()
‘整个类还包括一些其他的方法,但核心部分就是上述的,可以看出他们十分简单,并且利用了MSXML解析器快速的查看返回的响应信息,并使用XMLTOCURSOR()处理XML信息。
On to the server side
如果你了解Client端的代码,你也许已经猜到Server上的程序是如何运做的了吧。逻辑上相似,功能上相反。就象我上面提到的一样,Server端的组件无须和某种Web开发平台绑定,只要平台能够支持VFP的执行就可以了。在Listing3中我们使用Web Connection,我将在文章的结束部分描述如何使Server 端组件在ASP/COM上运行。
Listing 3: Setting up the wwHTTPSqlServer server component w/ Web Connection
FUNCTION wwHTTPSQLData()
*** Create Data Object and call Server Side Execute method
SET PROCEDURE TO wwHTTPSQLServer ADDITIVE
loData = CREATE("wwHTTPSQLServer")
loData.cAllowedCommands = "select,execute,insert,method,"
loData.cConnectString = "" && Read data from SQL
*** Pass the XML and execute the command
loData.S_Execute(Request.FormXML())
*** Create the output
loHeader = CREATEOBJECT("wwHTTPHeader")
loHeader.SetProtocol()
loHeader.SetContentType("text/xml")
loHeader.AddForceReload()
loHeader.AddHeader("Content-length",TRANSFORM(LEN(loData.cResponseXML)))
Response.Write( loHeader.GetOutput() )
Response.Write( loData.cResponseXML )
ENDFUNC
* wcDemoProcess :: wwHTTPSQLData
可以看出,Server端上的执行代码十分的简单—这里只是简单的调用S_Execute()方法来处理接受到的XML字符串或DOM节点。S_Execute()是一个比较上层的方法,如果你想更多的控制程序的执行,你可以使用一些较底层的方法。比如,以下的代码会对Sql命令中的”wws_”字符校验,确保带有”wws_”字符的sql命令无法访问West Wind数据库。用以下代码替换”loData.S_Execute(Request.FormXML())”:
IF loData.ParseXML()
*** Custom Check - disallow access to Web Store Files
IF ATC("WWS_", loData.cFullSQL) > 0
loData.S_ReturnError("Access to table denied")
ELSE
IF loData.ExecuteSQL()
loData.CreateXML()
ENDIF
ENDIF
ENDIF
注意,以上两种方法无论执行成功与否,都将返回XML格式的响应。即使产生了一个错误,结果还是XML。在下一个例子中,我将故意触发一个外部错误,并且使用S_ReturnError方法返回XML格式的错误信息,S_ReturnError将保证返回的错误的格式是一致的。
正如你所看到的,Server按以下步骤处理请求:
- 验证传来的XML请求。如果成功,将XML的内容存储在属性中。
- 执行请求中的Sql命令。
- 将结果编码为XML。对结果进行编码的方式由nTransportMode决定,或者如果出现了错误,则以XML格式返回错误。
以上三个步骤在Listing4中体现:
Listing 4: The core methods of the wwHttpSqlServer object
***********************************************************
* wwHTTPSQLServer :: ParseXML
****************************************
FUNCTION ParseXML(lcXML)
local loXML, lcFullSQL, lcSQL, ;
lcCursorName, lnAt, lcCommand
THIS.lError = .F.
THIS.cErrorMsg = ""
loXML = THIS.oXML
IF VARTYPE(lcXML) = "O"
THIS.oDOM = lcXML
THIS.oDOM.Async = .F.
this.cRequestXml = this.oDom.Xml
ELSE
IF EMPTY(lcXML)
lcXML = REQUEST.FormXML()
ENDIF
THIS.cRequestXML = lcXML
THIS.ODOM = loXML.LoadXML(lcXML)
IF ISNULL(THIS.oDom)
THIS.S_ReturnError("Invalid XML input provided.")
RETURN .F.
enDIF
ENDIF
lcFullSQL = THIS.GetXMLValue("sql")
lcFullSQL = STRTRAN(lcFullSQL,CHR(13)," ")
lcFullSQL = STRTRAN(lcFullSQL,CHR(10),"")
lcSQL = LOWER(LEFT(lcFullSQL,10))
lcCursorName = THIS.GetXMLValue("sqlcursor")
IF EMPTY(lcCursorName)
lcCursorName = "THTTPSQL"
ENDIF
THIS.nTransportmode = VAL(THIS.GetXMLValue("transportmode"))
IF THIS.GetXMLValue("noschema") = "1"
THIS.nSchema = 0
ENDIF
IF THIS.GetXMLValue("utf8") = "1"
THIS.lUtf8 = .T.
ENDIF
IF EMPTY(lcSQL)
THIS.S_ReturnError("No SQL statement to process.")
RETURN .F.
ENDIF
*** Check for illegal commands
lnAt = AT(" ",lcSQL)
lcCommand = LOWER(LEFT(lcSQL,lnAt - 1))
IF ATC(","+lcCommand+",","," + THIS.cAllowedCommands+",") = 0
THIS.S_ReturnError(lcCommand + " is not allowed or invalid.")
RETURN .F.
ENDIF
IF lcSQL # "select" AND lcSQL # "insert" AND lcSQL # "update" AND ;
lcSQL # "delete" AND lcSQL # "create" AND
lcSQL # "execute" AND lcSQL # "method"
THIS.S_ReturnError("Only SQL commands are allowed.")
RETURN .F.
ENDIF
THIS.cCommand = lcCommand
THIS.cCursorName = lcCursorName
THIS.cFullSQL = lcFullSQL
IF THIS.cConnectString # "NOACCESS"
*** Only allow access if the connection string is not set in
*** the server code already!
IF EMPTY(THIS.cConnectString)
THIS.cConnectString = THIS.GetXMLValue("connectstring")
ENDIF
ENDIF
RETURN .T.
ENDFUNC
************************************************************************
* wwHTTPSQLServer :: ExecuteSQL
****************************************
FUNCTION ExecuteSQL()
LOCAL llError, lcReturnVar, loSqlParameters, ;
loType, lcType, lvValue, lcMacro,
lcCursorName, lcFullSQL, lcMethodCall, loEval, ;
lcError, lnResultCursors, loSQL, lcCommand
lcReturnVar = ""
loSQLParameters = THIS.GetXMLValue("sqlparameters",2)
*** Check for named parameters
IF !ISNULL(loSQLParameters)
*** Create the variables and assign the value to it
FOR EACH oParm IN loSQLParameters.ChildNodes
loType = oParm.Attributes.GetNamedItem("type")
IF !ISNULL(loType)
lcType = loType.Text
ELSE
lcType = "C"
ENDIF
loReturn =oParm.Attributes.GetNamedItem("return")
IF !ISNULL(loReturn)
lcReturnVar = oParm.NodeName
ENDIF
DO CASE
CASE lcType = "C"
lvValue = oParm.text &&REPLACE VALUE WITH oParm.TEXT
CASE lcType = "N"
lvValue = VAL(oParm.Text)
CASE lcType = "D"
lvValue = CTOD(oParm.Text)
CASE lcType = "T"
lvValue = CTOT(oParm.Text)
CASE lcType = "L"
lvValue = INLIST(LOWER(oParm.Text),"1","true","on")
ENDCASE
lcMacro = oParm.NodeName + "= lvValue"
&lcMacro && Create the variable as a PRIVATE
ENDFOR
*** Once created they can be used as named parameter via ODBC ?Parm
*** or as plain variables in straight Fox Queries
ENDIF
lcCommand = THIS.cCommand
lcCursorName = THIS.cCursorName
lcFullSQL = THIS.cFullSql
SYS(2335,0) && Disallow any UI access in COM
DO CASE
*** Access ODBC connection
CASE !ISNULL(THIS.oSQL) OR (THIS.cConnectString # "NOACCESS" AND ;
!EMPTY(THIS.cConnectString) )
*** If we don't have a connection object
*** we have to create and tear down one
IF ISNULL(THIS.oSQL)
loSQL = CREATE("wwSQL")
loSQL.cSQLCursor = THIS.cCursorName
IF !loSQL.CONNECT(THIS.cConnectString)
THIS.S_ReturnError(loSQL.cErrorMsg)
SYS(2335,1) && Disallow any UI access in COM
RETURN .F.
ENDIF
ELSE
*** Otherwise use passed in connection
*** which can be reused
loSQL = THIS.oSQL
loSQL.cSQLCursor = lcCursorName
ENDIF
loSQL.cSkipFieldsForUpdates = THIS.cSkipFieldsForUpdates
THIS.nResultCursors = loSQL.Execute(lcFullSQL)
loSQL.cSkipFieldsForUpdates = ""
IF loSQL.lError
THIS.S_ReturnError(loSQL.cErrorMsg)
SYS(2335,1) && Disallow any UI access in COM
RETURN .F.
ENDIF
OTHERWISE && Fox Data
IF lcCommand = "select"
lcFullSQL = lcFullSQL + " INTO CURSOR " + lcCursorName + " NOFILTER"
ENDIF
*** Try to map stored procedures to Fox methods of this
*** class with the same name
IF lcCommand = "execute"
poTHIS = THIS
lcFullSQL = "poTHIS." + ParseSQLSPToFoxFunction(lcFullSQL)
endif
THIS.nResultCursors = 1
llError = .f.
TRY
&lcFullSql
CATCH
llError = .t.
ENDTRY
IF llError
THIS.S_ReturnError("SQL statement caused an error." + CHR(13) + lcFullSQL)
SYS(2335,1)
RETURN .F.
ENDIF
ENDCASE
SYS(2335,1)
*** Add the return value if used
IF !EMPTY(lcReturnVar)
THIS.cReturnValueXML = "<returnvalue>" + CRLF + ;
THIS.oXML.AddElement(lcReturnVar,&lcReturnVar,1) +;
"</returnvalue>" +CRLF
ENDIF
RETURN .T.
***********************************************************
* wwHTTPSQLServer :: CreateXML
****************************************
FUNCTION CreateXML()
LOCAL lcFileText, lcFileName, loHTTP, lcDBF
IF !INLIST(THIS.cCommand,"select","create",;
"execute","method")
*** If no cursor nothing needs to be returned
THIS.S_ReturnOK()
RETURN .t.
ENDIF
lcFileText = ""
IF USED(THIS.cCursorName)
*** Now create the cursor etc.
SELECT(THIS.cCursorName)
LogString(this.cCursorName + TRANSFORM(RECCOUNT()) )
DO CASE
*... other cases skipped for brevity
CASE THIS.nTransportMode = 1
*** VFP7 CursorToXML
lcFileText = ""
CURSORTOXML(ALIAS(),"lcFileText",1,;
IIF(THIS.lUTF8,48,32),;
0,IIF(THIS.nSchema=1,"1","0"))
OTHERWISE
THIS.S_RETURNError("Invalid Transportmode: " +
TRANSFORM(THIS.nTransportmode))
RETURN .F.
ENDCASE
ELSE
*** Force an empty cursor
lcFileText = THIS.oXML.cXMLHeader + ;
"<wwhttpsql>" + CRLF + ;
"</wwhttpsql>" + CRLF
ENDIF
IF !EMPTY(THIS.cReturnValueXML)
lcFileText = STRTRAN(lcFileText,"</wwhttpsql>",
THIS.cReturnValueXML + "</wwhttpsql>")
ENDIF
IF USED(THIS.cCursorName)
USE IN (THIS.cCursorName)
ENDIF
THIS.cResponseXML = lcFileText
RETURN .T.
ParseXml()方法对XML进行验证与存储,ExecuteSql()方法执行Sql命令。在ExecuteSql中还对已命名参数(Named Parmeters)进行处理,以便稍后在Sql命令执行时可以使用。Sql命令通过动态执行引擎Macro执行,并且放在在Try/Catch结构中,以便能够捕捉到任何运行时的错误。
在运行查询之前,我们设置SYS(2335,0)来拒绝任何诸如”文件未找到”等UI错误。Sys(2335)是表明拒绝任何通过COM的UI访问。但为什么要设置sys(2335)呢,理由很简单,因为这是一个在Server上运行的程序,谁都不愿意在在自己的服务器上经常出现莫名其妙的对话框。这个功能只对使用COM的VFP程序有效,如果你的VFP程序没有使用COM,那Sys(2335,0)对此无能为力。
ExecuteSql()方法还能处理存储过程(stored procedure),他甚至能将对存储过程的调用映射到对象的方法调用中去。你可以自己改写wwwHttpSqlServer,增加一些符合SqlServer中存储过程的方法。
当查询执行完毕后,CreateXml方法根据Client提供的属性(比如传送方式,是否进行UTF 8的编码等)将结果转换为特定的XML,并且设置cResponseXml属性。
过程中的任何错误都将使用S_ReturnError方法,以一种特定的XML格式返回,同时cResponseXML=XML,下面是一个典型的错误消息:
<?xml version="1.0"?>
<wwhttpsql>
<error>
<errormessage>Could not find stored procedure 'sp_ww_NewsId'. [1526:2812]</errormessage>
</error>
</wwhttpsql>
在Client上,wwwHttpSql首先查看是否有错误信息被传回,如果有,立刻将错误信息赋值给IError与cErrorMsg属性,并且安全的返回。所以标准的wwwHttpSql应该总是在使用传回的数据之前检查IError标志位。
Dealing with the 255 character literal string limit in VFP
你需要注意的是VFP有一个255字符限制,简单来说就是你不能执行以下语句:
UPDATE SomeTable set LDescript='<longer than 255 char string>'
所以,如果你这样的执行sql代码:
lcSql = [UPDATE SomeTable set LDescript='] + lcLDescript + [']
程序很快就会因为字符数超过255而无法执行,为了解决这个问题,我们在查询中使用Named Parameters,就象我们在wwwHttpSqk中的AddSqlParameter()方法中实现的一样。将你的代码改为:
oHSql = CREATEOBJECT("wwHttpSql")
lcDescript = [Some Long String]
lcSQL = oHSql.AddSqlParameter("parmDescript",lcDescript)
oHSql.ExecuteSql([UPDATE SomeTable SET LDescript=parmDescript])
这样,我们将所需参数传送到Server上,在执行Sql命令之前重新构造,从而避免了字符数超过255。
你同样也可以针对存储过程使用AddSqlParameter,参数传送到Server,解包,通过Sql Passthrough插入到查询中,Listing 6为我们解释了如何做:
Listing 6: Calling a stored procedure using named parameters over the Web
oHSQL = CREATEOBJECT("wwHTTPSQL")
oHSQL.cServerUrl = "http://localhost/wconnect/wwhttpsql.http"
oHSQL.cSQLConnectString = ;
"driver={sql server};server=(local);database=wwDeveloper; "
oHSQL.cSQLCursor = "TDevelopers"
pnID = 0
pcTablename = "wwDevRegistry"
oHSQL.AddSQLParameter("pnID",pnID,,.T.) && Return this one back
oHSQL.AddSQLParameter("pcTableName",pcTableName)
oHSQL.AddSQLPArameter("pcIDTable","wwr_id")
oHSQL.AddSQLParameter("pcPKField","pk")
*** returns 0
? oHSQL.Execute("Execute sp_ww_NewId ?pcTableName,?@pnID")
*** pnID result value
? oHSQL.vResultValue
*** or explicitly retrieve a return value if there’s more than one
? oHSQL.GetSQLReturnValue("pnID")
注意,我在上面的例子中使用了cSQLConnectString来设置在Server上使用哪种连接,我将在稍后在对这种方法进行讨论。如果我们没有在Server上设定使用何种连接,那么我们可以在Client上设定,并将他传送到Server上。
你可以看到这里有一些额外的参数被设定:
<?xml version="1.0"?>
<wwhttpsql>
<sql>Execute sp_ww_NewId ?pcTableName,?@pnID</sql>
<sqlcursor>TSQLQuery</sqlcursor>
<sqlconnectstring> driver={sql server};server=(local);database=wwDeveloper;
</sqlconnectstring>
<transportmode>1</transportmode>
<utf8>1</utf8>
<sqlparameters>
<pnid type="N" return="1">0</pnid>
<pctablename type="C">wwDevRegistry</pctablename>
<pcidtable type="C">wwr_id</pcidtable>
<pcpkfield type="C">pk</pcpkfield>
</sqlparameters>
</wwhttpsql>
What about Security
如果你阅读了上面的文章,你也许会说:”这种方法很Cool,但太不安全了!你在Web上暴露你的数据接口,并且你无法限制在接口上执行的命令。”你说的对。
安全很重要,对于Http服务来说,Windows提供了两种验证方式,集成Windows验证(Windows Auth)和基本验证(Basic Authentication),你可以使用任意的一种验证方法来保护你的Url上的服务,在wwwHttpSql类中的cUsername和cPasseord提供了验证方法所需要的信任状(credentials)。(可以参考Winhelp中的” 验证方法”。)
你也可以使用基本验证,在我们的Web Connection服务器中,你可以使用如下方法进行检查:
*** Check for validation here
IF !THIS.Login("ANY")
RETURN
ENDIF
“Any”是指所有登陆的用户,但你也可以验证一个用户列表,但基本验证不支持进行组(Group)验证。这个验证方法是在任何对象被创建之前进行的,所以十分的安全,为你的程序提供了更高级别的保障。
如果你需要在传输过程中对数据进行加密处理,那么你可以使用HTTPS/SSL协议,你只需要在Server上提供一份证书就可以了。
在Server上,我们还可以通过更改cAllowedCommands来限制可以执行的Sql命令的类型,比如:
cAllowedCommands = ",select,insert,update,delete,execute,method,"
你可以移除不允许执行的Sql命令的类型,如果你不希望用户更改数据库中的数据,只留下”select”关键字就可以了。
你还可以在执行ParseXML()之前根据不同的逻辑来选择执行Sql命令的类型,但你必须使用更底层的方法来执行Sql命令,如下所示:
Listing 7– Checking the parsed SQL for filter criteria to disalllow commands
loData = CREATE("wwHTTPSQLServer")
loData.cAllowedCommands = "select,execute,insert,method,update"
loData.cConnectString = "" && Allow Odbc Access
IF loData.ParseXML(Request.FormXml())
*** Custom ERror Checking - disallow access to West Wind Files
IF ATC("WWS_", loData.cFullSQL) > 0
loData.S_ReturnError("Access to table denied")
ELSE
IF loData.ExecuteSQL()
loData.CreateXML()
ENDIF
ENDIF
ENDIF
ParseXML将XML中的相关信息存储到相应的属性中,所以你可以在ParseXML()方法之后读取wwwHttpSqlServer对象的任何属性,在这里我只是简单的对”wws_”关键字进行了过滤,你可以在这里写出复杂的逻辑来。
这样你就一举两得,即可以使用基于Windows的验证,又可以在Server上对Sql命令进行过滤。
Implementing the wwHttpSqlServer with ASP
在上面的列子中,我都是使用Web Connection作为wwwHttpSqlServer的Web平台,但我曾经说过,Server端的wwwHttpSqlServer可以运行在任何支持VFP的平台上,下面显示了在COM环境中wwwHttpSqlServer的主要部分
Listing 9 – wwHttpSqlServerCom implementation for operation in ASP and asp.net
DO wwHttpSqlServer && force libraries to be pulled in
DEFINE CLASS wwHttpSqlServerCOM as wwHttpSqlServer OLEPUBLIC
cAppStartPath = ""
************************************************************************
FUNCTION INIT
*********************************
*** Function: Set the server's environment. IMPORTANT!
************************************************************************
*** Make all required environment settings here
*** KEEP IT SIMPLE: Remember your object is created
*** on EVERY ASP page hit!
SET RESOURCE OFF && Best to compile into a CONFIG.FPW
SET EXCLUSIVE OFF
SET REPROCESS TO 2 SECONDS
SET CPDIALOG OFF
SET DELETED ON
SET EXACT OFF
SET SAFETY OFF
*** IMPORTANT: Figure out your DLL startup path
IF application.Startmode = 3 OR Application.StartMode = 5
THIS.cAppStartPath = ADDBS(JUSTPATH(Application.ServerName))
ELSE
THIS.cAppStartPath = SYS(5) + ADDBS(CURDIR())
ENDIF
*** If you access VFP data you probably will have to
*** use this path plus a relative path to get to it!
*** You can SET PATH here, or else always access data
*** with the explicit path
DO PATH WITH THIS.cAppStartpath
DO PATH WITH THIS.cAppStartPath + "wwDemo"
DO PATH WITH THIS.cAppStartPath + "wwDevRegistry"
*** Make sure to call the base constructor!
DODEFAULT()
ENDFUNC
ENDDEFINE
在上面一段程序中,我假设VFP访问的数据文件被放在存放DLL的目录或程序起始目录下的 wwDemo或wwDevRegistry目录中。
如果你是作为匿名用户登陆站点的话,你将会以IUSER_<MachineName>的身份来访问文件夹以保证Asp将会在一个安全的环境中访问那些数据文件,但可惜的是,在这里 IUSER_<MachineName>没有权利读写文件夹所以你可以采取以下两种办法中的任何一种:1.保证IUSER_account帐户可以读写存放数据的文件夹,2.不以匿名身份登陆,而是使用有权限读写存放数据的文件夹的帐户来登陆站点。
使用以下语句编译
BUILD MTDLL wwHttpDataService FROM wwHttpDataService RECOMPILE
使用以下语句测试,
o = CREATE("wwHttpDataService.wwHttpSqlServerCom")
如果成功的话,你可以象下面的程序一样将以下语句添加到你的ASP页面中去:
Listing 10 – Server Implementation for classic ASP
<%
'*** Get the XML input - easiest to load in DOM object
'set oXML = Server.CreateObject("MSXML2.DOMDOCUMENT")
set oXML = Server.CreateObject("MSXML2.FreeThreadedDOMDocument")
oXml.Async = false ' Make sure you read async
oXML.Load(Request)
set loData = Server.CreateObject("wwHttpDataService.wwHttpSqlServerCOM")
'loData.cConnectString = "server=(local);driver={SQL Server};database=wwDeveloper;"
loData.lUtf8 = False
loData.S_Execute(oXML)
'if loData.ParseXml(oXML)
' if loData.ExecuteSql()
' loData.CreateXml()
' end if
'end if
Response.Write(loData.cResponseXML)
'Response.Write(loData.CERRORMSG) ' debug
%>
注意你必须使用XML Free Thread DOM来保证XML被缓存在Post中。你可以简单的使用DOMDocument的Load方法来加载请求。在Asp中你还必须设置FreeThreadedDomDocument以保证线程的安全。
就象VFP中的一样,在ASP中你可以选择使用S_Execute方法或者其他更底层的方法来处理XML。我的建议是如果你想较少的调用外部的COM的话,使用S_Execute(),如果你需要更复杂的逻辑控制,则使用其他底层的方法。
接下来,我们唯一要做的就是指向Asp页面的Url.
Asp在这里工作的很好,但不要忘记asp.net。但我不推荐这样做,因为.NET中托管代码(managed Code)调用非托管代码(unmanaged code)挺麻烦的,asp.net必须通过TLBIMP来调用COM对象,而且在性能上又得不偿失,所以对于这种不太复杂而又对性能要求比较高的程序来说,Asp是最好的选择。
如果你必须使用asp.net,请查看以下连接中的文章。
http://www.west-wind.com/presentations/VfpDotNetInterop/aspcominterop.asp
From query to business object
现在,我们怎样使用这个功能呢?到目前为止,我们已经构造了一个2层(2 tier)的应用体系:Client上的前台程序和基于Web的远程数据引擎,他们可以很好的工作。但对于大多数开发者使用的多层的,基本商业对象模型来说,我们的体系还需要改进,幸运的是,我们只要通过简单的修改,就可以将我们的对象融入商业对象框架中去了。
我在这里将使用我自己编写的wwBusiness类作为例子来说明如何将我们的对象融入商业对象框架中去。在这之前,我有必要介绍一下wwBusiness类,以便大家更好的理解。
wwBusiness是一个简单的商业对象类,他提供了基本的CRUD针对各种不同数据源的(Create,Read,Update,Delete)功能。我们利用诸如Load(),Save(),New(),Query()等方法对不同数据源的数据进行操作。wwBussiness的一个特点是使用一个内部的oData成员来存储记录的基本数据。Load,New,Find方法将会将记录的数据赋给oData,一般来说,数据来自游标使用SCATTER NAME指向的记录。然而,我们可以重写(Overridden)一些方法来在oData对象中记录更多或更少的信息,只要相应类的方法(Save,Load,GetBlankRecord)也被重写以便支持更改过的数据。
wwBusiness支持以下三种数据访问方式:本地的VFP数据,SQLSERVER数据和通过兼容的接口来自与Web的数据。 在这里,Web数据提供接口是wwwHttpSqlData,下面让我们看一下这个数据提供接口是如何工作的。
我们需要将商业对象框架wwBusiness替代wwHttpSql来处理Sql命令,所以在客户端将wwBusiness类包含了wwHttpSql类。在Server端我们不需要做任何的改动,还是继续使用wwHttpSqlServer,图2为我们清楚的展示了这一点:
Figure 2 –.使用wwBussiness.wwHttpSql取代wwHttpS作为代理来访问Web数据源。
为了让wwBusiness访问Web数据源,我们需要对他进行一些改动。我们增加了一个参数cServerUrl,他类似与SQLSERVER中的连接字符串,用来定义需要连接的Url地址。并且增加了一个DataMode,DataMode=4表明使用wwHttpSql访问Web上的数据,Listing 8向我们展示了wwBusiness是如何通过wwHttpSql数据提供接口工作的:
Listing 8: Using wwBusiness with a Web data source
oDev = CREATEOBJECT("cDeveloper")
oDev.nDataMode = 4 && Web wwHttpSql
oDev.cServerUrl = "http://localhost/wconnect/wc.dll?http~HTTPSQL_wwDevRegistry"
*** Execute a raw SQL statement against the server
odev.Execute("delete wwDevregistry where pk = 220")
IF oDev.lError
? oDev.cErrorMsg
ENDIF
*** Run a query that returns a cursor
lnRecords = oDev.Query("select * from wwDevRegistry where company > 'L' ")
IF oDev.lError
? oDev.cErrorMsg
ELSE
BROWSE
ENDIF
*** Load one object
oDev.Load(8)
? oDev.oData.Company
? oDev.oData.Name
oDev.oData.Company = "West Wind Technologies"
IF !oDev.Save()
? oDev.cErrorMsg
ENDIF
*** Create a new record
? oDev.New()
loData = oDev.oData
loData.Company = "TEST COMPANY"
loData.Name = "Rick Sttrahl"
? oDev.Save()
*** Show added rec
? oDev.Query()
GO BOTT
BROWSE
有趣的是这里的代码比起连接SQLSERVER或Fox数据来说一点也不复杂,唯一不同的地方是cServerUrl与NDataMode的设定。假设在Server上运行着wwHttpSqlServer,并且数据也准备好了,我们就可以轻易的访问Web上的数据了,这是不是很Cool!
你可能还需要一些代码来设置访问时的登陆信息,时限(timeout)等:
*** Optional - configure any HTTP settings you need using wwHTTP properties
oDev.Open()
oDev.oHTTPSQL.cUsername = "rick"
oDev.oHTTPSQL.cPassword = "keepguessingbuddy"
oDev.oHTTPSQL.nConnectTimeout = 40
oDev.oHTTPSQL.nTransportMode = 0 && Use wwXML style
Open()方法只是创建wwHttpSql对象用以和Server进行通信。在wwHttpSql对象创建之后,你就可以设置诸如Username,password,timeout等属性了。
如果你不想在每次发送请求的时候都创建wwHttpSql对象,那么你可以在wwBusiness对象的oHttpSql属性中保留已经创建的对象,如下所示:
oDev.oHttpSql = THISFORM.oPersistedHttp
oDev.oHttpSql.nConnectTimeout = 40
如果你有些验证或代理信息需要设置的话,这很有用,你不必每次都去设置他们了。
wwBusiness还支持将继承,我们可以使用CreateChildObject()方法,将父对象的oHttpSql或oSql属性传递给任何子对象,这样的话,你就不必在每一个子对象中进行再配置了。
Hooking up to the wwBusiness object
那么到底如何在wwBusiness中使用wwHttpSql对象呢?Listing9中的程序将会为我们展示使用三种DataMode的Load方法将记录读取到oData中
Listing 9: The wwBusiness object Load() method with Web access support (4)
* wwBusiness.Load
LPARAMETER lnpk, lnLookupType
LOCAL loRecord, lcPKField, lnResult
THIS.SetError()
IF VARTYPE(lnpk) # "N"
THIS.SetError("Load failed - no key passed.")
RETURN .F.
ENDIF
*** Load(0) loads an empty record
IF lnPK = 0
RETURN THIS.Getblankrecord()
ENDIF
IF !THIS.OPEN()
RETURN .F.
ENDIF
DO CASE
CASE THIS.ndatamode = 0
lcPKField = THIS.cPKField
LOCATE FOR &lcPKField = lnpk
IF FOUND()
SCATTER NAME THIS.oData MEMO
IF THIS.lcompareupdates
SCATTER NAME THIS.oOrigData MEMO
ENDIF
THIS.nUpdateMode = 1 && Edit
ELSE
SCATTER NAME THIS.oData MEMO BLANK
IF THIS.lcompareupdates
SCATTER NAME THIS.oOrigData MEMO BLANK
ENDIF
THIS.SetError("GetRecord - Record not found.")
RETURN .F.
ENDIF
CASE THIS.ndatamode = 2 OR This.nDataMode = 4
IF this.nDataMode = 4
loSQL = this.oHttpSql
ELSE
loSql = loSql
ENDIF
lnResult = loSQL.Execute("select * from " + THIS.cFileName + " where " + ;
THIS.cPKField + "=" + TRANSFORM(lnpk))
IF lnResult # 1
IF loSql.lError
THIS.SetError(loSql.cErrorMsg)
ENDIF
RETURN .F.
ENDIF
IF RECCOUNT() > 0
SCATTER NAME THIS.oData MEMO
IF THIS.lcompareupdates
SCATTER NAME THIS.oOrigData MEMO
ENDIF
THIS.nUpdateMode = 1 && Edit
ELSE
SCATTER NAME THIS.oData MEMO BLANK
IF THIS.lcompareupdates
SCATTER NAME THIS.oOrigData MEMO BLANK
ENDIF
THIS.SetError("No match found.")
RETURN .F.
ENDIF
ENDCASE
RETURN .T.
注意,对于nDataMode=VFP的模式,我们只是简单的使用LOCATR和SCATTER,然而当nDataMode=SQL(2)或Web(4)时,我们执行了一个Select语句,并且使用SCATTER。注意针对SQL和Web的代码十分的类似,因为我们对于SQLSERVER的访问是通过类似wwHttpSql的wwSQL类来实现的,而wwSQL与wwHttpSql接口定义是一样.
接下来,我们看一个复杂一点的例子,使用Save方法将数据对数据库进行插入或更新操作:
Listing 10: The wwBusiness :: Save() method
LOCAL lcPKField, llRetVal, loRecord
llRetVal = .T.
THIS.SetError()
*** Optional auto Validation
IF THIS.lValidateOnSave AND ;
!THIS.VALIDATE()
RETURN .F.
ENDIF
loRecord = THIS.oData
IF !THIS.OPEN()
RETURN .F.
ENDIF
DO CASE
CASE THIS.ndatamode = 0
DO CASE
CASE THIS.nupdatemode = 2 && New
APPEND BLANK
GATHER NAME loRecord MEMO
THIS.nupdatemode = 1
CASE THIS.nupdatemode = 1 && Edit
lcPKField = THIS.cPKField
LOCATE FOR &lcPKField = loRecord.&lcPKField
IF FOUND()
GATHER NAME loRecord MEMO
ELSE
APPEND BLANK
GATHER NAME loRecord MEMO
ENDIF
ENDCASE
CASE THIS.ndatamode = 2 OR THIS.nDataMode = 4
IF THIS.nDataMode = 2
loSQL = THIS.oSQL
ELSE
loSQL = THIS.oHTTPSql
ENDIF
DO CASE
CASE THIS.nupdatemode = 2 && New
loSQL.cSQL = THIS.SQLBuildInsertStatement(loRecord)
loSQL.Execute()
IF loSQL.lError
THIS.SetError(loSQL.cErrorMsg)
RETURN .F.
ENDIF
THIS.nupdatemode = 1
CASE THIS.nupdatemode = 1 && Edit
*** Check if exists first
loSQL.Execute("select " +THIS.cPKField +" from " + THIS.cFileName +;
" where " + THIS.cPKField + "=" + TRANS(loRecord.pk))
IF loSQL.lError
THIS.SetError(loSQL.cSQL)
RETURN .F.
ENDIF
IF RECCOUNT() < 1
loSQL.Execute( THIS.SQLBuildInsertStatement(loRecord) )
ELSE
loSQL.Execute( THIS.SQLBuildUpdateStatement(loRecord) )
ENDIF
IF loSQL.lError
THIS.SetError(loSQL.cErrorMsg)
RETURN .F.
ENDIF
ENDCASE
ENDCASE
RETURN llRetVal
再次提醒你,SQL与Web数据访问方式是使用一段几乎相同的代码来实现的,即使在wwBusiness中,我们也不需要更改任何代码,因为wwHttpSql与wwSql在接口定义上是一样的。
可以看到Insert与Update语句是由SqlBuildInsertStatement方法实现的,而且根据oData中的内容自动的生成Insert或Update语句。
在商业对象(business object)的其他一些方法(Method)中也有类似的情况,所以如果我们需要访问Web上的一个远程数据源的话,我们几乎不用更改商业对象的任何代码,而且只需要几行简单的代码就可以实现,是不是很Cool。
Where’s the Remote?
当我们构造一个分布式的应用程序时,我们总是要考虑到如何才能集成远程数据源中的数据,你可以有两种完全不同的方法来实现。1.在你的本地机器上实现所有的业务逻辑(business logic),而不是在Server端,只从server上下载你需要的数据,就象WebService一样。这样做的另一个好处是你只需要在管道(wire)中传送数据,而不需要担心SOAP验证与格式,就象使用VFP和VFP进行通讯一样,这样做更有效率。2.在Server上布置逻辑,这不是我们讨论的重点,在这里我不细说。
但请牢牢记住,如果你只在你的Client中实现逻辑,而完全不使用业务逻辑层的话,你实际上只是实现了一个两层体系(2-tier Enviroment),而Server 端对你的分布式程序来说只是一个数据服务端,这和传统意义上的分布式程序完全不同,但他却能提供更简单,扩展性更好的应用。
下一次你构造你的Web服务时,考虑一下这种方式是多么的简单,他也许不适合所有的分布式应用,但确是一个从Server上快速,有效的获取数据(May be dirty)的好方法。