Sections

FG&A s.r.l.

Personal tools
You are here: Home Articoli AutoQuery: semplificare le query con BDE, dbExpress o Interbase Express
Document Actions

AutoQuery: semplificare le query con BDE, dbExpress o Interbase Express

Succede spesso di dover eseguire una query molto semplice (o anche molto complessa) che serve solo per ottenere un risultato e poi deve essere subito chiusa....

Author: Mario Graziosi, mgnospam@fgasoftware.com 

Succede spesso di dover eseguire una query molto semplice (o anche molto complessa) che serve solo per ottenere un risultato e poi deve essere subito chiusa. Per esempio:

  • Ottenere il numero di righe in una tabella (count(*))
  • Ottenere un elenco per caricarlo in un vector e, dopo averlo caricato, chiudere la query

Possiamo definire una query (es. TQuery) sulla form, impostarne l'SQL ed eseguirla, utilizzandola più volte per ogni tipo di esigenza. Se la query è usata da più metodi, si può ipotizzare di definirla in un data module. Questa soluzione, anche se semplice, aumenta la complessità nel nostro codice perchè a tutti gli effetti, la query che abbiamo sulla form (o, peggio, nel data module) verrà condivisa da più parti dello stesso programma e prima o poi ne pagheremo i costi di manutenzione. Inoltre rende il nostro codice fragile perchè se la query non viene chiusa (a causa di una eccezione o per distrazione) le risorse allocate (memoria e cursore sul server) non verranno rilasciate.

La soluzione ideale è quella di definire la query localmente alla funzione e rilasciarla immediatamente dopo aver ottenuto il risultato desiderato. Per esempio:

    int getCustomersCount()
        {
        TQuery* q = new TQuery(0); // [1]
        q->SQL->Text = "select count(*) from CUSTOMER";
        q->Database = myDatabase;
        q->Open();
        int result = q->Fields->Fields[0]->AsInteger;
        q->Close(); // Questo non e' necessario
        delete q;
        return (result);
        }

Alcuni punti che è fondamentale notare nel precedente stralcio di codice sono:

  • Ho utilizzato una TQuery per semplicità perchè penso che il BDE sia ancora il metodo di accesso più diffuso tra gli sviluppatori Borland. Tuttavia ricordo che è consigliabile utilizzare dbExpress o altri metodi di accesso più moderni e per i quali si prevede più continuità del BDE.
  • Nella riga [1] istanzio un oggetto query sullo heap (le classi VCL possono solo essere istanziate sullo heap) e passo NULL (zero) come owner. Ciò significa che la responsabilità della delete è esclusivamente di mia responsabilità.
  • La chiamata q->Close() non è necessaria perchè comunque la delete garantisce la chiamata a Close(). Ho voluto aggiungere la chiamata esplicita solo per ricordare che è importante chiudere una query perchè altrimenti il database server mantiene un cursore aperto.

La soluzione presentata sopra è ideale da un punto di vista di organizzazione del codice (la query è relegata all'interna della funzione), perchè presenta due problemi:

  1. È ripetitiva perchè ogni volta è necessario ripetere lo stesso codice: allocare la query, impostare il database e ricordarsi di rilasciare la risorsa acquisita.
  2. E' fragile perchè in fase di eccezione, o anche se si ritorna prematuramente dalla funzione, la query non viene rilasciata e quindi rischiamo di lasciare un cursore aperto sul server.

Il problema può essere facilmente risolto con una piccola e semplice classe che chiamerò AutoQuery:

   class AutoQuery {
    public:
        inline AutoQuery(): q(new TQuery(0))
            { q->Database = myDatabase; }
        inline ~AutoQuery()
            {
            q->Close();
            delete q;
            }
        inline TQuery* operator->()
            { return q; }
        inline operator TQuery*()
            { return q; }
    private:
        TQuery* q;

    private:
        AutoQuery(const AutoQuery& rhs);
        AutoQuery& operator=(const AutoQuery& rhs);
    };

Prima vediamo come possiamo riscrivere getCustomersCount() sfruttando la nostra classe, ne valutiamo i vantaggi e poi esamineremo la classe AutoQuery.

    int getCustomersCount()
        {
        AutoQuery q;
        q->SQL->Text = "select count(*) from CUSTOMER";
        q->Open();
        return (q->Fields->Fields[0]->AsInteger);
        }

Come possiamo vedere, in questa nuova versione, le nostre originali otto righe si sono ridotte a quattro. Inoltre il codice è diventato più lineare: creiamo una query, ne impostiamo l'SQL, apriamo e restituiamo il risultato. Ma il principale vantaggio di questa funzione è questa: anche a fronte di eccezioni (o dimenticanze) siamo sicuri che la query verrà chiusa e che tutte le risorse (specialmente quelle lato server) verranno rilasciate.

È giunta l'ora di esaminare il funzionamento della classe. Il costrutture è abbastanza semplice e intuitivo:

    inline AutoQuery(): q(new TQuery(0))
        { q->Database = myDatabase; }

Nella lista di inizializzazione istanzia la TQuery impostando l'owner a NULL. In seguito, nel corpo del costruttore, imposta il database (che spesso è definito in un data module).

Il distruttore è banale, ma è anche il metodo più importante perchè garantisce il rilascio della risorsa acquisita. Verrà seguito quando q perde visibilità nel nostro caso quando eseguiamo esplicitamente il return).

    inline ~AutoQuery()
        {
        q->Close();
        delete q;
        }

L'overload dell'operatore -> è fondamentale se vogliamo trattare una AutoQuery come se fosse una normalissima TQuery*:

    inline operator TQuery*()
        { return q; }

Infatti, quando scriviamo q->SQL->Text = "qualcosa" stiamo sfruttando l'overload dell'operatore -> e, dal compilatore, viene tramutato in qualcosa come:

    q->operator->()->SQL->Text = "qualcosa";

permettendoci di fatto di lavorare direttamente sull'oggetto TQuery invece che sulla nostra AutoQuery.

L'overload del conversion operator è conveniente per avere cast automatici qualora necessari. Per esempio, immaginiamo di avere la funzione usa_q() che riceve una TQuery* come parametro e la funzione passa_q() che passa una AutoQuery:

    void usa_q(TQuery* q)
        {
        // fa qualcosa su "q"
        }

    void passa_q()
        {
        AutoQuery q;
        usa_q(q);
        // ... codice omesso per semplicita'
        }

In questo modo, sfruttando l'operatore di conversione, la funzione usa_q() non deve in alcun modo essere modificata per ricevere una AutoQuery ne tantomeno deve essere modificata passa_q() per passare una AutoQuery perchè questa viene convertita automaticamente ad una TQuery* nel momento in cui viene passata ad una funzione che si aspetta una TQuery*.

Per approfondire l'argomento potete trovare in AutoQuery.h una classe templetizzata che presenta una soluzione un po' più adattabile del meccanismo presentato in questo articolo. La classe, essendo templetizzata, può essere adattata a TQuery (BDE), SQLQuery (dbExpress) e TIBQuery (Interbase Express). Potete trovare anche i sorgenti di un Demo che utilizza AutoQuery (compilati con C++ Builder 6) e l'eseguibile dello stesso Demo (anch'esso, compilato con C++ Builder 6, link statico).