In questo breve articolo vengono introdotti molti argomenti interessanti, dalle funzionalità di ADO 2.5, all'utilizzo di pagine XML, allo sviluppo di applicativi client/server a tre livelli (three-tier). A trattarli tutti con dovizia, ne risulterebbe materiale sufficiente a scrivere un libro intero, quindi questo articolo non si propone come una guida completa a tali argomenti, ma li introduce e li tratta in maniera strettamente necessaria al fine di spiegare il funzionamento dei progetti d'esempio e di impostare un semplice applicativo per la gestione di un database remoto. Al fine di rendere il codice e l'esempio più semplice possibile, le funzionalità sono state limitate all'essenziale e alla fine verranno indicate possibili implementazioni e qualche consiglio su come realizzarle.
Molto spesso, ad esempio nel caso di soluzioni di e-Commerce o siti web dinamici che si interfacciano ad un database, si ha la necessità di gestire un database residente su un server remoto, accessibile tramite il protocollo HTTP.
Supponiamo infatti che abbiamo un database residente sul server aziendale o su quello che ci fornisce hosting per il nostro sito e che abbiamo bisogno di aggiornarlo, mantenendo comunque una replica sul nostro PC locale: le soluzioni che mi vengono in mente sono:
Però potremo anche aver bisogno di interfacciarci al database, quindi non semplicemente aggiornarlo,
ma impostare come origine dati di un oggetto Recordset una tabella appartenente al db remoto, così da
poter gestire il recordset come se si trattasse di un db locale, utilizzando l'interfaccia
di ADO.
In questo breve
articolo vedremo appunto come realizzare un applicativo di questo tipo.
Realizzare un'applicativo client/server per l'interfacciamento ad un database remoto.
Verranno sviluppate due applicazioni a tre livelli (three-tier) con funzionalità analoghe ma che
adottano due tecnologie diverse:
In questo articolo e negli esempi allegati si utilizzerà un database in formato Access 97,
ma si può facilmente trasportare il codice per gestire un db SQL Server o un altro DBMS.
La versione di ADO installata sul client dev'essere la 2.5 (2.0 per il secondo esempio,
quello che utilizza la DLL ActiveX)
o successive, mentre il server dev'essere NT con IIS e bisogna impostare i permessi di
lettura e scrittura alla directory contenente il database.
Per quanto riguarda la DLL ActiveX, oltre a registrare tale componente sul client,
è necessario specificare i riferimenti sul server, aggiungendo nel registro di configurazione
la chiave DLLRemoteDB.clsRemoteDB al percorso:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W3SVC\Parameters\ADCLaunch.
Il progetto si divide in due componenti: il componente client ed il componente server. Il componente
a livello client è l'applicazione Visual Basic che interrogherà il database, mentre la parte
server include il livello intermedio, costituito da un insieme di pagine ASP che verranno
utilizzate per generare il recordset da fornire al client, ed il livello fonte dati,
costituito da un database Access. In pratica funziona così:
Partiamo dal presupposto che non è possibile specificare nella stringa di connessione il percorso
HTTP su cui risiede il database, altrimenti non avrebbe senso scrivere questo articolo. La soluzione è
quella di impostare come origine dei dati di un recordset una pagina ASP che interroga il database e fornisce
un recordset formattato in XML.
Vediamolo in pratica.
Sappiamo che
per aprire un recordset con i dati di una tabella, in ASP scriveremo:
| <% | |
| |
| %> | |
Il nome della tabella da aprire viene passata come parametro TableName; inoltre
si noti che il cursore impostato per la connessione e per il recordset è adUseClient,
mentre per il recordset deve essere impostato un blocco di tipo ottimistico a blocchi (adLockBatchOptimistic), al fine di poter fornire al
client un recordset con i diritti di lettura/scrittura e non solo di lettura
(come di default). Inoltre questi cursori sono consigliati nel caso di
aggiornamenti batch poiché il record viene bloccato solo quando viene invocato
l'aggiornamento e non per tutta la durata della modifica (il che sarebbe
impossibile dal momento che la modifica la esegue il client sconnesso dal
database).
Una volta aperto il recordset, bisognerà in qualche modo renderlo disponibile al client, il quale
(vedremo tra poco il codice) si aspetta che la pagina ASP gli restituisca un oggetto Recordset.
Questo non è proprio possibile, per cui quello che possiamo
fare è formattare il contenuto del recordset in XML e poi ci pensa ADO (dalla
versione 2.1 in poi) a convertirlo in un oggetto Recordset vero e proprio.
| <% | |
| |
| %> | |
L'oggetto Stream è essenzialmente un file salvato in memoria e non su disco,
quindi l'istruzione rs.Save stm,
adPersistXML essenzialmente converte (visto che
non viene memorizzato da nessuna parte) il recordset in formato XML. Infine il contenuto dello stream
(proprietà ReadText dell'oggetto ADO Stream), viene restituito al client in formato XML
(Response.ContentType = "text/xml").
A questo apunto il server ha terminato il suo compito; al
client è stata restituita una pagina XML che contiene il recordset (struttura,
intestazioni e valori dei campi): ora sta ad ADO convertirla in un oggetto
Recordset ADO.
Vediamo com'è il codice sul client:
Set rs = New ADODB.Recordset
With rs
If .State = adStateOpen Then .Close
.CursorLocation = adUseClient
.Open "http://localhost/inetpub/wwwroot/query.asp?TableName=Address"
End With
Come vedete si tratta di un codice molto semplice: viene impostato il cursore per recordset, come
già detto, su adUseClient e quindi si invoca il metodo Open dell'oggetto
Recordset indicando come origine dati l'URL della pagina ASP (in questo caso il nome della tabella
da aprire è Address).
Tutto qui? In effetti, se non fosse per la carenza di documentazione a riguardo, fin qui sarebbe
piuttosto semplice; le cose si complicano un po' se qualcosa va storto...
Infatti se viene generato un errore in una fase qualsiasi, come ad esempio in fase di connessione
al database o di lettura del recordset, ADO si aspetta comunque di ricevere un oggetto Recordset, o
quantomeno "un qualcosa che ci assomigli" (mi riferisco al recordset formattato in XML) e non un
messaggio d'errore. Come fare, dunque? Qui occorre un po' d'astuzia... Io ho risolto con un piccolo
artificio: ADO si aspetta un recordset? Un recordset qualunque, poiché non sa che c'è nel database...
Bene, in caso di errore genero un recordset di una sola riga con i campi necessari a descrivere
il tipo di errore; il client si accorge che il recordset è un po' "particolare" e gestisce l'errore.
Vediamolo in pratica: sulla pagina ASP che genera il recordset, dopo aver inserito un bel
On Error Resume
Next all'inizio della procedura (la gestione degli errori in ASP non è il
massimo), prima di salvare il recordset scrivo:
| <% | |
| |
| %> | |
In pratica nel caso in cui venga restituito un errore, viene generato un recordset che riporta il codice,
la descrizione dell'errore e l'oggetto che l'ha generato; il campo Type viene completato
con la stringa "##Error##" affinché il client riconosca che quel
recordset non contiene i dati prelevati dalla tabella del database ma la
descrizione di un messagio d'errore. La verifica dello stato del recordset e
relativa chiusura nel caso in cui risulti aperto non dovebbe esser necessaria,
poiché si presume che se è avvenuto un errore, il recordset non sia stato
aperto. Una verifica in più, però, non fa mai male: almeno ci togliamo ogni
ombra di dubbio.
Affinché il client si accorga che il recordset restituito dall'ASP contiene un messaggio d'errore anziché
i dati voluti, ho effettuato un controllo sul nome ed il valore del primo campo: se risultano
essere rispettivamente Type e "##Error##", stampo a video il contenuto degli altri
campi per segnalare l'errore:
If rs.Fields(0).Name = "Type" And rs.Fields(0).Value = "##Error##" Then
MsgBox rs("Description") & vbCrLf & "Codice: " & _
rs("Code"), vbCritical, rs("Source")
End If
Ho supposto che un controllo combinato sul nome del campo ed il suo contenuto sia sufficiente per stabilire se si tratta di un recordset contenente un messaggio di errore o i dati voluti: in caso contrario si possono modificare tali valori.
A questo punto abbiamo il nostro bel recordset (o il nostro bel messaggio d'errore :-);
lo modifichiamo in locale restando sconnessi dal database remoto, quindi abbiamo bisogno di
salvare le modifiche. Anche in questo caso ci appoggeremo ad una pagina ASP che effettuerà
l'update (batch) e anche in questo caso utilizzaremo XML come veicolo per la comunicazione tra
il client ed il server, inviando uno stream come parametro alla pagina ASP.
La pagina ASP che eseguirà l'update è
forse ancora più semplice di quella che esegue la query:
| <% | |
| |
| %> | |
Innanzitutto si imposta la connessione attiva dell'oggetto Recordset (ActiveConnection)
al database già aperto in precedenza, dopo di che si richiama il metodo UpdateBatch che aggiorna il recordset. Si noti solo
come viene aperto il recordset: viene passato direttamente il codice XML inviato
dal client e convertito automaticamente da ADO.
Da quanto abbiamo appena visto, possiamo già immaginare come dovrà essere la procedura
per l'aggiornamento sul client: il recordset viene salvato in uno stream XML, quindi inviato alla pagina ASP.
L'unico problema è come inviare al server lo stream XML e come ricevere un eventuale messaggio d'errore:
A questo ci pensa un nuovo oggetto introdotto in ADO 2.5: MSXML, un oggetto
che fornisce l'interfaccia per la gestione degli XML.
Il codice sul
client sarà il seguente:
Dim stm As ADODB.Stream
Dim xml As MSXML.XMLHTTPRequest
Set xml = New MSXML.XMLHTTPRequest
Set stm = New ADODB.Stream
rs.Save stm, adPersistXML
xml.Open "POST", "http://localhost/inetpub/wwwroot/Update.asp", False
xml.Send stm.ReadText
If xml.responseText <> "" Then MsgBox Right(xml.responseText, _
Len(xml.responseText) - InStr(xml.responseText, vbCrLf) - 1), _
vbCritical, Left(xml.responseText, InStr(xml.responseText, vbCrLf) - 1)
Così viene creato lo stream XML contenente la struttura ed i dati del recordset e con i metodi
Open quindi Send dell'oggetto MSXML viene prima aperta
la pagina ASP, poi inviato l'XML con il metodo POST.
L'ultima riga introduce la gestione degli errori per questa procedura: l'oggetto
MSXML permette anche di ricevere dati in formato XML in risposta da una pagina ASP.
Questo grazie alla proprietà responseText: in questo caso vengono stampati a
video il codice di errore, la descrizione e l'oggetto che lo ha generato,
inviati dalla pagina ASP in questo modo:
| <% | |
| |
| %> | |
Come avete potuto constatare, ho considerato solo gli aspetti fondamentali per la realizzazione
di questo esempio, al fine di renderne più semplice la comprensione.
Utili implementazioni possono essere, oltre ad una più dettagliata gestione degli errori
(l'importante era capire come intercettarli e processarli), la gestione delle transazioni,
l'autenticazione dell'utente e magari anche la generazione della query SQL da parte del client.
A tal proposito, però, è necessario prestare la massima attenzione poiché se si passasse la query
alla pagina ASP, sarebbe possibile che l'utente generi una query del tipo "DROP TABLE",
il che non sarebbe troppo salutare per il nostro db...
In generale si ricordi che le pagine ASP sono l'interfaccia di gestione del database e che la directory
su cui questo risiede ha sia i permessi di lettura che di scrittura. Quindi sarebbe meglio che le pagine ASP
non risiedano nella stessa directory del database e progettarle attentamente affinché l'utente
non si prenda troppe libertà (come nel caso della query citata poc'anzi).
Anche questo progetto, ovviamente, è diviso tra i componenti client e server:
Il livello client è un front-end in Visual Basic molto simile a quello sviluppato nell'esempio
precedente, mentre sul server sono raggruppati il livello fonte dati (che sarà sempre il
nostro database Access) ed il livello intermedio, costituito non più da un'insieme di
pagine ASP, ma da una DLL ActiveX.
Questa DLL esporrà fondamentalmente tre medoti che consentiranno al client di stabilire
una connessione con il database, ottenere un oggetto Recordset e inviare le modifiche al recordset
modificato.
Il metodo per la
connessione al database è così definito:
Public Function Connect() As String
Set conn = New ADODB.Connection
On Error GoTo ErrHandler:
With conn
.CursorLocation = adUseClient
.ConnectionString = "driver={Microsoft Access Driver (*.mdb)};dbq=" & _
App.Path & "\example.mdb"
.Open
End With
Exit Function
ErrHandler:
Connect = Err.Source & vbCrLf & Err.Number & vbCrLf & Err.Description
End Function
Fino a qui niente di nuovo: viene effettuata la connessione al database allo stesso modo di
quanto visto sopra e nel caso in cui venga generato un errore, viene restituita al client
una stringa che include il codice d'errore, la descrizione e l'oggetto che lo ha generato.
Conn è un oggetto ADODB.Connection dichiarato a livello di
modulo affinché mantenga la sua validità all'interno di tutti e tre i metodi
della classe.
Il metodo per restituire al client l'oggetto Recordset è invece:
Public Function GetRecordset(TableName As String) As Recordset
On Error Resume Next
Connect
Set rs = New ADODB.Recordset
With rs
.CursorLocation = adUseClient
.Source = "SELECT * FROM " & TableName
Set .ActiveConnection = conn
.CursorType = adOpenStatic
.LockType = adLockOptimistic
.Open
End With
Set GetRecordset = rs
End Function
Anche questo è molto simile a quanto visto precedentemente: il nome della tabella viene passato
come parametro alla funzione ed utilizzato nella generazione della query SQL ed i cursori applicati
al recordset sono gli stessi dell'esempio precedente. Come nell'esempio precedente viene
ristabilita la connessione al solito database ed impostata come connessione attiva del recordset.
A differenza dell'esempio precedente, però, qui non è necessaria una gestione degli
errori: infatti se avviene un errore nella connessione al database, questo viene gestito
dal metodo Connect, mentre se viene generato
un errore nella generazione del recordset, dal momento che questo viene
restituito direttamente al client, senza alcuna conversione di formato, l'errore
potrà benissimo venir gestito dal client stesso. Discorso analogo vale per
l'altro metodo, quello che ci permette di aggiornare le modifiche:
Public Function UpdateRecordset(RSToUpdate As Recordset) As Recordset
On Error Resume Next
Connect
Set rs = RSToUpdate
Set rs.ActiveConnection = conn
rs.Filter = adFilterPendingRecords
rs.UpdateBatch adAffectGroup
Set UpdateRecordset = rs
End Function
Anche questo codice è già visto, ma a differenza di quanto fatto precedentemente, viene
impostato un filtro al recordset per isolare solo i record modificati, quindi questi (e solo
questi) vengono aggiornati con il metodo UpdateBatch.
Così come l'oggetto Connection, anche l'oggetto ADODB.Recordser, rs, è dichiarato a livello di modulo.
Se questo era il componente a livello intermedio (middle-tier), piuttosto simile a quello
visto nell'esempio precedente (se non più semplice, visto che adotta un numero minore di
tecnologie), il componente a livello di client sarà forse ancora più semplice e forse più
efficace, poiché permetterà una migliore gestione degli errori.
Innazitutto è necesario istanziare gli oggetti:
Public rs As ADODB.Recordset
Public ds As RDS.DataSpace
Public objProxy As Object
Set ds = New RDS.DataSpace
ds.InternetTimeout = 15000
Set objProxy = ds.CreateObject("DLLRemoteDB.clsRemoteDB", "http://localhost")
I tre oggetti vengono definiti a livello di modulo, così da mantenere la loro validità
all'interno di tutte le procedure; qui è stato introdotto un nuovo tipo di oggetto:
RDS.DataSpace. La connessione tra il livello client ed il livello intermedio
viene quasi sempre gestita da RDS (mentre nell'esempio precedente è stato utilizzato
l'oggetto MSXML) e l'oggetto DataSpace consente appunto di
creare un proxy per gestire tale connessione. Per completezza viene impostato un
TimeOut di 15 secondi per garantire il completamento della connessione anche nei
casi peggiori: mi auguro non dobbiate aumentare tale valore...
Una volta creato il proxy, si può direttamente richiamare il metodo GetRecordset
fornito dal nostro ActiveX, poiché la connessione, se ben vi ricordate, viene impostata
automaticamente. Però, dal momento che dobbiamo anche verificare che la connessione si andata
a buon fine e l'invio di un eventuale messaggio d'errore è stato delegato unitamente
al metodo Connect, conviene prima invocare quest'ultimo,
verificare che non siano stati generati errori, quindi richiedere il recordset.
Dim strErr As String
strErr = objProxy.Connect
If strErr = "" Then
Set rs = New ADODB.Recordset
Set rs = objProxy.GetRecordset("Address")
Else
MsgBox Right(strErr, Len(strErr) - InStr(strErr, vbCrLf) - 1), vbCritical, Left(strErr, InStr(strErr, vbCrLf) - 1)
End If
In questo modo se non vengono generati errori viene invocato il metodo GetRecordset
del proxy, pasandogli come parametro il nome della tabella sulla quale eseguire
la query, altrimenti viene visualizzato un messaggio con la descrizione
dell'errore.
Una volta apportate le modifiche al recordset, basta richiamare il metodo UpdateRecordset passandogli come parametro il
recordset modificato. Come nel caso precedente viene fatta una verifica in fase
di ripristino della connessione al database:
Dim strErr As String
strErr = objProxy.Connect
If strErr = "" Then
Set rs = objProxy.UpdateRecordset(rs)
Else
MsgBox Right(strErr, Len(strErr) - InStr(strErr, vbCrLf) - 1), vbCritical, Left(strErr, InStr(strErr, vbCrLf) - 1)
End If
Anche in questo esempio ho considerato solo gli aspetti fondamentali, al fine di renderne
più semplice la comprensione.
Analogamente a quanto detto per l'esempio precedente, utili implementazioni possono
essere, oltre ad una più dettagliata gestione degli errori (in questo caso non molto diversa
da quella cui si è abituati di solito per la gestione di un database locale), la gestione
delle transazioni, l'autenticazione dell'utente e magari anche la generazione della
query SQL da parte del client, con le stesse note del precedente esempio.
A questo punto può venir spontaneo chiedersi: quale soluzione è preferibil adottare?
L'utilizzo di un componente ActiveX o un componente realizzato in ASP?
Non credo esista una risposta definitiva o, almeno io non sono in grado di darla.
Sono due soluzioni diverse, nonostante si prefiggano lo stesso scopo e ottangano lo
stesso risultato. La scelta migliore è a mio avviso quella che meglio si adatta alle
esigenze specifiche, a seconda delle caratteristiche di ogni singola applicazione. Ad
esempio se l'applicazione deve utilizzare XML come formato di interscambio, non solo tra
client e server, allora la soluzione più adatta è la prima; la seconda, dal canto suo,
permette una migliore e più efficiente gestione degli errori e permette, inoltre,
l'implementazione di regole aziendali o di procedure complesse che richiedono una maggior
flessibilità rispetto a quella offerta da un linguaggio di scripting; inoltre può risultare
più semplice interfacciarsi ad altri componenti COM distribuiti su più livelli. La prima
soluzione, ancora, risulta più immediata nel caso di applicazioni Web based, che non richiederebbero
così l'utilizzo di componenti compilati, oppure nel caso in cui non si abbia la facoltà
di registrare componenti sul server (come ad esempio nel caso di molti servizi di Web Hosting).
Queste sono solo alcune ipostesi che mi vengono in mente: dal momento che generalmente
un'applicazione di questo tipo richiede un analisi preventiva piuttosto attenta e complessa,
non risulterà difficile scegliere la soluzione che meglio si adatterà a quella specifica esigenza:
l'importante è sapere che non c'è un'unica strada e conoscerne i punti d'accesso ed i possibili sbocchi.
Spero che questo breve articolo sia stato d'aiuto a tal
proposito, che vi abbia permesso di acquisire le nozioni necessarie per
costruire lo scheletro di un'applicazione client/server a tre livelli basata su
una fonte dati remota. Ora sta a voi implementarla secondo le vostre necessità.
Prima di chiudere, vale la pena di dare un'occhiata a come appare l'ADO Stream che
contiene il recordset formattato in XML.
Questo è il risultato della query che ritorna lo
stream al client:
<xml xmlns:s='uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882'
xmlns:dt='uuid:C2F41010-65B3-11d1-A29F-00AA00C14882'
xmlns:rs='urn:schemas-microsoft-com:rowset'
xmlns:z='#RowsetSchema'>
<s:Schema id='RowsetSchema'>
<:ElementType name='row' content='eltOnly' rs:updatable='true'>
<s:AttributeType name='Name' rs:number='1' rs:nullable='true'
rs:write='true' rs:basetable='Address' rs:basecolumn='Name'>
<:datatype dt:type='string' rs:dbtype='str' dt:maxLength='50'/>
</s:AttributeType>
<:AtributeType name='Address' rs:number='2' rs:nullable='true'
rs:write='true' rs:basetable='Address' rs:basecolumn='Address'>
<:datatype dt:type='string' rs:dbtype='str' dt:maxLength='50'/>
<:AttributeType>
<s:AttributeType name='City' rs:number='3' rs:nullable='true'
rs:write='true' rs:basetable='Address' rs:basecolumn='City'>
<s:datatype dt:type='string' rs:dbtype='str' dt:maxLength='50'/>
</s:AttributeType>
<s:AttributeType name='Tel' rs:number='4' rs:nullable='true'
rs:write='true' rs:basetable='Address' rs:basecolumn='Tel'>
<s:datatype dt:type='string' rs:dbtype='str' dt:maxLength='10'/>
</s:AttributeType>
<s:extends type='rs:rowbase'/>
</s:ElementType>
</s:Schema>
<rs:data>
<z:row Name='Pippo' Address='Via Battisti, 35' City='Trieste'
Tel='000111'/>
<z:row Name='Pluto' Address='Via XX Settembre, 15' City='Genova'
Tel='111000'/>
<z:row Name='Paperino' Address='Corso V. Emanuele, 30'
City='Milano' Tel='101010'/>
</rs:data>
</xml>
Invece quello che segue è il contenuto dello stream con le modifiche al recordset che viene passato alla pagina ASP predisposta per l'aggiornamento:
<xml xmlns:s='uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882'
xmlns:dt='uuid:C2F41010-65B3-11d1-A29F-00AA00C14882'
xmlns:rs='urn:schemas-microsoft-com:rowset'
xmlns:z='#RowsetSchema'>
<s:Schema id='RowsetSchema'>
<s:ElementType name='row' content='eltOnly' rs:updatable='true'>
<s:AttributeType name='Name' rs:number='1' rs:nullable='true'
rs:write='true' rs:basetable='Address' rs:basecolumn='Name'>
<s:datatype dt:type='string' rs:dbtype='str' dt:maxLength='50'/>
</s:AttributeType>
<s:AttributeType name='Address' rs:number='2' rs:nullable='true'
rs:write='true' rs:basetable='Address' rs:basecolumn='Address'>
<s:datatype dt:type='string' rs:dbtype='str' dt:maxLength='50'/>
</s:AttributeType>
<s:AttributeType name='City' rs:number='3' rs:nullable='true'
rs:write='true' rs:basetable='Address' rs:basecolumn='City'>
<s:datatype dt:type='string' rs:dbtype='str' dt:maxLength='50'/>
</s:AttributeType>
<s:AttributeType name='Tel' rs:number='4' rs:nullable='true'
rs:write='true' rs:basetable='Address' rs:basecolumn='Tel'>
<s:datatype dt:type='string' rs:dbtype='str' dt:maxLength='10'/>
</s:AttributeType>
<s:extends type='rs:rowbase'/>
</s:ElementType>
</s:Schema>
<rs:data>
<rs:update>
<rs:original>
<z:row Name='Pippo' Address='Via Battisti, 35' City='Trieste'
Tel='000111'/>
</rs:original>
<z:row Address='Via Battisti, 36'/>
</rs:update>
<z:row Name='Pluto' Address='Via XX Settembre, 15' City='Genova'
Tel='111000'/>
<z:row Name='Paperino' Address='Corso V. Emanuele, 30'
City='Milano' Tel='101010'/>
</rs:data>
</xml>
| [Scarica il progetto d'esempio] | Ultima revisione: 04/04/2001 |
© 2001, Sirri Moreno (sirri@morenosoft.com)
E' vietata qualsiasi riproduzione, anche parziale, senza il consenso dell'autore.