Il presente articolo è stato tradotto automaticamente.

Windows con C++

Utilizzo di database in Windows Azure

Kenny Kerr

Kenny KerrMicrosoft ha una lunga storia di confondere gli sviluppatori con una vertiginosa serie di tecnologie di accesso ai dati. Ci fu un tempo in cui sembrava come se ogni release di Windows, SQL Server o Visual Studio ha inaugurato un nuovo accesso dati API. Da qualche parte lungo la linea — intorno 1996, credo — Microsoft applicato il suo consueto entusiasmo per spostare gli sviluppatori da ODBC, OLE DB.

ODBC, acronimo di Open Database Connectivity, era il vecchio standard per l'accesso ai sistemi di gestione di database. OLE DB sta per... aspetta... Oggetto collegamento e incorporamento di Database ed era l'utopia di accesso universale e nuovi dati. Ma questo nome è completamente fuorviante, che tratterò in un attimo.

Ricordo ancora leggendo la colonna di Don Box nel numero di luglio 1999 di MSDN Magazine (poi chiamato Microsoft Systems Journal) dove ha descritto la motivazione e l'ispirazione che porta a OLE DB. Mi ricordo di pensare al tempo che questo era molto più complicato di ODBC, ma che certo era molto più estensibile. Il motivo che del nome OLE DB è così fuorviante è che non ha nulla a che fare con OLE e non è specificamente per i database. Veramente è stato progettato come un modello di accesso dati universale per tutti i dati — relazionale o altrimenti — come testo, XML, database, motori di ricerca, è il nome. OLE DB ha debuttato quando COM era la rabbia sulla piattaforma Windows, così sua API COM-pesante e naturale estensibilità ha fatto appello a molti sviluppatori.

Ancora, come un database relazionale, API, esso ha mai raggiunto la prestazione di ODBC. Tecnologie di accesso ai dati successivi, come quelli di Microsoft .NET Framework, abbandonato tutti, ma la funzionalità di accesso database così il sogno di accesso ai dati universale ha cominciato a esaurirsi. Poi nell'agosto 2011 il team SQL Server , i più grandi fautori di OLE DB, ha fatto l'annuncio mozzafiato che "Microsoft è allineare con ODBC per l'accesso ai dati relazionali nativo" (bit.ly/1dsYgTD). Hanno dichiarato che il mercato si muoveva lontano da OLE DB e ODBC verso la. Così si torna a ODBC come voi e capire come accedere ai SQL Server per la prossima generazione di applicazioni C++ native.

La buona notizia è che ODBC è relativamente semplice. È anche estremamente veloce. È stato spesso sostenuto che OLE DB sovraperformato ODBC, ma questo è raramente il caso. La cattiva notizia è che ODBC è un'API C-stile vecchia che pochi sviluppatori ricordi — o mai imparato — come utilizzare. Fortunatamente, C++ moderno viene in soccorso e può fare programmazione ODBC una brezza. Se si desidera accedere ai database in Windows Azure con C++, quindi è necessario abbracciare ODBC. Osserviamoli in dettaglio.

Come tante altre API C-style, ODBC è modellato intorno a una serie di maniglie che rappresentano oggetti. Così io uso ancora il mio fidato di classe unique_handle che ho scritto e utilizzato in numerose colonne. È possibile ottenere una copia del handle.h da dx.codeplex.com e seguire insieme. Maniglie ODBC sono, tuttavia, un po' stupido. L'API ODBC deve essere detto il tipo di ogni maniglia come viene utilizzato, sia nella creazione e liberando un manico (e relativo oggetto sottostante).

Un tipo di handle è espressa con un SQLSMALLINT, che è solo un valore short int. Invece di definire una classe di tratti unique_handle per ogni tipo di oggetto che ODBC definisce, ho intenzione di rendere i tratti un template di classe stessa. Figura 1 illustrato che cosa questo potrebbe assomigliare.

Figura 1 una classe di tratti ODBC

