SQL, SQLite und ODBC
« | 20 Feb 2021 | »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 Datenbanksqlite3_prepare(db_handle, "SELECT * FROM some_table", sizeof(sql_command), &statement_handle, NULL)
Allokiert Ressourcen für eine SQL Abfrage oder ein Kommandowhile(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 quasiint64
,double
,char*
odervoid*
)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”, also0 < 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.