[转载]使用 Visual FoxPro提供一个基于互联网的数据服务(翻译)

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。    select * from wwDevRegistry    TDevelopers    1 当client上执行Execute()命令时,会执行以下步骤:
  1. 根据你在程序中设置的属性,生成如上的XML。
  2. XML被发送到指定的Url。
  3. Server端处理请求并返回XML形式的结果。一般来说,结果都是以XML形式返回的,除非发生了硬件错误,而软件错误也会以XML形式返回。
  4. Client收到响应信息并进行解析。
  5. 首先会验证响应信息是否为XML形式的,如果不是,则产生一个错误(Error),请求失败。
  6. 如果响应信息中包含错误的XML节,则请求失败,将错误信息赋值给IError和CErrorMSg。
  7. 如果响应的XML信息中包含一个返回值,那我们将他赋值给VReturnValue。
  8. 如果在响应的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 = ; "" + 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) + "" + CRLF + ;                                 THIS.cSQLParameters + ;                                 CHR(9) + "" + CRLF,"") IF THIS.lUTF8    lcXML = lcXML + loXML.AddElement("utf8","1",1) ENDIF lcXML = lcXML + "" 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,"","") 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 "" $ 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按以下步骤处理请求:
  1. 验证传来的XML请求。如果成功,将XML的内容存储在属性中。
  2. 执行请求中的Sql命令。
  3. 将结果编码为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 = ""  + CRLF + ;            THIS.oXML.AddElement(lcReturnVar,&lcReturnVar,1) +;            "" +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 + ;                      "" + CRLF + ;                      "" + CRLF ENDIF IF !EMPTY(THIS.cReturnValueXML)    lcFileText = STRTRAN(lcFileText,"",             THIS.cReturnValueXML + "") 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,下面是一个典型的错误消息:              Could not find stored procedure 'sp_ww_NewsId'. [1526:2812]    在Client上,wwwHttpSql首先查看是否有错误信息被传回,如果有,立刻将错误信息赋值给IError与cErrorMsg属性,并且安全的返回。所以标准的wwwHttpSql应该总是在使用传回的数据之前检查IError标志位。 Dealing with the 255 character literal string limit in VFP你需要注意的是VFP有一个255字符限制,简单来说就是你不能执行以下语句: UPDATE SomeTable set LDescript='' 所以,如果你这样的执行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上。 你可以看到这里有一些额外的参数被设定:    Execute sp_ww_NewId ?pcTableName,?@pnID    TSQLQuery    driver={sql server};server=(local);database=wwDeveloper;            1    1             0          wwDevRegistry          wwr_id          pk    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_的身份来访问文件夹以保证Asp将会在一个安全的环境中访问那些数据文件,但可惜的是,在这里 IUSER_没有权利读写文件夹所以你可以采取以下两种办法中的任何一种: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)的好方法。

相关内容推荐