Der Weg über sqlite3_prepare und sqlite3_step

C-Code

sqlite3* dbs;
sqlite3_stmt *stmt;
char command[300];
char* ptr_command = command;
int cols;
int i;
char* col_name;
char* col_value;
 
printf("SQL Kommando eingeben:\n");
fgets(ptr_command,300,stdin);
 
if (sqlite3_prepare_v2(dbs,ptr_command,strlen(command),&stmt,0)!=SQLITE_OK)
{
  printf("SQL-Fehler: %s\n",sqlite3_errmsg(dbs));
}
cols = sqlite3_column_count(stmt);
while (sqlite3_step(stmt) == SQLITE_ROW)
{
  for (i=0;i<cols;i++)
  {
     if (sqlite3_column_name(stmt,i))
       col_name =  (char*) sqlite3_column_name(stmt,i);
     else col_name = "LEER";
 
     if (sqlite3_column_text(stmt,i)
       col_value = (char*) sqlite3_column_text(stmt,i);
     else col_value = "LEER";
 
     printf("Inhaltstyp: %i %s: %s\n",sqlite3_column_type(stmt,i),col_name,col_value);
   }
}
sqlite3_finalize(stmt);

C++-Code

sqlite3* dbs;
string command;
sqlite3_stmt *stmt;
string col_name;
string col_value;
int cols;
 
cout << "SQL Kommando eingeben:";
getline(cin,command);
 
if (sqlite3_prepare_v2(dbs,command.c_str(),command.size(),&stmt,0)!=SQLITE_OK)
{
   cerr << "SQL-Fehler: " << sqlite3_errmsg(dbs) << endl;
}
cols = sqlite3_column_count(stmt);
 
while (sqlite3_step(stmt) == SQLITE_ROW)
{
   for (int i=0;i<cols;i++)
   {
     if(sqlite3_column_name(stmt,i))
       col_name = const_cast<const char*>(sqlite3_column_name(stmt,i));
     else col_name = "LEER";
 
     if (sqlite3_column_text(stmt,i))
       col_value = reinterpret_cast<const char*>(sqlite3_column_text(stmt,i));
     else col_value = "LEER";
 
     cout << "Inhaltstyp: " << sqlite3_column_type(stmt,i) << " ";
     cout << col_name << ": ";
     cout << col_value << endl;
    }
}
sqlite3_finalize(stmt);

Erklärung

Gehen wir die benutzten Funktionen doch der Reihe nach durch:

sqlite3_prepare_v2(dbs,ptr_command,strlen(command),&stmt,0

Diese Funktion compiliert ein UTF-8 codiertes SQL-Kommando in einen Bytecode. Dies ist notwendig um es später ausführen zu können. Sie erwartet die folgenden Parameter:

  1. Sqlite3-Pointer auf eine geöffnete Datenbank
  2. char-Pointer auf das erste Zeichen des SQL-Kommandos
  3. Integerwert für die maximale Länge des eingelesenen Kommandos in Byte
  4. Einen sqlite3_stmt** Pointer in dem das compilierte Statement landen soll
  5. Einen char** Pointer der auf einen unbenutzten Teil der eingegeben SQL-Kommandos zeigt.

Was hat es mit dem letzten Pointer auf sich? Im Gegensatz zu sqlite3_exec() und sqlite3_get_table() kann über sqlite3_prepare_v2() immer nur ein SQL-Kommando bearbeitet werden. Wenn mehrere Kommandos durch Semikolon getrennt eingegeben werden, dann zeigt dieser Pointer auf das erste Zeichen der noch unbearbeiteten Kommandos.

sqlite3_errmsg() sollte bereits bekannt sein.

sqlite3_step(stmt)

sqlite3_step() führt das compilierte SQL-Kommando schließlich aus. Handelt es sich um ein Kommando, das kein Ergebnis zurückliefert, dann läuft das Kommando bei einmaliger Ausführung komplett ab. Wird ein Ergebnis zurückgeben, so muss für jede Datenzeile einmal sqlite3_step() aufgerufen werden, bis SQLITE_DONE zurückgeliefert wird. Erst dann ist das compilierte Statement komplett ausgeführt und alle vorhanden Datenbankzeilen ausgelesen. SQLITE_ROW im Codebeispiel bedeutet, dass Step noch ein weiteres Mal aufgerufen werden kann, da noch eine weitere Datenzeile vorhanden ist. Eine ausführliche Beschreibung findest du in der Befehlsreferenz.

sqlite3_finalize(stmt);

sqlite3_finalize() gibt den Speicher frei, der durch sqlite3_prepare_v2 mit dem Statement belegt wurde. Diese Funktion muss aufgerufen werden, damit kein Speicherleck ensteht.

Kommen wir als letztes zu den sqlite3_column_* Funktionen. Mit diesen Funktionen lassen sich die Werte aus einer Tabelle auswerten, solange das sqlite3_stmt existiert. Das heißt, sie müssen unbedingt vor sqlite3_finalize() ausgeführt werden.

sqlite3_column_count(stmt);

sqlite3_column_count() liefert die Anzahl der Spalten als Integer zurück, die im aktuellen Statement enthalten sind.

sqlite3_column_type(stmt,i);

sqlite3_column_type liefert den Datentyp eines Tabellenfeldes als Integer zurück, der im aktuellen Statement enthalten ist. i gibt dabei die Spaltennummer an. Welche Werte für welchen Datentyp stehen, kann man in der Referenz nachlesen.

sqlite3_column_name(stmt,i)

sqlite3_column_name() liefert den Namen einer Tabellenspalte im aktuellen Statement als char-Pointer zurück. i gibt die Spaltennummer an.

sqlite3_column_text(stmt,i)

sqlite3_column_text() ist eine, aus einer ganzen Reihe von Funktionen, die den Vorteil dieser relativ aufwendigen Methode ausmachen. Sie existiert für jeden Datentyp, der in der Datenbank gespeichert werden kann. Man kann dadurch die Werte ohne Typumwandlung aus der Datenbank holen. Die genauen Funktionsnamen sind in der Referenz zu finden. Sie arbeiteten immer im aktuellen Statement und i gibt die Spaltennummer an.

Schlusswort

Diese Möglichkeit erfordert zwar den meisten Code, bietet aber gerade dadurch die Möglichkeit hochspezialisierte Funktionen zu schreiben und auch noch das letzte Quäntchen an Performance herauszukitzeln. Der Aufwand lohnt sich vor allem bei SQL Kommandos, die Daten aus einer Tabelle ausgeben. Wieso ist das so? Die ersten beiden Funktionen haben beide die gleiche Schwäche: Sie liefern das Ergebnis als char-Array zurück, also als Zeichenkette bzw. String. Wenn im Tabellenfeld aber keine Zeichenkette, sondern z. B. ein int-Wert steht, muss man diesen zuerst umwandeln. Wenn die Tabelle vielleicht sogar Binärobjekte(BLOBs) enthält, verbieten sich die anderen beiden Funktionen ohnehin von selbst.

Um noch zusätzliche Performance herauszuholen kann ein prepared_statement über die Funktionen sqlite3_reset() und das sqlite3_bind Interface auch mehrfach verwendet werden, wenn der gleiche Befehl, aber mit anderen Parametern benutzt wird. Beim Ausführen der sqlite3_prepare_v2() Funktion wird das SQL-Kommando in Bytecode übersetzt. Das Ausführen eines Befehls ist aber fast immer schneller als die Neucompilierung. Dadurch kann man zusätzliche Laufzeit sparen. Das mag auf einem modernen Rechner nicht ins Gewicht fallen, kann aber auf einem leistungsschwachen System durchaus einen spürbaren Unterschied machen. Sollte es notwendig werden diese Möglichkeiten voll auszuspielen, so sollte man sich in die umfangreiche Dokumentation (Englisch) auf der Herstellerseite einarbeiten.