L'accès à vos données avec ODBC en C Date de publication : 07 août 2008 Par Jessee Edouard (Accueil) Le but de ce tutoriel est de vous initier à la programmation d'applications frontales de base de données en langage C en utilisant la technologie ODBC. La maîtrise du langage C, du langage SQL et d'au moins un SGBD (Access, MySQL, Oracle, ...) est donc évidemment requise. Aucun rappel ne sera effectué sur ces trois points. Commentez cet article : 1 commentaire · Version PDF Version hors-ligne I. Introduction II. Notions fondamentales II-A. Les pilotes ODBC II-B. Les sources de données ODBC II-C. Le gestionnaire de pilotes ODBC II-D. Les versions III. Créer une source de données sous Windows IV. Programmation IV-A. La base de données IV-B. Création d'une simple application IV-C. Amélioration IV-D. Une autre méthode : attacher les colonnes V. Analyse des résultats VI. Les diagnostics VII. Se connecter de différentes manières VII-A. La fonction SQLDriverConnect VII-B. Les paramètres VII-C. La chaîne de connexion VIII. Connaître la structure d'une base de données VIII-A. La fonction SQLTables VIII-B. La fonction SQLColumns IX. Conclusion I. Introduction ODBC (Open DataBase Connectivity) est une interface indépendante de tout système d'exploitation (elle est portable) et de tout langage de programmation (elle est ouverte) permettant de se connecter à n'importe quel serveur de base de données (même Access ou Excel, etc.) et ce de manière standard. Fondamentalement, pour accéder à une base de données, le programmeur est censé utiliser les APIs du SGBD utilisé (ou tout au moins un wrapper). Cela présente un inconvénient majeur : elle rend les programmes dépendant du SGBD. Par contre, c'est la seule solution lorsqu'on souhaite accéder à toutes les fonctionnalités du logiciel, mais cela est loin de nos objectifs. ODBC est une interface qui cache les détails de chaque SGBD. Pour être un peu plus exact, ODBC est une spécification d'une API de SGBD, elle-même basée sur deux autres spécifications connues sous le nom de X/Open et ISO CLI, formellement X/Open CAE "SQL Call-Level Interface" et ISO/IEC 90753:1995 (E) "Call-Level Interface". Dans ce tutoriel, nous allons découvrir comment utiliser ODBC sous Windows en langage C mais, comme nous l'avons déjà dit tout à l'heure, ODBC existe sur de nombreux systèmes incluant Windows, MacOS et UNIX, ce qui implique que ce tutoriel vous est quand même aussi destiné même si vous n'êtes pas sous Windows. II. Notions fondamentales II-A. Les pilotes ODBC ODBC fonctionne grâce à des pilotes, appelés justement pilotes ODBC, qui convertissent les appels ODBC en appels natifs pour le SGBD. Pour accéder à une base de données en utilisant ODBC, il faut donc qu'un pilote adéquat soit installé. Ce pilote est généralement téléchargeable sur le site du constructeur s'il n'est pas déjà installé. Si vous êtes sous Windows, vous n'avez pas besoin d'installer les pilotes pour Oracle, Access, dBase, Excel, SQL Server et bien d'autres encore puisqu'ils ont déjà été installés en même temps que Windows, à moins que vous vouliez tout simplement héberger plusieurs pilotes sur votre ordinateur ou pour n'importe quelle autre raison. II-B. Les sources de données ODBC Une source de données ODBC est en quelque sorte un lien permettant à une application utilisant ODBC de se connecter à la base de données décrite dans cette source de données. Une source de données représente donc une base de données ainsi que le pilote qu'il faut utiliser pour s'y connecter. La création d'une source de données se fait en quelques clics (ou en ligne de commandes ...) en utilisant un utilitaire généralement fourni par le constructeur (normalement fourni en même temps que le pilote). Les paramètres à spécifier lors de la création d'une source de données varient selon le SGBD. Il s'agit de paramètres comme l'emplacement du serveur, le port sur lequel il faut se connecter, le nom de la base, etc. mais aussi et surtout le nom de la source de données appelée aussi DSN pour Data Source Name. C'est ce nom qu'il faudra spécifier en argument de certaines fonctions comme SQLConnect qui permet de se connecter à une base de données par exemple. II-C. Le gestionnaire de pilotes ODBC Les applications qui utilisent ODBC ne se lient pas directement à un pilote ODBC mais au gestionnaire de pilotes ODBC. Le gestionnaire de pilotes ODBC fournit des services (l'API ODBC) aux applications en s'appuyant en grande partie sur des services fournis par les pilotes ODBC. C'est le pilote qui communique réellement au SGBD (c'est-à-dire qui implémente les appels) et fournit les résultats, s'il y en a, au gestionnaire, qui a son tour les transmettra à l'application. Sous Windows, les applications utilisent odbc32.dll, le Gestionnaire de pilotes ODBC de Microsoft. Sous les autres plateformes, on a unixODBC, le standard industriel sous UNIX et iODBC (Independant ODBC), également disponible sous UNIX mais plus populaire sous MacOS. Tous deux sont des projets Open Source. En particulier, unixODBC est livré avec la plupart des distributions Linux et UNIX tandis que iODBC est installé sous MacOS X depuis la version 10.2. II-D. Les versions L'API ODBC n'est pas restée la même depuis la version 1.0. Chaque nouvelle version a amené des améliorations et de nouvelles fonctions par rapport à la version précédente. La majorité des gestionnaires de pilotes ODBC sont actuellement conformes à la version 3.0 mais nombreux sont ceux qui supportent également une version 3.5x (un sur ensemble de la version 3.0). Une des plus améliorations les plus importantes apportées par cette version par rapport à la précédente est le support d'UNICODE. Dans ce tutoriel, nous-nous limiterons cependant à la version 3.0, ainsi nos programmes pourront fonctionner aussi bien avec un driver 3.0 qu'un driver 3.5x. III. Créer une source de données sous Windows Windows permet d'administrer toutes les sources de données au sein d'une même interface qui est l'Administrateur de sources de données ODBC. En outre, il permet également de voir la liste de tous les pilotes installés. Une manière de l'ouvrir est d'aller dans Panneau de configuration > Outils d'administration > Sources de données (ODBC), ou tout simplement en exécutant directement le fichier odbcad32.exe. Pour ajouter une nouvelle source de données, il suffit de cliquer sur Ajouter, de sélectionner le pilote à utiliser (ce qui revient à selectionner le SGBD utilisé) puis de se laisser guider par l'assistant. Sachez également que Windows distingue les sources de données utilisateurs visibles uniquement pour l'utilisateur courant, les sources de données systèmes visibles pour tous les utilisateurs et les sources de données fichiers qui sont représentées par un fichier. En général, on préfèrera les sources de données utilisateurs ou systèmes. IV. Programmation IV-A. La base de données Avant de commencer à programmer, créez d'abord une base de données que vous nommerez pays_db contenant une table appelée pays_tbl constituée de deux champs : pays (VARCHAR(14)) et capitale (VARCHAR(14)). Dans la suite, nous supposons qu'un utilisateur nommé melem, utilisant le mot de passe 1234, ait accès à cette base. Vous remplacerez donc ces nom et mot de passe par les vôtres ou rien (chaînes vides) si ce n'est pas requis. Créez ensuite une source de données liée à cette base et nommez-la pays_dsn sans spécifier d'utilisateur ni mot de passe par défaut afin que l'authentification soit obligatoire à chaque connexion. Lorsque tout est prêt, vous pouvez continuer. IV-B. Création d'une simple application Pour utiliser ODBC sous Windows, il faut se lier avec odbc32.lib. En ce qui concerne le fichiers d'en-tête, cela dépend des fonctions que l'on va utiliser mais dans tous les cas, il faut tout d'abord inclure windows.h. Une fois la source de données créée, se connecter à la base de données est relativement très simple. Avant toute chose, il faut tout d'abord créer un environnement. A l'intérieur d'un environnement, on pourra ensuite ouvrir une ou plusieurs connexions. Une fois la connexion ouverte, on peut désormais envoyer des commandes (qui seront par exemple des requêtes SQL) à la base de données. Nous allons donc commencer par créer notre environnement, plus précisément : allouer un handle d'environnement. Dans ODBC 3.x, la fonction à utiliser est SQLAllocHandle. SQLRETURN SQLAllocHandle(SQLSMALLINT HandleType, SQLHANDLE InputHandle, SQLHANDLE * OutputHandlePtr); En premier argument, on doit spécifier le type du handle qu'on veut allouer. Dans notre cas il s'agit d'un handle d'environnement donc on met SQL_HANDLE_ENV. En deuxième argument, nous devons spécifier le « parent » du handle que l'on veut créer. Dans notre cas, on doit passer NULL car un environnement ne peut pas avoir de parent (mais un environnement par exemple est parent d'une éventuelle connexion). En troisième argument, nous devons fournir l'adresse de la variable dans laquelle nous voulons stocker le handle retourné. Et enfin, cette fonction retourne comme toutes SQL_SUCCESS ou SQL_SUCCESS_WITH_INFO en cas de succès, autre chose en cas d'erreur. La différence entre SQL_SUCCESS et SQL_SUCCESS_WITH_INFO sera expliqué un peu plus bas mais pour l'instant, sachez que pour tester si une fonction a réussi, il faut comparer sa valeur de retour avec SQL_SUCCESS et SQL_SUCCESS_WITH_INFO. Une fonction a réussi si elle a retourné SQL_SUCCESS ou SQL_SUCCESS_WITH_INFO. Heureusement, il existe une macro qui fait bien cette comparaison, c'est la macro SQL_SUCCEEDED. #include <windows.h> /* A inclure uniquement sous Windows */ #include <sql.h> /* API ODBC de base */ #include <sqlext.h> /* Fonctions propres a ODBC 3.x */ int main() { SQLHENV env; if ( !SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_ENV, NULL, &env)) ) /* SQLAllocHandle a echoue */ ; else { /* On continue */ SQLFreeHandle(SQL_HANDLE_ENV, env); } return 0; } Ensuite, comme nous voulons utiliser ODBC 3.x (que ce soit 3.0 ou 3.5x) , nous devons configurer notre environnement de manière à supporter cette version. SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0); A l'intérieur d'un environnement, on peut créer autant de connexions que l'on souhaite (dans la mesure où le système et/ou le SGBD le permet). Dans notre cas, nous allons créer une seule connexion. SQLHDBC con; SQLAllocHandle(SQL_HANDLE_DBC, env, &con); Et maintenant, la partie la plus intéressante, nous allons nous connecter à la base de données représentée par la source de données pays_dsn, en utilisant le login melem et le mot de passe 1234. La fonction à utiliser est SQLConnect. SQLCHAR dsn[] = "pays_dsn", uid[] = "melem", pwd[] = "1234"; if ( !SQL_SUCCEEDED(SQLConnect(con, dsn, SQL_NTS, uid, SQL_NTS, pwd, SQL_NTS)) ) /* SQLConnect a echoue */ ; else { /* On continue */ SQLDisconnect(con); } SQL_NTS signifie Null Terminated String, c'est-à-dire chaîne de caractères terminée par zéro. En effet SQLConnect ne requiert pas que les chaînes passées en arguments doivent se terminer par le caractère de fin de chaîne du C ('\0'), mais requiert le nombre de caractères de chaque chaîne. SQL_NTS permet de lui indiquer qu'on a quand même pris le soin de mettre le caractère nul en fin de chaîne, ce qui nous permet de passer des chaînes sans qu'on ait à spécifier leur longueur. Lorsque la connexion a réussi, on peut désormais échanger des données avec la base. Nous-nous proposons de récupérer (puis afficher) tous les enregistrements de la table pays_tbl. Il faut commencer par créer un handle d'un tampon qui va servir à contenir une requête ou le résultat d'une requête. Nous utiliserons donc ce handle chaque fois que nous voulons exécuter une requête puis pour récupérer le résultat de la requête. SQLHSTMT stmt; SQLAllocHandle(SQL_HANDLE_STMT, con, &stmt); Pour exécuter notre requête, ce n'est pas plus compliqué : SQLExecDirect(stmt, "SELECT * FROM pays_tbl;", SQL_NTS); Que l'on aurait également pu faire en deux temps (mais on va garder la première) : SQLPrepare(stmt, "SELECT * FROM pays_tbl;", SQL_NTS); SQLExecute(stmt); Il ne nous reste donc plus qu'à parcourir les enregistrements retournés et ensuite les afficher. La fonction qui nous permettra d'y arriver est SQLFetch, qu'on utilisera en boucle. SQLRETURN SQLFetch(SQLHSTMT StatementHandle); Contrairement à SQLFetch qui ne sait qu'avancer d'un pas (c'est-à-dire aller à l'enregistrement suivant), la fonction SQLFetchScroll permet de spécifier une direction et un pas. SQLRETURN SQLFetchScroll(SQLHSTMT StatementHandle, SQLSMALLINT FetchOrientation, SQLINTEGER FetchOffset); Avec les différentes directions possibles : SQL_FETCH_NEXT, SQL_FETCH_PRIOR, SQL_FETCH_FIRST, SQL_FETCH_LAST, SQL_FETCH_ABSOLUTE, SQL_FETCH_RELATIVE et SQL_FETCH_BOOKMARK. La fonction SQLExtendedFetch est identique à SQLFetchScroll mais fournit plus d'infos sur le résultat de l'opération. Comme nous le savons très bien une ligne (un enregistrement) est organisée en colonnes (numérotées de 1 à n). La fonction SQLGetData permet de récupérer le contenu d'une colonne de la ligne courante dans le jeu d'enregistrements identifié par StatementHandle. SQLRETURN SQLGetData( SQLHSTMT StatementHandle, SQLUSMALLINT ColumnNumber, SQLSMALLINT TargetType, SQLPOINTER TargetValuePtr, SQLINTEGER BufferLength, SQLINTEGER * IndicatorPtr ); Comme nous pouvons le constater, la fonction a besoin de connaître le type de la variable destinée à contenir la donnée qu'on veut récupérer. Quel que soit le type de la donnée dans la base, le pilote ODBC fera toujours les conversions nécessaires suivant la valeur de l'argument TargetType (évidemment il faut quand même que la conversion ait un sens ...). Le type SQL_C_CHAR (chaîne de caractères C) s'avère donc particulièrement intéressant dans la mesure où il est possible de formater n'importe quelle donnée en chaîne de caractères et qu'il est ensuite assez facile d'analyser une chaîne, de la convertir en entier ou en flottant ou encore d'en extraire les informations utiles. Cependant, rien ne nous empêche d'utiliser directement les types SQL_C_BIT (caractère), SQL_C_SLONG, SQL_C_ULONG, SQL_C_DOUBLE, SQL_C_BINARY (données au format brut), etc. Le dernier argument de cette fonction permet de spécifier l'adresse d'une variable destinée à contenir des informations supplémentaires sur la donnée retournée (longueur si c'est une chaîne, SQL_NULL_DATA si la colonne est vide, etc.). Et voilà, on a pratiquement terminé. Avouez que c'a n'a rien de compliqué, mais on va tout de même faire un petit exemple en guise de résumé. Fichier : pays_dsn.c #include #include #include #include <stdio.h> <windows.h> <sql.h> <sqlext.h> int main() { SQLHENV env; if ( !SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_ENV, NULL, &env)) ) fprintf(stderr, "La fonction SQLAllocHandle a echoue (SQL_HANDLE_ENV).\n"); else { if ( !SQL_SUCCEEDED(SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0)) ) fprintf(stderr, "La fonction SQLSetEnvAttr a echoue.\n"); else { SQLHDBC con; if ( !SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_DBC, env, &con)) ) fprintf(stderr, "La fonction SQLAllocHandle a echoue (SQL_HANDLE_DBC).\n"); else { SQLCHAR dsn[] = "pays_dsn", uid[] = "melem", pwd[] = "1234"; if ( !SQL_SUCCEEDED(SQLConnect(con, dsn, SQL_NTS, uid, SQL_NTS, pwd, SQL_NTS)) ) fprintf(stderr, "La fonction SQLConnect a echoue.\n"); else { SQLHSTMT stmt; if ( !SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_STMT, con, &stmt)) ) fprintf(stderr, "La fonction SQLAllocHandle a echoue (SQL_HANDLE_STMT).\n"); else { if ( !SQL_SUCCEEDED(SQLExecDirect(stmt, "SELECT * FROM pays_tbl;", SQL_NTS)) ) fprintf(stderr, "La fonction SQLExecDirect a echoue.\n"); else { SQLCHAR pays[15], capitale[15]; printf("PAYS > CAPITALE\n\n"); while (SQL_SUCCEEDED(SQLFetch(stmt))) { SQLGetData(stmt, 1, SQL_C_CHAR, pays, sizeof(pays), NULL); SQLGetData(stmt, 2, SQL_C_CHAR, capitale, sizeof(capitale), NULL); printf("%-14s %-14s\n", pays, capitale); } } SQLFreeHandle(SQL_HANDLE_STMT, stmt); } SQLDisconnect(con); } SQLFreeHandle(SQL_HANDLE_DBC, con); } } SQLFreeHandle(SQL_HANDLE_ENV, env); } return 0; } IV-C. Amélioration Le programme précédent était trop dépendant de la structure actuelle de la table pays_tbl. En effet, il y a une fonction très utile qu'on n'a pas encore étudiée : SQLNumResultCols. SQLRETURN SQLNumResultCols(SQLHSTMT StatementHandle, SQLSMALLINT * ColumnCountPtr); Comme son nom l'indique, cette fonction permet de récupérer le nombre de colonnes dans le jeu d'enregistrement retourné par une commande ou une requête. Cela permet par exemple de traiter le résultat d'une requête SELECT sans avoir à connaître d'avance la structure de ce résultat, comme le montre l'extrait de code suivant : if ( !SQL_SUCCEEDED(SQLExecDirect(stmt, "SELECT * FROM pays_tbl;", SQL_NTS)) ) fprintf(stderr, "La fonction SQLExecDirect a echoue.\n"); else { SQLSMALLINT nbcolumns; SQLCHAR (*columns) [15]; SQLNumResultCols(stmt, &nbcolumns); columns = malloc(nbcolumns * sizeof(columns[0])); if (columns != NULL) { printf("TABLE : PAYS_TBL\n\n"); while (SQL_SUCCEEDED(SQLFetch(stmt))) { SQLSMALLINT i; for(i = 0; i < nbcolumns; i++) { SQLGetData(stmt, i + 1, SQL_C_CHAR, columns[i], sizeof(columns[i]), NULL); printf("%-14s ", columns[i]); } putchar('\n'); } free(columns); } } IV-D. Une autre méthode : attacher les colonnes Cette méthode permet d'éviter d'effectuer un appel répété à SQLGetData pour lire le contenu d'un enregistrement en attachant préalablement les colonnes du tableau à différentes variables avant d'entrer dans une boucle (alors que SQLGetData était appelée à chaque itération), en utilisant la fonction SQLBindCol comme le montre l'exemple suivant : if ( !SQL_SUCCEEDED(SQLExecDirect(stmt, "SELECT * FROM pays_tbl;", SQL_NTS)) ) fprintf(stderr, "La fonction SQLExecDirect a echoue.\n"); else { SQLSMALLINT nbcolumns; SQLCHAR (*columns) [15]; SQLNumResultCols(stmt, &nbcolumns); columns = malloc(nbcolumns * sizeof(columns[0])); if (columns != NULL) { SQLSMALLINT i; /* Attacher les colonnes */ for(i = 0; i < nbcolumns; i++) SQLBindCol(stmt, i + 1, SQL_C_CHAR, columns[i], sizeof(columns[i]), NULL); printf("TABLE : PAYS_TBL\n\n"); while (SQL_SUCCEEDED(SQLFetch(stmt))) { for(i = 0; i < nbcolumns; i++) printf("%-14s ", columns[i]); putchar('\n'); } free(columns); } } V. Analyse des résultats Les résultats retournés par une commande se présentent souvent sous la forme d'un jeu d'enregistrements, chaque enregistrement étant composé de un ou plusieurs champs (colonnes). Pour bien exploiter ces résultats, il faut connaître les attributs de chaque champ (son nom, le type de la donnée qu'elle contient, sa longueur max, etc.). Nous avons déjà vu la fonction SQLNumResultCols qui permet de connaître le nombre de colonnes dans un jeu d'enregistrements donné. La fonction SQLColAttribute permet de lire un attribut d'une colonne dont le numéro est spécifié en argument. SQLRETURN SQLColAttribute( SQLHSTMT StatementHandle, SQLUSMALLINT ColumnNumber, SQLUSMALLINT FieldIdentifier, SQLPOINTER CharacterAttributePtr, SQLSMALLINT BufferLength, SQLSMALLINT * StringLengthPtr, SQLPOINTER NumericAttributePtr ); StatementHandle identifie le jeu d'enregistrements dans lequel on souhaite travailler, ColumnNumber le numéro de la colonne qui nous intéresse et FieldIdentifier l'attribut qu'on veut lire. Quand cet attribut est une chaîne de caractères (par exemple le nom de la colonne), alors il est retourné dans le tampon pointé par CharacterAttributePtr. BufferLength doit alors indiquer la taille de ce tampon. StringLengthPtr quant à lui permet de spécifier un pointeur vers un tampon dans lequel sera retourné la longueur de la chaîne, sans compter le caractère de fin de chaîne. Si elle est supérieure ou égale à BufferLength, alors seuls les BufferLength - 1 premiers caractères de la chaîne ont été copiés dans le tampon pointé par CharacterAttributePtr, le dernier caractère étant toujours le caractère de fin de chaîne. Quand cet attribut est un nombre, il est copié dans le tampon pointé par NumericAttributePtr. CharacterAttributePtr et NumericAttributePtr ne sont jamais utilisés en même temps. Voici les champs qui nous intéressent le plus souvent : SQL_DESC_BASE_COLUMN_NAME : nom de la colonne. SQL_DESC_LABEL : label de la colonne (peut être un titre, un alias ou le nom de la colonne par exemple). SQL_DESC_NULLABLE : un entier qui indique si oui (SQL_NULLABLE) ou non (SQL_NO_NULLS) la colonne accepte NULL (SQL). Peut être SQL_NULLABLE_UNKNOWN. SQL_DESC_LENGTH : longueur max (sans compter le '\0' quand c'est une chaîne) du champ lorsque ce champ contient une chaîne (de taille fixe ou de taille variable) ou des données brutes. SQL_DESC_OCTET_LENGTH : longueur max du champ lorsque ce champ contient une chaîne (de taille fixe ou de taille variable) ou des données brutes. SQL_DESC_TYPE : un entier qui indique le type de la donnée que contient la colonne. SQL_DESC_TYPE_NAME : une chaîne qui indique le type de la donnée que contient la colonne. Nous sommes donc à présent en mesure d'écrire une fonction simple permettant d'exécuter et d'obtenir sur la sortie standard le résultat d'une commande. La fonction suivante est loin de la perfection mais elle nous servira quand même assez dans ce tutoriel. SQLRETURN sql_do_cmd(SQLHSTMT stmt, SQLCHAR * query) { SQLRETURN ret = SQLExecDirect(stmt, query, SQL_NTS); if ( !SQL_SUCCEEDED(ret) ) fprintf(stderr, "SQL ERROR : %s\n", query); else sql_show_results(stmt); return ret; } Avec la fonction sql_show_results définie comme suit : void sql_show_results(SQLHSTMT stmt) { SQLSMALLINT nbcolumns; SQLCHAR (*columns) [15]; SQLNumResultCols(stmt, &nbcolumns); if (nbcolumns > 0) { columns = calloc(nbcolumns, sizeof(columns[0])); /* Il faut utiliser calloc */ /* car chaque champ doit etre initialise afin d'eviter d'afficher de valeurs */ /* non desirees lorsque le champ est cense etre vide. */ if (columns != NULL) { SQLSMALLINT i; for(i = 0; i < nbcolumns; i++) SQLBindCol(stmt, i + 1, SQL_C_CHAR, columns[i], sizeof(columns[i]), NULL); /* Afficher l'en-tete */ putchar('+'); for(i = 0; i < nbcolumns; i++) printf("--------------+"); putchar('\n'); putchar('|'); for(i = 0; i < nbcolumns; i++) { SQLCHAR column_name[15] = ""; SQLColAttribute(stmt, i + 1, SQL_COLUMN_NAME, column_name, sizeof(column_name), NULL, NULL); printf("%-14s|", column_name); } putchar('\n'); putchar('+'); for(i = 0; i < nbcolumns; i++) printf("--------------+"); putchar('\n'); /* Afficher les enregistrements */ while (SQL_SUCCEEDED(SQLFetch(stmt))) { putchar('|'); for(i = 0; i < nbcolumns; i++) printf("%-14s|", columns[i]); putchar('\n'); putchar('+'); for(i = 0; i < nbcolumns; i++) printf("--------------+"); putchar('\n'); /* Avant de tenter de lire l'enregistrement suivant, initialiser les champs */ for(i = 0; i < nbcolumns; i++) columns[i][0] = '\0'; } /* Termine */ free(columns); } } } Il y a aussi la fonction SQLDecribeCol qui permet d'obtenir en un seul appel le nom d'une colonne, le type de la donnée contenue dans cette colonne ainsi que d'autres informations qui peuvent être utiles. Lorsque l'information que l'on désire connaître ne peut pas être obtenue par SQLDescribeCol, on continuera à utiliser SQLColAttribute. SQLRETURN SQLDescribeCol( SQLHSTMT StatementHandle, SQLSMALLINT ColumnNumber, SQLCHAR * ColumnName, SQLSMALLINT BufferLength, SQLSMALLINT * NameLengthPtr, SQLSMALLINT * DataTypePtr, SQLUINTEGER * ColumnSizePtr, SQLSMALLINT * DecimalDigitsPtr, SQLSMALLINT * NullablePtr ); VI. Les diagnostics Nous avons déjà vu la macro SQL_SUCCEEDED qui retourne VRAI ou FAUX selon qu'on lui a passé une valeur indiquant un succès ou un échec. Plus précisément SQL_SUCCEEDED retourne VRAI si l'argument vaut SQL_SUCCESS ou SQL_SUCCESS_WITH_INFO et FAUX dans le cas contraire. Si nous n'avons pas expliqué plutôt ce que signifiaient ces codes, ce n'est pas par hasard ... Selon l'issue d'un appel, le driver reporte parfois un certain nombre de diagnostics permettant d'avoir des informations supplémentaires sur le déroulement de l'appel. A noter que le gestionnaire de pilotes lui-même est également susceptible de reporter des diagnostics. Ces diagnostics se présentent sous la forme d'un jeu d'enregistrements. Quand une fonction retourne SQL_SUCCESS, cela signifie qu'elle a réussi et qu'en général, aucun diagnostic n'a été reporté par le driver ou le gestionnaire lui-même. Quand une fonction retourne SQL_ERROR, cela signifie qu'elle a échoué et que le le driver ou le gestionnaire de pilotes ou les deux en même temps ont reporté des diagnostics décrivant les différentes causes de l'erreur. Quand une fonction retourne SQL_SUCCESS_WITH_INFO, cela signifie qu'elle a réussi mais le pilote ou le gestionnaire de pilotes ou les deux en même temps ont reporté des diagnostics fournissant plus de détails sur la réussite de l'appel. Quand une fonction retourne valeur qui n'est ni SQL_SUCCESS, ni SQL_SUCCESS_WITH_INFO, ni SQL_ERROR, cela signifie que la fonction a échoué mais qu'aucun diagnostic n'a été reporté. Il y a toutefois quelques exceptions à cette règle. Un diagnostic est composé de plusieurs champs. Le nombre exact de champs par diagnostic dépend de la version d'ODBC utilisée. D'ailleurs, un pilote peut ajouter des champs supplémentaires mais ces champs ne sont donc, évidemment, pas standards. De plus, la première ligne (numérotée 0) du jeu de diagnostics retourné par un appel est appelée en-tête et possède une structure différente de celle des autres diagnostics qui sont appelés les enregistrements. En d'autres termes, les champs qui constituent l'en-tête sont différents des champs qui constituent un enregistrement. L'en-tête est rarement consulté, mais cela ne signifie pas qu'il est n'a aucune utilité. La fonction SQLGetDiagField permet de lire la valeur d'un champ d'un diagnostic donné. Il n'existe pas de fonction permettant de connaître le nombre d'enregistrements dans le jeu de diagnostics retourné mais cette information peut être lue dans le champ SQL_DIAG_NUMBER de l'en-tête. Dans la pratique, on n'a pas vraiment besoin de ce champ puisqu'il suffit d'utiliser cette fonction à l'intérieur d'une boucle, jusqu'à ce qu'elle retourne SQL_NO_DATA, pour parcourir tous les enregistrements. SQLRETURN SQLGetDiagField( SQLSMALLINT SQLSMALLINT SQLSMALLINT DiagInfoPtr, SQLSMALLINT BufferLength, SQLSMALLINT HandleType, SQLHANDLE Handle, RecNumber, DiagIdentifier, SQLPOINTER * StringLengthPtr ); HandleType doit indiquer le type du handle fourni en deuxième argument de la fonction. Par exemple, si SQLConnect retourne SQL_ERROR et qu'on veut connaître la ou les raisons de cet échec, on effectuera un ou plusieurs appels à SQLGetDiagRec en passant SQL_HANDLE_DBC dans l'argument HandleType et le handle de la connexion dans l'argument Handle. RecNumber indique le numéro de la ligne qu'on veut lire (0 pour l'en-tête, 1 pour le premier enregistrement), DiagIdentifier le champ qui nous intéresse, DiagInfoPtr un pointeur vers le tampon destiné à recevoir l'information lue et BufferLength la taille de ce tampon. Et enfin, l'argument optionnel StringLengthPtr n'est utilisé que lorsque l'information demandée est une chaîne de caractères. Dans ce cas, il permet de passer un pointeur vers un tampon destiné à recevoir la longueur de cette chaîne (caractère de fin de chaîne exclu). Si elle est supérieure ou égale à BufferLength, alors seuls les BufferLength - 1 premiers caractères de la chaîne ont été copiés dans le tampon pointé par DiagInfoPtr, le dernier caractère étant toujours le caractère de fin de chaîne. Parmi les champs constituant chaque enregistrement, SQL_DIAG_SQLSTATE et SQL_DIAG_MESSAGE_TEXT sont des plus importants car ils fournissent respectivement son code ainsi que sa description. Le champ SQL_DIAG_NATIVE a également son importance puisqu'il s'agit, un peu à la manière de SQLSTATE, d'un code d'erreur, sauf que sa signification est dépendante du driver. Un SQLSTATE est une chaîne composée de 5 (SQL_SQLSTATE_SIZE) caractères (6 lorsqu'on compte le caractère de fin de ligne) servant à déterminer globalement la cause d'une erreur, la description détaillée étant fournie par le MESSAGE_TEXT. La taille d'un tel message, en incluant le caractère de fin de chaîne, est garanti être tout au plus égale à SQL_MAX_MESSAGE_LENGTH. Voici une fonction qui permet d'afficher les différents diagnostics retournés par un appel lorsqu'il y en a : void sql_perror(SQLSMALLINT HandleType, SQLHANDLE Handle, const char * title) { SQLCHAR s[SQL_MAX_MESSAGE_LENGTH]; SQLSMALLINT ligne = 1; fprintf(stderr, "%s :\n", title); while (SQLGetDiagField(HandleType, Handle, ligne, SQL_DIAG_MESSAGE_TEXT, s, sizeof(s), NULL) != SQL_NO_DATA) { fprintf(stderr, "%d. %s\n", ligne, s); ligne++; } } Etant donné que les champs SQLSTATE, NATIVE et MESSAGE_TEXT sont les plus utilisés lorsqu'on lit un diagnostic, ODBC fournit la fonction SQLGetDiagRec qui s'utilise à peu près de la même manière que SQLGetDiagField, à la différence qu'elle retourne directement ces 3 champs alors qu'on aurait effectué 3 appels pour les avoir si on avait utilisé SQLGetDiagField. Par contre, SQLGetDiagRec ne peut lire qu'un enregistrement (ce qui signifie qu'il faut passer un nombre supérieur ou égal à 1 dans l'argument RecNumber). Pour lire l'en-tête, il faut utiliser la fonction SQLGetDiagField. SQLRETURN SQLGetDiagRec( SQLSMALLINT HandleType, SQLHANDLE Handle, SQLSMALLINT RecNumber, SQLCHAR * SqlState, SQLINTEGER * NativeErrorPtr, SQLCHAR * MessageText, SQLSMALLINT BufferLength, SQLSMALLINT * TextLengthPtr ); VII. Se connecter de différentes manières VII-A. La fonction SQLDriverConnect La fonction SQLConnect permet de se connecter à une base de données en spécifiant le nom de la source de données correspondante, le login et le mot de passe, les informations relatives à la base elle-même étant stockée dans la source de données. Cependant, il peut arriver parfois que l'on souhaite spécifier plus de paramètres de connexion (qui peuvent être spécifiques du SGBD) et dans ce cas on utilisera la fonction SQLDriverConnect. SQLRETURN SQLDriverConnect( SQLHDBC ConnectionHandle, SQLHWND WindowHandle, SQLCHAR * InConnectionString, SQLSMALLINT StringLength1, SQLCHAR * OutConnectionString, SQLSMALLINT BufferLength, SQLSMALLINT * StringLength2Ptr, SQLUSMALLINT DriverCompletion ); C'est une fonction très souple, permettant de réaliser de grandes choses. Un des paramètres importants de cette fonction est le paramètre DriverCompletion. Ce paramètre permet de spécifier la manière d'établir la connexion. Nous y reviendrons assez souvent mais parlons tout d'abord des autres paramètres. VII-B. Les paramètres ConnexionHandle est bien sûr le handle de connexion à associer à la connexion. WindowHandle n'a de sens que si le gestionnaire de pilotes ODBC a été sommé d'afficher une boîte de dialogue permettant à l'utilisateur de fournir lui-même les paramètres de connexion (source de données, login, mot de passe, etc.), sinon on mettra tout simplement NULL. Quand il est utilisé, WindowHandle permet de spécifier quelle fenêtre sera le parent de cette boîte de dialogue. Dans une application console, on pourra par exemple spécifier le handle de la fenêtre console que l'on peut récupérer à l'aide de la fonction GetConsoleWindow depuis Windows 2000 ou alors le handle du bureau que l'on peut récupérer à l'aide de GetDesktopWindow. Sous UNIX, n'importe quelle valeur différente de 0 permet de donner au driver l'autorisation d'afficher la boîte de dialogue. InConnectionString est une chaîne contenant les paramètres de connexion séparés par un point-virgule, chaque nom de paramètre étant suivi du signe = et de la valeur du paramètre, par exemple : "DSN=pays_dsn;UID=melem;PWD=1234". Comme on peut le voir d'après cet exemple, la dernière paire paramètre-valeur n'a plus besoin d'être terminée par un pointvirgule. Certains paramètres (comme DSN, UID et PWD par exemple) sont standard, d'autres sont spécifiques au SGBD. StringLength1 doit bien sûr spécifier la longueur de la chaîne InConnectionString mais comme d'habitude on peut utiliser SQL_NTS. En fait, deux cas peuvent se présenter : Les paramètres de connexion sont fournis par le programme, dans ce cas InConnexionString et StringLength1 trouvent pleinement leur utilité. Les paramètres de connexion sont à récupérer depuis une boîte de dialogue, dans ce cas on peut mettre NULL ou une chaîne vide dans InConnexionString et 0 dans StringLength1. Si on spécifie quand même un ou plusieurs paramètres, les champs de la boîte de dialogue, selon le paramètre DriverCompletion, seront initialisés avec ces valeurs sinon elles seront tout simplement ignorées. A noter que, à cause de sa syntaxe, certains caractères doivent s'utiliser avec précaution. Le cacartère ; par exemple ne peut pas apparaître n'importe où ! Ainsi, si la valeur d'un paramètre (ou dans certains cas le nom d'un paramètre) comporte un ou plusieurs caractères parmi [ ] { } ( ) , ; ? * = ! @, il est conseillé de la mettre entre accolades (PARAM={value};) afin d'éviter toute mauvaise interprétation. Une fois toutes les informations nécessaires recueillies, le driver va initier la connexion puis retournera via les paramètres facultatifs OutConnectionString et StringLength2Ptr les informations sur la chaîne de connexion qui fut utilisée. Si le paramètre OutConnectionString est utilisé, BufferLength doit spécifier le nombre de caractères maximum qu'on peut placer dans le buffer. Et enfin, comme nous l'avons déjà dit dès le départ, le paramètre DriverCompletion permet d'indiquer à la fonction certaines directives pour l'aider à établir la connexion. Lorsque ces directives parviennent au driver (ce qui signifie que le pilote à utilisé a été spécifié dans la chaîne de connexion) : SQL_DRIVER_PROMPT affiche inconditionellement une boîte de dialogue pour entrer les paramètres de connexion. Les champs correspondant à des paramètres déjà connus seront initialisés. SQL_DRIVER_COMPLETE ou SQL_DRIVER_COMPLETE_REQUIRED permettent toutes d'établir la connexion à partir de la chaîne InConnexionString mais si et seulement si les paramètres fournis dans cette chaîne sont incomplets (c'est-à-dire qu'un ou plusieurs paramètres indispensables n'ont pas été fournis) ou invalides, alors une boîte de dialogue avec les champs éventuellement initialisés par les valeurs des paramètres déjà fournis va être affichée pour compléter le informations manquantes sauf que si on a utilisé SQL_DRIVER_COMPLETE_REQUIRED, alors les champs déjà fournis ne pourront plus être modifiés alors que dans l'autre cas (SQL_DRIVER_COMPLETE) l'utilisateur pourra modifier même les paramètres qui sont déjà bons. SQL_DRIVER_NOPROMPT spécifie que la connexion doit se faire en utilisant la chaîne passée via le paramètre InConnexionString. Si la chaîne est mauvaise, la fonction échouera. Si la chaîne de connexion ne spécifie aucun pilote (discuté dans le paragraphe suivant) et qu'une boîte de dialogue doit être affichée, alors c'est le gestionnaire de pilotes qui se chargera du travail. Si une boîte de dialogue s'affiche et que l'utilisateur annule celle-ci, SQLDriverConnect retournera SQL_NO_DATA. VII-C. La chaîne de connexion Les la liste complète des paramètres qu'on peut spécifier pour se connecter à une base de données dépend du SGBD. Néanmoins certains paramètres comme DSN, UID et PWD sont standard (ce sont les paramètres qu'on devait passer à la fonction SQLConnect). Mais il y a également deux autres paramètres qu'on peut utiliser à la place de DSN (il ne faut surtout pas les utiliser en même temps !) que nous n'avons pas encore étudiés : DRIVER et FILEDNS. DRIVER permet de se connecter au serveur lui-même en spécifiant le nom (tel qu'il apparaît dans l'administrateur de sources de données) du pilote à utiliser. Si vous avez bien compris, ce paramètre permet de se connecter à un SGBD (ou, en spécifiant plus de paramètres, directement à une base de données) sans passer par une source de données ! On peut obtenir la liste de tous les pilotes installés à l'aide de la fonction SQLDrivers. On peut également observer cette liste depuis l'administrateur de sources de données ODBC. Hélas, cela ne nous renseigne pas toujours sur tous les paramètres. Ces paramètres, on les trouve normalement dans la documentation du pilote mais aussi ... dans une source de données fichier ! En effet, une source de données fichier est un bête fichier texte qui contient les paramètres nécessaires (et en plus, pas seulement ce qui sont vraiment nécessaires ...) pour la connexion. Quant à FILEDSN, il est utilisé pour se connecter à une base de données en passant par une source de données fichier. C'est la méthode la moins conseillée. Voici donc, à titre illustratif, un programme qui se connecte à MySQL par l'intermédiaire du pilote MySQL ODBC 3.51 Driver et qui, une fois connecté, affiche la liste des bases de données actuellement sur le serveur. Dans cet exemple, nous supposons que le serveur se trouve sur la machine locale (localhost). #include #include #include #include <stdio.h> <windows.h> <sql.h> <sqlext.h> void sql_perror(SQLSMALLINT HandleType, SQLHANDLE Handle, const char * title); SQLRETURN sql_do_cmd(SQLHSTMT stmt, SQLCHAR * query); int main() { SQLHENV env; if ( !SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_ENV, NULL, &env)) ) fprintf(stderr, "SQLAllocHandle (SQL_HANDLE_ENV) : la fonction a echoue.\n"); else { if ( !SQL_SUCCEEDED(SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *)SQL_OV_ODBC3, 0)) ) sql_perror(SQL_HANDLE_ENV, env, "SQLSetEnvAttr"); else { SQLHDBC con; if ( !SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_DBC, env, &con)) ) sql_perror(SQL_HANDLE_ENV, env, "SQLAllocHandle (SQL_HANDLE_DBC)"); else { SQLCHAR in[] = "DRIVER=MySQL ODBC 3.51 Driver;UID=melem;PWD=1234"; if ( !SQL_SUCCEEDED(SQLDriverConnect(con, NULL, in, SQL_NTS, NULL, 0, NULL, SQL_DRIVER_NOPROMPT)) ) sql_perror(SQL_HANDLE_DBC, con, "SQLDriverConnect"); else { SQLHSTMT stmt; if ( !SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_STMT, con, &stmt)) ) sql_perror(SQL_HANDLE_DBC, con, "SQLAllocHandle (SQL_HANDLE_STMT)"); else { /* ********************************************************************** */ /* [MySQL] La commande SHOW DATABASES affiche toutes les bases de donnees */ /* ********************************************************************** */ sql_do_cmd(stmt, "SHOW DATABASES;"); SQLFreeHandle(SQL_HANDLE_STMT, stmt); } SQLDisconnect(con); } SQLFreeHandle(SQL_HANDLE_DBC, con); } } SQLFreeHandle(SQL_HANDLE_ENV, env); } return 0; } VIII. Connaître la structure d'une base de données VIII-A. La fonction SQLTables Cette fonction permet entre autres d'énumérer les tables (tables et vues confondues ...) contenues dans une base de données. Les informations retournées par cette fonction se présentent sous la forme d'un jeu d'enregistrements (un enregistrement par table) composé chacun d'exactement champs à savoir : TABLE_CAT (champ n° 1) : nom du catalogue auquel appartient la table. Peut être vide. TABLE_SCHEM (champ n° 2) : lorsqu'un schéma a été défini, nom du schéma auquel appartient la table, sinon contient une chaîne vide. TABLE_NAME (champ n° 3) : nom de la table. TABLE_TYPE (champ n° 4) : le type de la table (par exemple TABLE indique que c'est une table, VIEW indique que c'est une vue, et ainsi de suite). Et enfin REMARKS (champ n° 5) : commentaires ou simplement une chaîne vide. La fonction SQLTables est déclarée ainsi : SQLRETURN SQLTables( SQLHSTMT StatementHandle, SQLCHAR * CatalogName, SQLSMALLINT NameLength1, SQLCHAR * SchemaName, SQLSMALLINT NameLength2, SQLCHAR * TableName, SQLSMALLINT NameLength3, SQLCHAR * TableType, SQLSMALLINT NameLength4 ); StatementHandle est le handle qu'on veut utiliser pour récupérer les résultats. Les tables qui pourraient être retournées par SQLTables sont uniquement celles qui appartiennent à la base de données dans laquelle a été allouée StatementHandle. CatalogName, SchemaName, TableName et TableType sont des paramètres optionnels qui permettent de limiter le résultat uniquement aux tables répondant à certains critères. Ces paramètres sont par défaut sensibles à la casse mais ce comportement (ainsi que d'autres propriétés) peut être modifié en affectant la valeur SQL_TRUE à l'attribut SQL_ATTR_METADATA_ID, ce qui peut se faire à l'aide de la fonction SQLSetStmtAttr. Ils peuvent également contenir des caractères jokers. Les seuls jokers standard sont cependant _ (underscore) qui représente exactement un caractère et % qui représente 0 ou plusieurs caractères. Un pilote donné peut supporter d'autres caractères spéciaux. Le caractère d'échappement est \ (ainsi, \% par exemple sera interprété comme le caractère % et non le caractère backslash suivi du joker %). NULL est autorisé pour indiquer qu'il n'y pas de critère spécifique sur la valeur d'un paramètre. Quant aux paramètres NameLengthI, ils indiquent bien sûr la longueur de la chaîne passée dans le paramètre correspondant ou simplement SQL_NTS lorsque la chaîne est déjà terminée par un zéro. La fonction suivante permet de lister toutes les tables (et vues ..) d'une base de données. SQLRETURN sql_tables(SQLHSTMT stmt) { SQLRETURN ret = SQLTables(stmt, NULL, 0, NULL, 0, NULL, 0, NULL, 0); if ( !SQL_SUCCEEDED(ret) ) sql_perror(SQL_HANDLE_STMT, stmt, "SQLTables"); else sql_show_results(stmt); return ret; } Voici d'autres exemples d'appels à SQLTables qui vous aideront peut-être à mieux comprendre le fonctionnement de cette fonction : SQLTables(stmt, NULL, /* Affiche toutes les */ SQLTables(stmt, NULL, /* Affiche toutes les */ SQLTables(stmt, NULL, /* Affiche toutes les autres ...) */ SQLTables(stmt, NULL, /* Affiche toutes les */ SQLTables(stmt, NULL, /* Affiche les tables */ SQLTables(stmt, NULL, /* Affiche les tables */ 0, NULL, 0, NULL, 0, NULL, 0); tables 0, NULL, 0, "%", SQL_NTS, NULL, 0); tables 0, NULL, 0, "%", SQL_NTS, "TABLE", SQL_NTS); tables (TABLEs uniquement, pas les vues et 0, NULL, 0, "%", SQL_NTS, "VIEW", SQL_NTS); vues 0, NULL, 0, "%tbl", SQL_NTS, NULL, 0); dont le nom se termine par tbl 0, "", SQL_NTS, "%", SQL_NTS, NULL, 0); qui n'appartiennent a aucun schema Sachez également que certaines formes spéciales d'utilisation de SQLTables existent. Par exemple, quand CatalogName vaut SQL_ALL_CATALOGS ("%") et que les autres paramètres vallent NULL ou une chaîne vide, alors SQLTables retourne la liste de tous les catalogues existants. Quand SQLTables est appelée ainsi, les autres champs du jeu d'enregistrements retourné contiennent tous une chaîne vide et donc n'ont aucun intérêt. VIII-B. La fonction SQLColumns Cette fonction s'utilise de la même manière que SQLTables et permet d'énumérer les colonnes des différentes tables de la base, avec la possibilité de limiter le résultat à uniquement certaines colonnes de certaines tables, tout comme dans SQLTables. SQLRETURN SQLColumns( SQLHSTMT StatementHandle, SQLCHAR * CatalogName, SQLSMALLINT NameLength1, SQLCHAR * SchemaName, SQLSMALLINT NameLength2, SQLCHAR * TableName, SQLSMALLINT NameLength3, SQLCHAR * ColumnName, SQLSMALLINT NameLength4 ); Les paramètres ne sont plus à présenter. Seulement, sachez que lorsque CatalogName est utilisé (c'est-à-dire n'est pas NULL), il ne peut pas contenir de caractère joker (les jokers sont autorisés dans les autres paramètres). SQLColumns retourne le résultat sous forme d'un jeu d'enregistrements composé chacun de 18 champs (ou plus selon le driver) dont les plus intéressants, à part TABLE_CAT (champ n° 1), TABLE_SCHEM (champ n° 2) et TABLE_NAME (champ n° 3) que nous connaissons déjà, sont : COLUMN_NAME (4) : nom de la colonne. DATA_TYPE (5) : un entier permettant de connaître le type de la donnée de la colonne. TYPE_NAME (6) : une chaîne qui fournit le type de la donnée de la colonne COLUMN_SIZE (7) : un entier qui indique la taille de la colonne. Sa signification est dépendante de DATA_TYPE. NULLABLE (11) : un entier permettant de savoir si le champ accepte la valeur NULL (SQL). REMARKS (12) : commentaires. Et IS_NULLABLE (18) : une chaîne qui indique si oui ou non la colonne accepte la valeur NULL (SQL). IX. Conclusion En résumé, nous avons appris à créer une source de données, à se connecter à un serveur ou directement à une base de données (avec ou sans source de données), à exécuter une requête et à récupérer les résultats. Bien entendu, cela ne signifie pas que les possibilités d'ODBC s'arrêtent là, bien au contraire et c'est pourquoi nous n'avons étudié ici que les bases mais au moins, vous êtes maintenant prêt aborder les fonctionnalités avancées (procédures stockées, batchs SQL, requêtes paramétrées, transactions, curseurs, extensions du langage SQL, etc.) de cette API.