template <SQLSMALLINT T>
struct sql_traits
{
  using pointer = SQLHANDLE;
  static auto invalid() noexcept -> pointer
  {
    return nullptr;
  }
  static auto close(pointer value) noexcept -> void
  {
    VERIFY_(SQL_SUCCESS, SQLFreeHandle(T, value));
  }
};

La classe di tratti metodo close in Figura 1 è dove si può cominciare a vedere come dovete comunicare ODBC il tipo di ogni maniglia quando utilizzato con alcune funzioni ODBC generiche. Perché sto usando l'ultima build di anteprima del compilatore Visual C++ (il novembre 2013 CTP a partire da questa scrittura) sono in grado di sostituire la specifica eccezione throw obsoleto con l'identificatore noexcept, aprendo la porta per il compilatore di generare codice più ottimo in alcuni casi. Purtroppo, anche se questo compilatore fornisce anche la possibilità di dedurre il tipo restituito per funzioni di auto, include un bug che impedisce di farlo per le funzioni membro di modelli di classe. Naturalmente, poiché la classe di tratti è esso stesso un modello di classe, un alias di modello è utile:

template <SQLSMALLINT T>
using sql_handle = unique_handle<sql_traits<T>>;

Ora posso definire alias tipo per i vari oggetti ODBC, ad esempio gli oggetti dell'ambiente e istruzione:

using environment = sql_handle<SQL_HANDLE_ENV>;
using statement = sql_handle<SQL_HANDLE_STMT>;

Sarò anche definire uno per le connessioni, anche se io uso un nome più specifico:

using connection_handle = sql_handle<SQL_HANDLE_DBC>;

La ragione di questo è che le connessioni richiedono un po' più di lavoro per ripulire in tutti i casi. Mentre gli oggetti ambiente e istruzione non bisogno molto più di questo, oggetti connessione davvero bisogno di una classe di connessione a trattare in modo affidabile con connettività. Prima posso occuparmene, è necessario creare un ambiente.

La funzione SQLAllocHandle generica crea oggetti vari. Qui, ancora una volta, si vede la separazione dell'oggetto — o almeno la maniglia — e il relativo tipo. Anziché duplicare questo codice in tutto il luogo, utilizzerò nuovamente un template di funzione per riportare le informazioni di tipo insieme. Ecco un modello di funzione per la funzione ODBC SQLAllocHandle generico:

template <typename T>
auto sql_allocate_handle(SQLSMALLINT const type,
                         SQLHANDLE input)
{
  auto h = T {};
  auto const r = SQLAllocHandle(type,
                                input,
                                h.get_address_of());
  // TODO: check result here ...
return h;
}

Naturalmente, questo è ancora solo come generico come la funzione ODBC, ma espone la genericità nella maniera C++-amichevole. Potrai tornare alla gestione degli errori in un attimo. Perché questo modello di funzione verrà allocare un handle di un determinato tipo e restituire un wrapper di maniglia, semplicemente posso utilizzare uno degli alias tipo definito in precedenza. Per un ambiente, potrei fare questo:

auto e = sql_allocate_handle<environment>(SQL_HANDLE_ENV, nullptr);

La maniglia di ingresso o handle del padre, il secondo parametro, fornisce un handle padre opzionale per alcuni contenimento logico. Un ambiente non ha un padre ma invece agisce come il genitore per gli oggetti di connessione. Purtroppo, ci vuole un po' più di sforzo per creare un ambiente. ODBC richiede che esso dire quale versione di ODBC che mi aspettavo. Farlo impostando un attributo di ambiente con la funzione SQLSetEnvAttr. Ecco che cosa questo potrebbe apparire come quando avvolto in una funzione di supporto pratico:

auto create_environment()
{
  auto e = 
    sql_allocate_handle<environment>(SQL_HANDLE_ENV, nullptr);
  auto const r = SQLSetEnvAttr(e.get(),
    SQL_ATTR_ODBC_VERSION,
    reinterpret_cast<SQLPOINTER>(SQL_OV_ODBC3_80),
    SQL_INTEGER);
  // TODO: check result here ...
return e;
}

