SQL, SQLite und ODBC

Für gewöhnlich versuche ich Datenbanken zu meiden. Die meisten brauchen aufwändige Installationen und fressen Ressourcen, obwohl man oft nur ein paar Datenfelder sortiert speichern möchte.

Eine Ausnahme mache ich jedoch gerne. Und zwar für SQLite. Tja und wenn man schon SQL basierte Datenbanken anbinden möchte, dann ist der gute alte ODBC Standard auch nicht fern …


SQLite

SQLite ist eine In-Process-Datenbank. Man braucht also keinen separaten Server, sondern bettet eine Bibliothek ein, die eine Datenbank Datei öffnen und bearbeiten kann.
Genau das macht SQLite zur perfekten Embedded-Datenbank, denn sie ist rein in C geschrieben und kann statisch zum Programm gelinkt werden. Dann hat man seinen eigenen DB Server direkt in der EXE drinnen.

Der Ablauf einer Anfrage sieht in SQLite etwa so aus:

  • sqlite3_open("dbfile.sqlit", &db_handle)
    Öffnet eine Datenbank
  • sqlite3_prepare(db_handle, "SELECT * FROM some_table", sizeof(sql_command), &statement_handle, NULL)
    Allokiert Ressourcen für eine SQL Abfrage oder ein Kommando
  • while(sqlite3_step(statement_handle) != SQLITE_DONE)
    Nun läuft man alle Ergebnis Datensätze durch (falls vorhanden)
    • sqlite3_column_count(statement_handle)
      Gibt die Anzahl der Felder im aktuellen Datensatz zurück. Die Felder werden über einen Index (0 <= index < column_count) adressiert.
    • sqlite3_column_name(statement_handle, index)
      Gibt den Namen des Feldes im aktuellen Datensatz zurück.
    • sqlite3_column_type(statement_handle, index)
      Gibt den primären Datentypen eines Feldes per SQLite-Konstante zurück. (also quasi int64, double, char* oder void*)
    • sqlite3_column_int64(statement_handle, index)
      Gibt den Feldinhalt als 64 bit Integer zurück.
    • sqlite3_column_double(statement_handle, index)
      Gibt den Feldinhalt als 64 bit Fließkommazahl zurück.
    • sqlite3_column_bytes(statement_handle, index)
      Gibt für BLOB und TEXT die Anzahl der Bytes des Feldes zurück.
    • sqlite3_column_blob(statement_handle, index)
      Gibt einen Pointer auf das erst Byte eines Bytefeldes zurück.
    • sqlite3_column_text(statement_handle, index)
      Gibt einen Pointer auf das erste Zeichen in einem Text-String zurück.
  • sqlite3_finalize(statement_handle)
    Gibt alle offenen Ressourcen der Abfrage frei.
  • sqlite3_close(db_handle)
    Schließt die Datenbank.

Viel einfacher geht es wohl kaum aus Schnittstellensicht. Und alles andere wie das Setzen von Option erfolgt per Kommando. Es gibt da ein paar spezielle PRAGMA Anweisungen, wie z.B.: PRAGMA foreign_keys = ON mit der man das Verwalten von Fremdschlüsseln bewusst einschalten kann.
Das erledigt man auch über sqlite3_prepare, sqlite3_step und sqlite3_finalize, denn SQLite deaktiviert beim Start manche Features um die Ausführung zu beschleunigen.
Wie auch immer … Fremdschlüssel sind etwas, was ich trotzdem von jeder Datenbank erwarte.

Ganz besonders cool: Diese API existiert für die meisten Programmiersprachen, sie kann also “universal” genutzt werden, nicht nur in C oder C++.

ODBC

Die Open DataBase Connectivity ist mir seit dem alten Windows 3.1 ein Begriff und war der Versuch alle Datenbanken hinter einer API zu verstecken. Mit unixODBC gibt es auch eine kompatible Implementierung für Linux und andere POSIX Systeme.

Wichtig ist die Konfiguration von Datenquellen und deren Namen im System, denn über einen entsprechenden Connection-String verbindet man sich dann per ODBC als Wrapper zu einer beliebigen Datenbank von SQLite über MS-SQL bis hin zu MySQL usw.

Der API Aufbau sieht hier ähnlich aus, wenn auch etwas komplexer:

  • SQLAllocEnv(&henv)
    Allokiert eine ODBC Umgebung.
  • SQLAllocConnect(henv, &hdbc)
    Allokiert eine ODBC Datenbankverbindung.
  • SQLConnect(hdbc, "my_odbc_name", sizeof("my_odbc_name"), "user", sizeof("user"), "passwd", sizeof("passwd"))
    Verbindet sich zu einer konfigurierten Datenbank.
  • SQLAllocStmt(hdbc, &hstmt)
    Allokiert Ressourcen für ein SQL Statement.
  • SQLPrepare(hstmt, "SELECT * FROM some_table", sizeof(sql_command))
    Bereitet die Ausführung eines SQL Statements vor.
  • SQLExecute(hstmt)
    Startet die Ausführung eines SQL Statements auf der Datenbank.
  • while(SQL_SUCCESS == SQLFetch(hstmt))
    Liest eine Datenzeile aus dem Abfrageergebnis.
    • SQLNumResultCols(hstmt, &col_count)
      Gibt die Anzahl der Datenfelder im aktuellen Datensatz zurück.
    • SQLDescribeCol(hstmt, (index + 1), colname_buffer, colname_buffer_len, &colname_buffer_used, &coltype, &colvalue_len, &colvalue_digits, &colvalue_nullable)
      Liefert Informationen über ein Datenfeld, wie Name, Datentype oder Größe. Der Index beginnt allerdings bei “1”, also 0 < index <= SQLNumResultCols
    • SQLGetData(hstmt, (index + 1), sql_type, buffer, sizeof(buffer), &buffer_used)
      List die Daten eines Feldes in einen generischen Puffer.
  • SQLFreeStmt(stmt, SQL_CLOSE)
    Schließt Ressourcen einer Abfrage.
  • SQLFreeStmt(stmt, SQL_DROP)
    Gibt Ressourcen einer Abfrage final frei.
  • SQLDisconnect(hdbc)
    Schließt eine Datenbank Verbindung.
  • SQLFreeConnect(hdbc)
    Gibt allokierte Ressourcen einer Verbindung final frei.
  • SQLFreeEnv(henv)
    Gibt alle Ressourcen der ODBC Umgebung final frei.

Etwas kompliziert sind die Datentypen, weil es recht viele gibt und manche nicht extakt gleich mit SQLGetData angefordert werden können, wie sie mit SQLDescribeCol beschrieben sind.

Hinweis: In Windows gibt es natürlich immer eine Unicode API parallel zur ANSI API, womit man jeden String als TCHAR übergeben sollte und die Anzahl der Zeichen eben nicht sizeof wie im obigen Beispiel ist.

Fazit

Ab sofort sind SQLite und ODBC im GATE Framework vertreten und werden durch eine DBConnection und eine DBReader Klasse abstrahiert.
Die Connection verwaltet den Start von Abfragen und der Reader übersetzt immer den aktuellen Datensatz in GATE-kompatible Datentypen.

Für Suchanfragen eignet sich SQL natürlich viel besser als irgend welche nativen Datensätze, die binär in Dateien stehen. Und eben deshalb darf diese Schnittstelle im Framework nicht fehlen.

Hmm … dabei bemerke ich natürlich eines: Jetzt fehlt mir eine Art von editierbarer Grid-View im UI Framework.

Tja, so schafft man sich selbst Arbeit.