Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
高橋 理香
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 | いうえお] |