DBアクセス
データアダプタを使用したDBアクセスについての解説です。
接続確認
接続確認用として、SELECT文を実行する以下のコード(C#)を使用します。
using System;
using System.Data;
using System.Data.Odbc;
namespace OdbcExample
{
public class Select1
{
public static void Main(string[] args)
{
// 接続文字列
string constr = "DSN=TestData_DB2;" +
"UID=sta;" +
"PWD=password";
// SQL文字列
string sstr = "SELECT ProductID, ProductName, Price, ProductDescription " +
"FROM Products";
using (OdbcConnection dbcon = new OdbcConnection(constr))
{
using (OdbcDataAdapter da = new OdbcDataAdapter(sstr, dbcon))
using (DataTable dt = new DataTable())
{
// SELECT実行
da.Fill(dt);
foreach (DataRow dr in dt.Rows)
{
Console.WriteLine("ID:{0} NAME:{1} PRICE:{2} DESCRIPTION:{3}", dr[0], dr[1], dr[2], dr[3]);
}
}
}
}
}
}
/*
* ビルド:
*
* gmcs select1.cs -r:System.Data.dll
*
* 実行:
*
* mono select1.exe
*
*/
$ mono select1.exe ID:0 NAME:DB2 PRICE: DESCRIPTION:DB2 Express-C 9.5.2 ID:1 NAME:Clamp PRICE:12.48 DESCRIPTION:Workbench clamp ID:50 NAME:Flat Head Screwdriver PRICE:3.17 DESCRIPTION:Flat head ID:75 NAME:Tire Bar PRICE: DESCRIPTION:Tool for changing tires. ID:3000 NAME:3mm Bracket PRICE:0.52 DESCRIPTION:
動作確認
「System.Data.Odbc」はデータプロバイダファクトリ対応クラスを実装しています。そこで、各DBMSの動作確認で使用したコード(C#)を試しましたが、「Firebird編」と同様の追加だけでなく、SQL文内のパラメータの使用において、次の対応が必要でした。
- SQL文内で使用できるのは位置指定パラメータ「?」のみ(名前付きパラメータは不可)
- パラメータのDbTypeプロパティの明示的な指定
- パラメータのSourceVersionプロパティの明示的な指定
また、パラメータのDbTypeプロパティに「DbType.Decimal」を設定すると、
ERROR [HY104] [unixODBC][IBM][CLI Driver] CLI0143E 精度の値が無効です。 SQLSTATE=HY104
という例外が発生しました。そこで、Monoから提供されているSystem.Data.Odbcを調べてみると、DB2 CLI/ODBCドライバを使用する上で、多少の不具合があることが分かりました。
これまで動作確認を行った他のDBMSは、専用のデータプロバイダを使用できるので、System.Data.OdbcをDB2専用と割り切ると、以下の様な修正対応が考えられます。
System.Data.Odbcの修正
上記例外は、パラメータに値をバインドする際に発生しています。原因は、精度(「Price decimal(7, 2)」でいうところの有効桁数7)に対応する引数に「0」を渡してるからで、「OdbcParameter.cs」に以下の修正することで対応が可能です。
...
internal void Bind (OdbcCommand command, IntPtr hstmt, int ParamNum)
{
...
/* 修正前
ret = libodbc.SQLBindParameter (hstmt, (ushort) ParamNum, (short) paramdir,
_typeMap.NativeType, _typeMap.SqlType, Convert.ToUInt32 (Size),
0, (IntPtr) _nativeBuffer, 0, _cbLengthInd);
*/
// 修正後
if (DbType == DbType.Decimal) {
if (Precision == 0) {
string s = Convert.ToString (Value);
int l = s.Length;
int i = s.IndexOf (".");
if (i == -1) {
Precision = (byte) l;
}
else {
Precision = (byte) (l - 1);
Scale = (byte) (l - (i + 1));
}
}
ret = libodbc.SQLBindParameter (hstmt, (ushort) ParamNum, (short) paramdir,
_typeMap.NativeType, _typeMap.SqlType, Convert.ToUInt32 (Precision),
(short) Scale, (IntPtr) _nativeBuffer, 0, _cbLengthInd);
}
else {
ret = libodbc.SQLBindParameter (hstmt, (ushort) ParamNum, (short) paramdir,
_typeMap.NativeType, _typeMap.SqlType, Convert.ToUInt32 (Size),
0, (IntPtr) _nativeBuffer, 0, _cbLengthInd);
}
...
}
...
さらに、パラメータに設定する値をDbTypeに応じてC言語データ型に変換しているのですが、DbType.Decimalに対応する変換について、以下の修正が必要です。
...
internal void CopyValue ()
{
...
switch (_typeMap.OdbcType) {
...
case OdbcType.BigInt:
Marshal.WriteInt64 (_nativeBuffer, Convert.ToInt64 (Value));
return;
/* 修正前
case OdbcType.Decimal:
case OdbcType.Numeric:
// for numeric, the buffer is a packed decimal struct.
// ref http://www.it-faq.pl/mskb/181/254.HTM
int [] bits = Decimal.GetBits (Convert.ToDecimal (Value));
buffer = new byte [19]; // ref sqltypes.h
buffer [0] = Precision;
buffer [1] = (byte) ((bits [3] & 0x00FF0000) >> 16); // scale
buffer [2] = (byte) ((bits [3] & 0x80000000) > 0 ? 2 : 1); //sign
Buffer.BlockCopy (bits, 0, buffer, 3, 12); // copy data
for (int j = 16; j < 19; j++) // pad with 0
buffer [j] = 0;
Marshal.Copy (buffer, 0, _nativeBuffer, 19);
return;
*/
case OdbcType.SmallInt:
Marshal.WriteInt16 (_nativeBuffer, Convert.ToInt16 (Value));
return;
case OdbcType.TinyInt:
Marshal.WriteByte (_nativeBuffer, Convert.ToByte (Value));
return;
// 修正後
case OdbcType.Decimal:
case OdbcType.Numeric:
case OdbcType.Char:
case OdbcType.Text:
case OdbcType.VarChar:
buffer = new byte [GetNativeSize ()];
nativeBytes = enc.GetBytes (Convert.ToString (Value));
Array.Copy (nativeBytes, 0, buffer, 0, nativeBytes.Length);
buffer [buffer.Length-1] = (byte) 0;
Marshal.Copy (buffer, 0, _nativeBuffer, buffer.Length);
Marshal.WriteInt32 (_cbLengthInd, -3);
return;
case OdbcType.NChar:
...
}
...
修正前は、SQL_C_NUMERIC構造体を想定した変換を行っていますが、Linux上のDB2 CLI/ODBCドライバではSQL_C_NUMERICは使用できないようです。
その代わり、DB2 CLI/ODBCドライバは、文字C言語データ型:SQL_C_CHAR(例:"123.45\0"等)から10進数SQLデータ型:SQL_DECIMAL、SQL_NUMERIC(decimal(x, y))への変換をサポートしているので、文字列データと同様のC言語データ型変換を行うようにしました。
また、DbType、SourceVersionプロパティの初期値についても修正を行いました(明示的な指定が必要だった部分の緩和)。
...
public OdbcParameter ()
{
_cbLengthInd = new NativeBuffer ();
ParameterName = String.Empty;
IsNullable = false;
SourceColumn = String.Empty;
Direction = ParameterDirection.Input;
/* 修正前
_typeMap = OdbcTypeConverter.GetTypeMap (OdbcType.NVarChar);
*/
// 修正後
_typeMap = OdbcTypeConverter.GetTypeMap (OdbcType.VarChar);
sourceVersion = DataRowVersion.Current;
}
...
主な修正対応は以上です(修正を行ったSystem.Data.Odbc関連の資源をサンプルとして本稿に添付しました)。