A questo punto sono pronto per creare una connessione che, fortunatamente, è abbastanza semplice:

auto create_connection(environment const & e)
{
  return sql_allocate_handle<connection_handle>(
    SQL_HANDLE_DBC, e.get());
}

Collegamenti vengono creati nel contesto di un ambiente. Qui potete vedere che uso l'ambiente come il padre della connessione. Ho ancora bisogno di fare effettivamente una connessione, e cioè il lavoro della funzione SQLDriverConnect, alcuni di cui parametri possono essere ignorati:

auto connect(connection_handle const & c,
             wchar_t const * connection_string)
{
  auto const r = SQLDriverConnect(c.get(), nullptr,
    const_cast<wchar_t *>(connection_string),
    SQL_NTS, nullptr, 0, nullptr,
    SQL_DRIVER_NOPROMPT);
  // TODO: check result here ...
}

In particolare, la costante SQL_NTS dice solo la funzione che precedente stringa di connessione è con terminata null. Si potrebbe, invece, optare per fornire in modo esplicito la lunghezza. La costante SQL_DRIVER_NOPROMPT finale indica se richiedere all'utente se sono richiesto ulteriori informazioni per stabilire una connessione. In questo caso, sto dicendo "no" alle richieste.

Ma, come ho accennato in precedenza, con grazia chiusura della connessione è un po' più coinvolto. Il guaio è che mentre la funzione SQLFreeHandle viene utilizzata per liberare l'handle di connessione, presuppone che la connessione è chiusa e non chiuderà automaticamente una connessione aperta.

Quello che mi serve è una classe di connessione che rileva la connettività della connessione. Qualcosa di simile a quanto segue:

class connection
{
  connection_handle m_handle;
  bool m_connected { false };
public:
  connection(environment const & e) :
    m_handle { create_connection(e) }
  {}
  connection(connection &&) = default;
  // ...
};

Ora posso aggiungere un metodo connect alla mia classe utilizzando la funzione precedentemente definiti terzi Connetti e aggiornare di conseguenza lo stato di connessione:

auto connect(wchar_t const * connection_string)
{
  ASSERT(!m_connected);
  ::connect(m_handle, connection_string);
  m_connected = true;
}

Il metodo connect asserisce la connessione non è aperta per cominciare e tiene traccia del fatto che la connessione è aperta alla fine. Distruttore della classe di connessione quindi può scollegare automaticamente come necessario:

~connection()
{
  if (m_connected)
  {
    VERIFY_(SQL_SUCCESS, SQLDisconnect(m_handle.get()));
  }
}

In questo modo che la connessione è scollegata prima il distruttore di maniglia membro chiamato per liberare l'handle di connessione stessa. Ora posso creare un'ambiente ODBC e stabilire una connessione correttamente e in modo efficiente con poche righe di codice:

auto main()
{
  auto e = create_environment();
  auto c = connection { e };
  c.connect(L"Driver=SQL Server Native Client 11.0;Server=...");
}

Riguardo le dichiarazioni? Il modello di funzione sql_allocate_handle ancora una volta è utile, e io aggiungo solo un altro metodo per la mia classe di connessione:

auto create_statement()
{
  return sql_allocate_handle<statement>(SQL_HANDLE_STMT,
                                        m_handle.get());
}

Le dichiarazioni vengono create nel contesto di una connessione. Qui si può vedere come la connessione è il padre per l'oggetto della dichiarazione. Indietro nella mia funzione principale, posso creare un oggetto statement molto semplicemente:

auto s = c.create_statement();

ODBC fornisce una funzione relativamente semplice per l'esecuzione di istruzioni SQL, ma verrà nuovamente avvolgerlo per comodità:

