[转载]使用 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)的好方法。

原文地址:https://www.cnblogs.com/chinaontology/p/1313505.html