ODBC API を使用して mdb ファイルからメモ型データを取得する方法
高橋 理香
SQL Developer Support Eascalation Engineer
今回は最近見つかった、mdb ファイルからのデータ読み取りの問題の対応についてご紹介したいと思います。
1. 基本事項 - ODBC API によるデータの取得方法
ODBC API を使用してデータを取得する最も単純な方法として次のような関数シーケンスで実行する方法があります。
1) SQLExecDirect で SELECT 文を実行する。
2) SQLFetch で結果セットからレコードをフェッチする。
3) SQLGetData で取得したレコード内の列データを取得する。
※各種 API の詳細はリファレンスをご覧ください。
ODBC API Reference
https://msdn.microsoft.com/en-us/library/ms714562(v=VS.85).aspx
2. 発生しうる問題
次のような条件で、データの一部が欠ける問題が発生します。
- mdb からのデータ取得である。
- データには日本語 (DBCS) がデータに含まれている。
- データをバイナリ形式 (SQL_C_BINARY) で受け取る。
- SQLGetData で指定するバッファサイズが実際のデータ長よりも小さい。
- バッファの境界が日本語の先頭バイトの後ろに位置する。
例えば、次の文字列がデータに含まれていたとします。
0123456789あいうえお
これを非Unicode でバイナリで表現すると以下の通りです。
0
1
2
3
4
5
6
7
8
9
あ
い
う
え
お
30
31
33
32
34
35
36
37
38
39
82A0
82A2
82A4
82A6
82A8
もしも上記データを、バッファサイズを 11 バイトに指定した SQLGetData で受け取る場合、バッファの境界が "あ" という文字の途中の 82 までとなります。しかしながら、この 82 が 00 に埋められます。さらに、次回の SQLGetData によるデータ取得では、"あ" という文字の残りの A0 から以降のデータが読み取られることが期待されますが、実際には、"い" という文字から読み取られることになります。その結果、受け取ったデータは以下の通りとなり、文字列の途中に不正な値が入ることになります。
0
1
2
3
4
5
6
7
8
9
.
い
う
え
お
30
31
32
33
34
35
36
37
38
39
00
82A2
82A4
82A6
82A8
3. 関連するテクノロジー
ODBC を使用する各種テクノロジーでこの現象が発生するパターンとして現在確認しているのは以下の通りです。
- C++ で ODBC API を使用するアプリケーション
- JDBC-ODBC ブリッジを使用したアプリケーション
Microsoft より提供しているテクノロジーで ODBC API を呼び出すものとしては、VBScript から ADO を使用する場合や .NET Framework で System.Data.Odbc を使用する場合がありますが、これらはいずれも Access メモ型のデータを SQL_C_WCHAR で受け取る実装となっているため、問題は発生しません。
また、ODBC API を使用して mdb ファイルにあるテーブルのデータを取得する場合、以下のいずれかの ODBC ドライバを使用することができますが、いずれのケースでも発生します。
a) Microsoft Access Driver (*.mdb)
b) Microsoft Access Driver (*.mdb, *.accdb)
なお、a) は 32bit 版しかない提供していないため、x86 環境、もしくは、x64 環境上の WOW でしか使うことができません。詳細については、以下で説明しています。
Jet データベース エンジンを使用するアプリケーションの開発/動作環境
https://msdn.microsoft.com/ja-jp/data/gg607262
4. 問題を回避する方法
発生条件の1つでも異なれば問題は発生しませんので、以下のいずれかの対応で適切にDBCS を含むデータを SQLGetData で受け取ることができます。
- データの受け取りを文字列形式とする。(例: SQL_C_WCHAR)
- バッファサイズ≧ データベースから取得される文字列のバイト長 となるように指定する。(上記の例ではバッファサイズ ≧ 20 バイト)
上記のいずれも困難である (ODBC API 呼び出し元でデータ型を固定しているために変更できない、データ長が長すぎるために大きなバッファを確保したくない) 場合には、少々手間はかかりますが、実行する SELECT 文で MID 関数などを使用して文字列を分割して取得することで、問題を回避してデータを受け取ることができます。例えば、前述の JDBC-ODBC ブリッジを使用する場合には、ODBC API 呼び出しは JDBC 関連モジュールに実装されているため、この方法にて回避する必要があります。
5. 参考コード
参考のために、C++ のコードで問題を再現させる例をご紹介します。事前に必要な作業は以下の通りです。
- mdb ファイル内に t1 というテーブルを作成し、2列目に "0123456789あいうえお" というデータを入れます。
- "JetTestDSN" という名前で mdb ファイルにアクセスするための ODBC データソースを作成します。
#include "stdafx.h" #include <stdio.h> #include <windows.h> #include <sql.h> #include <sqlext.h> #include <odbcss.h> int _tmain(int argc, _TCHAR* argv[]) { SQLHENV henv = SQL_NULL_HENV; SQLHDBC hdbc = SQL_NULL_HDBC; SQLHSTMT hstmt = SQL_NULL_HSTMT; RETCODE retcode; SQLCHAR BinaryPtr[11] = {"\0"}; SQLLEN BinaryLenOrInd; SQLLEN NumBytes; retcode = SQLAllocHandle (SQL_HANDLE_ENV, NULL, &henv); retcode = SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, SQL_IS_INTEGER); retcode = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc); // 事前に作成した ODBC データソースを使用してデータベースに接続します。 retcode = SQLConnect(hdbc, (SQLCHAR*)"JetTestDSN", SQL_NTS, (SQLCHAR*)"",SQL_NTS, (SQLCHAR*)"", SQL_NTS); retcode = SQLAllocHandle(SQL_HANDLE_STMT, hdbc, &hstmt); // クエリを実行します。 retcode = SQLExecDirect(hstmt, (SQLCHAR*)"select * from t1", SQL_NTS); // 結果セットからレコードを取得します。 while (SQLFetch(hstmt) != SQL_NO_DATA) { // レコード内の列データをバイナリ形式で受け取ります。 while (SQLGetData(hstmt, 2, SQL_C_BINARY, BinaryPtr, 11, &BinaryLenOrInd) != SQL_NO_DATA) { NumBytes = (BinaryLenOrInd > 11) || (BinaryLenOrInd == SQL_NO_TOTAL) ? 11 : BinaryLenOrInd; printf("[%d | %s]\n", NumBytes, BinaryPtr); memset(BinaryPtr, 0, 11); } } SQLFreeHandle(SQL_HANDLE_STMT, hstmt); SQLDisconnect(hdbc); SQLFreeHandle(SQL_HANDLE_DBC, hdbc); SQLFreeHandle(SQL_HANDLE_ENV, henv); return 0; } |
上記を実行すると得られる結果は以下の通り、"あ" という文字が抜け落ちたものです。
[11 | 0123456789] [8 | いうえお] |