auto execute(statement const & s,
             wchar_t const * text)
{
  auto const r = SQLExecDirect(s.get(),
                               const_cast<wchar_t *>(text),
                               SQL_NTS);
  // TODO: check result here ...
}

ODBC è un'API C-stile estremamente vecchia e quindi non utilizzare const, anche in modo condizionale per i compilatori C++. Qui, ho bisogno di gettare via il "const-ness" quindi il chiamante è schermato da questa ignoranza-const. Indietro nella mia funzione principale, posso eseguire istruzioni SQL molto semplicemente:

execute(s, L"create table Hens ( ...
)");

Ma cosa succede se io eseguire un'istruzione SQL che restituisce un set di risultati? Cosa succede se io eseguire qualcosa come questo:

execute(s, L"select Name from Hens where Id = 123");

In tal caso, l'istruzione diventa effettivamente un cursore e ho bisogno di recuperare i risultati, se qualsiasi, uno alla volta. Questo è il ruolo della funzione SQLFetch. Potrei voler sapere se esiste una gallina con l'Id specificato:

if (SQL_SUCCESS == SQLFetch(s.get()))
{
  // ...
}

D'altra parte, potrei eseguire un'istruzione SQL che restituisce righe multiple:

execute(s, L"select Id, Name from Hens order by Id desc");

In tal caso, posso semplicemente chiamare la funzione SQLFetch in un ciclo:

while (SQL_SUCCESS == SQLFetch(s.get()))
{
  // ...
}

Ottenere i valori di colonna individuale è Qual è la funzione SQLGetData per. Questa è un'altra funzione generica, e dovete descrivere precisamente le informazioni che ci si aspetta così come il tampone dove te l'aspetti per copiare il valore risultante. Recupero di un valore di dimensione fissa è relativamente semplice. Figura 2 dimostra una semplice funzione per recuperare un valore di int SQL.

Figura 2 il recupero di un valore Integer SQL

auto get_int(statement const & s,
             short const column)
{
  auto value = int {};
  auto const r = SQLGetData(s.get(),
                            column,
                            SQL_C_SLONG,
                            &value,
                            0,
                            nullptr);
  // TODO: check result here ...
return value;
}

Il primo parametro in SQLGetData è l'handle di istruzione, il secondo è l'indice di colonna base uno, il terzo è il tipo ODBC per un int SQL e la quarta è l'indirizzo del buffer che riceverà il valore. Il secondo ultimo parametro viene ignorato perché questo è un tipo di dati di dimensioni fisse. Per altri tipi di dati, questo indicherebbe la dimensione del buffer in ingresso. Il parametro finale fornisce la lunghezza effettiva o la dimensione dei dati copiati nel buffer. Ancora una volta, questo non viene utilizzato per i tipi di dati di dimensione fissa, ma questo parametro può anche essere usato per restituire informazioni sullo stato come se il valore è null. Recupero di un valore di stringa è solo leggermente più complicato. Figura 3 Mostra un modello di classe che consente di copiare il valore in una matrice locale.

Figura 3 recuperando un SQL valore stringa

template <unsigned Count>
auto get_string(statement const & s,
                short const column,
                wchar_t (&value)[Count])
{
  auto const r = SQLGetData(s.get(),
                            column,
                            SQL_C_WCHAR,
                            value,
                            Count * sizeof(wchar_t),
                            nullptr);
  sql_check(r, SQL_HANDLE_STMT, s.get());
}

Si noti come in questo caso è necessario raccontare la funzione SQLGetData la dimensione del buffer per ricevere il valore effettiva, e devo farlo in byte, non caratteri. Se una query per il nome di un particolare hen e la colonna nome contiene un massimo di 100 caratteri, potrei utilizzare la funzione di get_string, come segue:

if (SQL_SUCCESS == SQLFetch(s.get()))
{
  wchar_t name[101];
  get_string(s, 1, name);
  TRACE(L"Hen’s name is %s\n", name);
}

