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


    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()命令时,会执行以下步骤:

      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 = ;

    "<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按以下步骤处理请求:

      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 = "<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)的好方法。

  • 相关阅读:
    在Oracle怎样查询表中的top10条记录
    Ant
    oracle 时间函数(sysdate)
    oracle 时间函数 (to_date)
    Maven仓库管理器
    数据库中select into from 和 insert into select的区别
    oracle 定时器简单用法
    oraclea 定时器
    XP Home Edition SP2 也可以装 Rational Rose 2003
    Unity3D动态天空之UniSky
  • 原文地址:https://www.cnblogs.com/chinaontology/p/1313505.html
Copyright © 2020-2023  润新知