ODBC 101: Ein Enten-Leitfaden zu ODBC
Was ist ODBC?
ODBC, ausgeschrieben Open Database Connectivity, ist ein Standard, mit dem unterschiedliche Programme mit unterschiedlichen Datenbanken sprechen können – natürlich einschließlich DuckDB. Dadurch lassen sich Programme leichter bauen, die mit vielen Datenbanken funktionieren. Das spart Zeit, weil Entwickler keinen eigenen Code für jede Datenbankverbindung schreiben müssen. Stattdessen nutzen sie die standardisierte ODBC-Schnittstelle, was Entwicklungszeit und Kosten senkt und Programme leichter wartbar macht. ODBC kann allerdings langsamer sein als andere Verbindungsmethoden, etwa ein nativer Treiber, weil eine zusätzliche Abstraktionsschicht zwischen Anwendung und Datenbank liegt. Da DuckDB spaltenorientiert und ODBC zeilenorientiert arbeitet, kann es bei der Nutzung von ODBC mit DuckDB außerdem Ineffizienzen geben.
Auf dieser Seite gibt es durchgängig Links zur offiziellen Microsoft-ODBC-Dokumentation, einer guten Quelle, um mehr über ODBC zu lernen.
Allgemeine Konzepte
Handles
Ein Handle ist ein Zeiger auf ein bestimmtes ODBC-Objekt, mit dem Sie mit der Datenbank interagieren. Es gibt mehrere Handle-Typen mit unterschiedlichen Zwecken: das Umgebungshandle, das Verbindungshandle, das Statement-Handle und das Deskriptor-Handle. Handles werden mit SQLAllocHandle alloziert. Die Funktion nimmt den zu allozierenden Handle-Typ und einen Zeiger auf das Handle entgegen; der Treiber erzeugt dann ein neues Handle des angegebenen Typs und gibt es an die Anwendung zurück.
Der DuckDB-ODBC-Treiber kennt die folgenden Handle-Typen.
Umgebung
| Handle-Name | Umgebung |
| Typname | SQL_HANDLE_ENV |
| Beschreibung | Verwaltet die Umgebungseinstellungen für ODBC-Operationen und stellt einen globalen Kontext für den Datenzugriff bereit. |
| Anwendungsfall | ODBC initialisieren, Treiberverhalten steuern, Ressourcen allozieren. |
| Weitere Informationen | Muss beim Start einmal pro Anwendung alloziert und am Ende freigegeben werden. |
Verbindung
| Handle-Name | Verbindung |
| Typname | SQL_HANDLE_DBC |
| Beschreibung | Repräsentiert eine Verbindung zu einer Datenquelle. Dient zum Herstellen, Verwalten und Beenden von Verbindungen. Legt sowohl den Treiber als auch die Datenquelle fest, die innerhalb des Treibers verwendet wird. |
| Anwendungsfall | Verbindung zu einer Datenbank herstellen, Verbindungszustand verwalten. |
| Weitere Informationen | Es können nach Bedarf mehrere Verbindungshandles angelegt werden, sodass gleichzeitige Verbindungen zu mehreren Datenquellen möglich sind. Hinweis: Das Allozieren eines Verbindungshandles stellt noch keine Verbindung her. Das Handle muss zuerst alloziert und anschließend verwendet werden, sobald die Verbindung besteht. |
Statement
| Handle-Name | Statement |
| Typname | SQL_HANDLE_STMT |
| Beschreibung | Behandelt die Ausführung von SQL-Anweisungen sowie die zurückgegebenen Ergebnissets. |
| Anwendungsfall | SQL-Abfragen ausführen, Ergebnissets abrufen, Statement-Optionen verwalten. |
| Weitere Informationen | Für parallele Abfragen können mehrere Handles pro Verbindung alloziert werden. |
Deskriptor
| Handle-Name | Deskriptor |
| Typname | SQL_HANDLE_DESC |
| Beschreibung | Beschreibt die Attribute einer Datenstruktur oder eines Parameters und erlaubt der Anwendung, die Struktur der zu bindenden bzw. abzurufenden Daten festzulegen. |
| Anwendungsfall | Tabellenstrukturen und Ergebnissets beschreiben, Spalten an Anwendungspuffer binden. |
| Weitere Informationen | Wird verwendet, wenn Datenstrukturen explizit definiert werden müssen, etwa beim Parameter-Binding oder beim Abrufen von Ergebnissets. Sie werden automatisch alloziert, wenn ein Statement alloziert wird, können aber auch explizit alloziert werden. |
Verbinden
Der erste Schritt ist die Verbindung zur Datenquelle, damit die Anwendung Datenbankoperationen ausführen kann. Zuerst muss die Anwendung ein Umgebungshandle allozieren und anschließend ein Verbindungshandle. Mit dem Verbindungshandle wird dann die Verbindung zur Datenquelle hergestellt. Es gibt zwei Funktionen, um sich mit einer Datenquelle zu verbinden: SQLDriverConnect und SQLConnect. Erstere verbindet sich über eine Verbindungszeichenfolge, letztere über einen DSN.
Verbindungszeichenfolge
Eine Verbindungszeichenfolge enthält die Informationen, die für die Verbindung zu einer Datenquelle nötig sind. Sie ist als durch Semikolon getrennte Liste von Schlüssel-Wert-Paaren formatiert. DuckDB verwendet derzeit jedoch nur den DSN und ignoriert die übrigen Parameter.
DSN
Ein DSN (Data Source Name) ist eine Zeichenfolge, die eine Datenbank identifiziert. Es kann ein Dateipfad, eine URL oder ein Datenbankname sein. Zum Beispiel sind C:\Users\me\duckdb.db und DuckDB gültige DSNs. Weitere Informationen zu DSNs finden Sie auf der Seite „Choosing a Data Source or Driver“ der SQL-Server-Dokumentation.
Fehlerbehandlung und Diagnose
Alle Funktionen in ODBC geben einen Code zurück, der Erfolg oder Fehlschlag der Funktion darstellt. Das erleichtert die Fehlerbehandlung: Die Anwendung kann den Rückgabecode jedes Funktionsaufrufs prüfen, um festzustellen, ob er erfolgreich war. Bei Misserfolg kann die Anwendung mit SQLGetDiagRec die Fehlerinformationen abrufen. Die folgende Tabelle definiert die Rückgabecodes:
| Rückgabecode | Beschreibung |
|---|---|
SQL_SUCCESS |
Die Funktion wurde erfolgreich abgeschlossen. |
SQL_SUCCESS_WITH_INFO |
Die Funktion wurde erfolgreich abgeschlossen, es sind aber zusätzliche Informationen verfügbar, einschließlich einer Warnung. |
SQL_ERROR |
Die Funktion ist fehlgeschlagen. |
SQL_INVALID_HANDLE |
Das übergebene Handle war ungültig. Das deutet auf einen Programmierfehler hin, etwa wenn ein Handle verwendet wird, bevor es alloziert wurde, oder es den falschen Typ hat. |
SQL_NO_DATA |
Die Funktion wurde erfolgreich abgeschlossen, es sind aber keine weiteren Daten verfügbar. |
SQL_NEED_DATA |
Es werden weitere Daten benötigt, etwa wenn Parameterdaten zur Ausführungszeit gesendet werden oder zusätzliche Verbindungsinformationen erforderlich sind. |
SQL_STILL_EXECUTING |
Eine asynchron ausgeführte Funktion läuft noch. |
Puffer und Binding
Ein Puffer ist ein Speicherblock zum Ablegen von Daten. Puffer speichern Daten, die aus der Datenbank abgerufen oder an die Datenbank gesendet werden. Die Anwendung alloziert die Puffer und bindet sie mit SQLBindCol und SQLBindParameter an eine Spalte in einem Ergebnisset oder an einen Parameter in einer Abfrage. Wenn die Anwendung eine Zeile aus dem Ergebnisset holt oder eine Abfrage ausführt, werden die Daten im Puffer gespeichert. Wenn die Anwendung eine Abfrage an die Datenbank sendet, werden die Daten im Puffer an die Datenbank übergeben.
Eine Anwendung einrichten
Im Folgenden finden Sie eine schrittweise Anleitung, um eine Anwendung aufzusetzen, die über ODBC eine Datenbank verbindet, eine Abfrage ausführt und die Ergebnisse in C++ abruft.
Um den Treiber und alles Weitere zu installieren, folgen Sie diesen Anweisungen.
1. SQL-Header-Dateien einbinden
Der erste Schritt ist das Einbinden der SQL-Header-Dateien:
#include <sql.h>#include <sqlext.h>Diese Dateien enthalten die Definitionen der ODBC-Funktionen sowie die von ODBC verwendeten Datentypen. Um diese Header-Dateien zu nutzen, muss das Paket unixodbc installiert sein:
Unter macOS:
brew install unixodbcUnter Ubuntu und Debian:
sudo apt-get install -y unixodbc-devUnter Fedora, CentOS und Red Hat:
sudo yum install -y unixODBC-develVergessen Sie nicht, den Speicherort der Header-Dateien in Ihren CFLAGS anzugeben.
Für MAKEFILE:
CFLAGS=-I/usr/local/include# orCFLAGS=-I/opt/homebrew/Cellar/unixodbc/2.3.11/includeFür CMAKE:
include_directories(/usr/local/include)# orinclude_directories(/opt/homebrew/Cellar/unixodbc/2.3.11/include)Sie müssen die Bibliothek außerdem in Ihrem CMAKE oder MAKEFILE linken.
Für CMAKE:
target_link_libraries(ODBC_application /path/to/duckdb_odbc/libduckdb_odbc.dylib)Für MAKEFILE:
LDLIBS=-L/path/to/duckdb_odbc/libduckdb_odbc.dylib2. ODBC-Handles definieren und mit der Datenbank verbinden
2.a. Verbinden mit SQLConnect
Richten Sie anschließend die ODBC-Handles ein, allozieren Sie sie und verbinden Sie sich mit der Datenbank. Zuerst wird das Umgebungshandle alloziert, dann die Umgebung auf ODBC-Version 3 gesetzt, danach das Verbindungshandle alloziert und schließlich die Verbindung zur Datenbank hergestellt. Der folgende Codeausschnitt zeigt, wie das geht:
SQLHANDLE env;SQLHANDLE dbc;
SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0);
SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
std::string dsn = "DSN=duckdbmemory";SQLConnect(dbc, (SQLCHAR*)dsn.c_str(), SQL_NTS, NULL, 0, NULL, 0);
std::cout << "Connected!" << std::endl;2.b. Verbinden mit SQLDriverConnect
Alternativ können Sie sich mit SQLDriverConnect mit dem ODBC-Treiber verbinden.
SQLDriverConnect akzeptiert eine Verbindungszeichenfolge, in der Sie die Datenbank mit jeder der verfügbaren DuckDB-Konfigurationsoptionen einrichten können.
SQLHANDLE env;SQLHANDLE dbc;
SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0);
SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
SQLCHAR str[1024];SQLSMALLINT strl;std::string dsn = "DSN=DuckDB;access_mode=READ_ONLY"SQLDriverConnect(dbc, nullptr, (SQLCHAR*)dsn.c_str(), SQL_NTS, str, sizeof(str), &strl, SQL_DRIVER_COMPLETE)
std::cout << "Connected!" << std::endl;3. Eine Abfrage hinzufügen
Nachdem die Anwendung eingerichtet ist, können wir eine Abfrage hinzufügen. Zuerst müssen wir ein Statement-Handle allozieren:
SQLHANDLE stmt;SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt);Anschließend können wir eine Abfrage ausführen:
SQLExecDirect(stmt, (SQLCHAR*)"SELECT * FROM integers", SQL_NTS);4. Ergebnisse abrufen
Nachdem wir eine Abfrage ausgeführt haben, können wir die Ergebnisse abrufen. Zuerst müssen wir die Spalten im Ergebnisset an Puffer binden:
SQLLEN int_val;SQLLEN null_val;SQLBindCol(stmt, 1, SQL_C_SLONG, &int_val, 0, &null_val);Anschließend können wir die Ergebnisse holen:
SQLFetch(stmt);5. Ergebnisse verarbeiten
Mit den Ergebnissen können wir nun tun, was wir möchten. Zum Beispiel können wir sie ausgeben:
std::cout << "Value: " << int_val << std::endl;Sie können auch weitere Abfragen ausführen und andere Datenbankoperationen vornehmen, etwa Daten einfügen, aktualisieren oder löschen.
6. Handles freigeben und Verbindung trennen
Zum Schluss müssen wir die Handles freigeben und die Verbindung zur Datenbank trennen. Zuerst das Statement-Handle:
SQLFreeHandle(SQL_HANDLE_STMT, stmt);Dann die Verbindung zur Datenbank trennen:
SQLDisconnect(dbc);Und schließlich das Verbindungshandle und das Umgebungshandle freigeben:
SQLFreeHandle(SQL_HANDLE_DBC, dbc);SQLFreeHandle(SQL_HANDLE_ENV, env);Das Freigeben der Verbindungs- und Umgebungshandles ist erst möglich, nachdem die Verbindung zur Datenbank geschlossen wurde. Ein Versuch, sie vor dem Trennen freizugeben, führt zu einem Fehler.
Beispielanwendung
Im Folgenden finden Sie eine Beispielanwendung: eine cpp-Datei, die sich mit der Datenbank verbindet, eine Abfrage ausführt, die Ergebnisse holt und ausgibt. Sie trennt außerdem die Verbindung, gibt die Handles frei und enthält eine Funktion zur Prüfung der Rückgabewerte von ODBC-Funktionen. Dazu gehört eine Datei CMakeLists.txt, mit der sich die Anwendung bauen lässt.
Beispiel-.cpp-Datei
#include <iostream>#include <sql.h>#include <sqlext.h>
void check_ret(SQLRETURN ret, std::string msg) { if (ret != SQL_SUCCESS && ret != SQL_SUCCESS_WITH_INFO) { std::cout << ret << ": " << msg << " failed" << std::endl; exit(1); } if (ret == SQL_SUCCESS_WITH_INFO) { std::cout << ret << ": " << msg << " succeeded with info" << std::endl; }}
int main() { SQLHANDLE env; SQLHANDLE dbc; SQLRETURN ret;
ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env); check_ret(ret, "SQLAllocHandle(env)");
ret = SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); check_ret(ret, "SQLSetEnvAttr");
ret = SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc); check_ret(ret, "SQLAllocHandle(dbc)");
std::string dsn = "DSN=duckdbmemory"; ret = SQLConnect(dbc, (SQLCHAR*)dsn.c_str(), SQL_NTS, NULL, 0, NULL, 0); check_ret(ret, "SQLConnect");
std::cout << "Connected!" << std::endl;
SQLHANDLE stmt; ret = SQLAllocHandle(SQL_HANDLE_STMT, dbc, &stmt); check_ret(ret, "SQLAllocHandle(stmt)");
ret = SQLExecDirect(stmt, (SQLCHAR*)"SELECT * FROM integers", SQL_NTS); check_ret(ret, "SQLExecDirect(SELECT * FROM integers)");
SQLLEN int_val; SQLLEN null_val; ret = SQLBindCol(stmt, 1, SQL_C_SLONG, &int_val, 0, &null_val); check_ret(ret, "SQLBindCol");
ret = SQLFetch(stmt); check_ret(ret, "SQLFetch");
std::cout << "Value: " << int_val << std::endl;
ret = SQLFreeHandle(SQL_HANDLE_STMT, stmt); check_ret(ret, "SQLFreeHandle(stmt)");
ret = SQLDisconnect(dbc); check_ret(ret, "SQLDisconnect");
ret = SQLFreeHandle(SQL_HANDLE_DBC, dbc); check_ret(ret, "SQLFreeHandle(dbc)");
ret = SQLFreeHandle(SQL_HANDLE_ENV, env); check_ret(ret, "SQLFreeHandle(env)");}Beispiel-CMakeLists.txt-Datei
cmake_minimum_required(VERSION 3.25)project(ODBC_Tester_App)
set(CMAKE_CXX_STANDARD 17)include_directories(/opt/homebrew/Cellar/unixodbc/2.3.11/include)
add_executable(ODBC_Tester_App main.cpp)target_link_libraries(ODBC_Tester_App /duckdb_odbc/libduckdb_odbc.dylib)