Infine, mentre io posso riutilizzare un oggetto di connessione per eseguire più istruzioni, una volta che l'oggetto dell'istruzione rappresenta un cursore, ho bisogno per essere sicuri di chiudere il cursore prima di eseguire qualsiasi sub­sequent dichiarazioni:

VERIFY_(SQL_SUCCESS, SQLCloseCursor(s.get()));

Ironicamente, questo non è un problema di gestione della risorsa. A differenza delle sfide con connessioni aperte, la funzione SQLFreeHandle non gli importa se l'istruzione ha un cursore aperto.

Ho evitato di parlare di gestione degli errori fino ad ora perché è un argomento complesso a sé stante. Funzioni ODBC restituiscono codici di errore, ed è vostra responsabilità controllare il valore di questi codici restituiti per determinare se l'operazione è riuscita. Solitamente le funzioni restituiranno la costante SQL_SUCCESS che indica successo, ma possono anche restituire la costante SQL_SUCCESS_WITH_INFO. Quest'ultimo è altrettanto successo ma implica ulteriori informazioni diagnostiche sono disponibile se si desidera recuperare. In genere, solo nelle build di debug recuperare le informazioni di diagnostica quando la costante SQL_SUCCESS_WITH_INFO è tornata. In questo modo posso raccogliere quante più informazioni possibili in sviluppo e non sprecare cicli di produzione. Naturalmente, sempre raccogliere queste informazioni quando effettivamente viene restituito un errore. Indipendentemente dalla causa, il processo mediante il quale viene recuperate le informazioni diagnostiche è lo stesso.

ODBC fornisce informazioni diagnostiche come set di risultati ed è possibile recuperare le righe una alla volta con la funzione di SQLGetDiagRec e un indice di riga di base uno. Basta fare in modo di chiamarlo con l'handle dell'oggetto che ha prodotto il codice di errore in questione.

Ci sono tre principali bit di informazione in ogni riga: un codice di errore nativo specifico per l'origine dati ODBC o driver; un codice di cinque caratteri, breve e criptico, stato che definisce la classe di errore a cui questo record potrebbe riferirsi; e una descrizione testuale più del record diagnostici. Dato il buffer necessario, posso semplicemente chiamare la funzione SQLGetDiagRec in un ciclo per recuperarle tutte, come mostrato Figura 4.

Figura 4 il recupero delle informazioni di errore diagnostico

auto native_error = long {};
wchar_t state[6];
wchar_t message[1024];
auto record = short {};
while (SQL_SUCCESS == SQLGetDiagRec(type,
                                    handle,
                                    ++record,
                                    state,
                                    &native_error,
                                    message,
                                    _countof(message),
                                    nullptr))
{
  // ...
}

Windows Azure insieme a SQL Server fornisce un modo incredibilmente semplice per iniziare a utilizzare database ospitati. Questo è particolarmente avvincente come il motore di database SQL Server è lo stesso uno sviluppatori C++ hanno conosciuto e utilizzato per anni. Mentre è stato scartato OLE DB, ODBC è più che all'altezza del compito e, infatti, è più semplice e più veloce di quanto sia mai stato OLE DB. Naturalmente, ci vuole un po' di aiuto da C++ per rendere il tutto si animano in modo coerente.

Scopri il mio corso Pluralsight, "10 tecniche pratiche per alimentare il vostro Visual C++ Apps" (bit.ly/1fgTifi), per ulteriori informazioni sull'utilizzo di Visual C++ per i database di access su Windows Azure. Fornire istruzioni dettagliate per configurare e utilizzare server di database e associano le colonne per semplificare il processo di recupero delle righe di dati, esempi semplificare e modernizzare il processo di gestione degli errori e molto altro ancora.

Kenny Kerr è un programmatore di computer basato in Canada, così come un autore per Pluralsight e MVP Microsoft. Ha Blog a kennykerr.ca e si può seguirlo su Twitter a twitter.com/kennykerr.