「Delphi6Personalでデータベース」についてとりとめもなく考える

(2004/6/26 くわわっ記)

■まえがき
Delphi6Personalを使ってお手軽にリレーショナルデータベースを扱うことはできないでしょうか。本来、Delphiはデータベースアプリケーションがとてもつくりやすい開発環境です。ただその実は、TDataBaseなどのデータベース用のとても便利なインターフェース(つまりコントロール)が、添付されてくるということであり、ところがそれは有償版のProfessional以上にしか添付されていません。じゃあ、何とかTDataBaseなしでデータベースアプリをつくるにはどうしたらいいか考えてみよう、というのがそもそも発端です。
あと一応、「SQLって何?」とかって言う質問はなしにしてください。私も偉そうにいえるレベルではありませんが、内容的にはその程度の予備知識が前提だってことです。

ここで何かの業務目的がありつつこのページたどりついた皆様、以下の点にご注意ください。
まず、業務目的でDelphi6Personalを使うのはライセンス違反です。いけません。
それとあくまで私的な好奇心がもともとの動機になっていますので、業務向けにはあまりに非効率的な内容となっています。業務に生かすことは費用対効果で考えても時間の無駄ですので、おやめになったほうがいいです。あくまで「どうやればいいんだろう?」「どうなってるんだろう?」という純粋な好奇心に共鳴いただける方を対象といたします。
もう一点、あらかじめ宣言しておきますが、筆者はこのページを書く1年前までリレーショナルデータベースの何たるかも知らなかった全くの素人です。内容に多くを期待することはおやめください。筆者は体系的な学習や経験も積んでおりません。このページ内容の大半は限りなくインチキです。コード実例など自分でお試しになる場合には、かならず自己責任でやってください。どんな危険なマチガイが含まれているか、わかりません。

■目次
ADO編
DAO編
ODBC編

■設定条件
以下の考察では、下記の条件下であることを前提とします。
・OSはWindowsXP(たぶん95以降であれば同じと思います(*1)が、保証はできません。何せテストしてませんので。)
・Delphi6Personalがインストールされてる
・データソースはMicrosoftAccessに標準添付されてる'NorthWind.mdb'を使う(*2)

*1 なぜかWindows標準のはずのデータベース機能(後述するADO,DAO,ODBCなど)が動かないときもあります。
Windowsのバージョンの違いとかで、Microsoft Jet データベース エンジンがインストールされてないってことのようです。
こういう場合にはマイクロソフトのサイトにあるMDAC_TYP.EXEというのを使います。
このファイルはマイクロソフトの提唱技術 MDAC(Microsoft Data Access Components)そのもので、ADOやら後述するODBCやら、とにかくWindowsからデータベースにアクセスする時に必要になりそうなものが色々パックされて提供されてるものです。
ただMDACにはバージョンが色々あって、ここで対象にしてるJetが含まれているのは2.5までみたいです。(最新は2.8)
うまく動かない人はまず
マイクロソフトのダウンロードセンタに行って、”MDAC”で検索し2.5を探してみてください。

*2 'NorthWind.mdb'を使うのは、データソースとしてMDBファイルがたぶんもっとも手軽に用意できそうだからってだけです。
Accessをお持ちでない方はどこかで(どこだ?)何かのMDBファイルを見つけてきて代わりに使ってください。
もし、MicosoftAccessが手元にないときにはMDBファイルをCSVファイルから作成できるありがたいソフトウェア(フリー)があります。
みっちーのデータベースメンテ for Access

■ADO編
のっけからなんですが「ADOとは?」という問いにはまだ満足に答えられません。未熟者です。
それでも一応書くと、、、
最近のWindowsにはデータベースにアクセスする機能が搭載されていて、そういうのは過去にもODBCとかDAOとかがあったんだけど、今はADO(ActiveX Data Object)っていうのが流行りっていうかMS推奨。
過去のODBCやDAOも相変わらず使えるんだけど、このADOってのは、それらとの翻訳係になってくれて、インターフェースを統一して一通りのリレーショナルデータベース操作ができる。
いわゆるActiveXによるオブジェクト実装になっていて、何でもかんでもやれメソッドだプロパティだ、で記述する。
と言った感じでしょうか。
↑もうすでに思いっきしまちがってるかも知れません。ご指摘いただいても結構ですが、むしろ阿呆の言ってることと思って、有識者のみなさんは無視を、初心者のみなさんは最初から疑いの目を持ってお読みください。それから(ここ重要)間違っても私に「〜はどうしたらできますか」などの質問を送ってこないように。聞く相手を間違えてます。

【やり方】
まず、いきなりその方法です。以下の記述をお読みください。これだけですぐに実際のプログラミングに入れます。断っておきますが私のオリジナルではありません。元は「2ちゃんねる」上での見知らぬどなたか同士のやり取りで、発言者ご本人がわかれば掲載許諾をいただきたいところなのですが、かの匿名掲示板でのことですので、とりあえず無断で引用させていただいています。もし身に覚えのある方がいらっしゃっればご一報ください。(と言ってもご本人かどうか確かめようもありませんが)なお当時とは状況の変わったところもありますので、若干補足させていただいています。
0.Delphi(のIDE)を起動する
1.メニューのプロジェクト→タイプライブラリの取り込みを選ぶ
2.一覧から、Microsoft ActiveX Data Objects 2.5 Libraryを選ぶ
(注:Jetが使えるのは2.5まで、それ以降のバージョンを取り込んで動いたように見えてるのはあなたがラッキーだったから。
一覧に2.5が見つからない人はまずMDAC_TYP.EXEを探しに行こう→下記「なぜかADOが動かないときには」参照)
3.インストールボタンを押す
4.パッケージ dclusr.bplは再構築されますと表示されるので、はいを押す
5.これでADOがコンポーネントに登録される
6.ActiveXタブからConnectionコンポーネントをフォームへ乗せる
7.ボタンを乗せて、そのイベントプロシージャを(下記サンプルコードのように)記述する
以上

【サンプルコード】
1.SQL文の実行
procedure TForm1.Button1Click(Sender: TObject);
var
RcA: OleVariant;
RcSt: Recordset;
begin
 Connection1.Open('Provider=Microsoft.Jet.OLEDB.4.0;
  Data Source=NorthWind.mdb','','',adConnectUnspecified); //(1)
 try
  RcSt := Connection1.Execute('SELECT * FROM 運送会社', RcA, 0); //(2)
  while not RcSt.EOF do begin //(3)
   ShowMessage(RcSt.Fields['運送会社'].Value); //(4)
   RcSt.MoveNext; //(5)
  end;
 finally
  RcSt.Close; //(6)
  Connection1.Close; //(7)
 end;
end; 
解説
mdbファイル'NorthWind.mdb'のテーブル'運送会社'のフィールド'運送会社'を表示させてます。ここでは【やり方】のコーナーにそのまま対応できるようにButton1をクリックする時のイベントプロシージャーとして書いてありますが、この後のサンプルコードは全て単独のプロシージャーないしファンクションとして書きますのでご注意を。動作としてはごくありふれた値の読み出しをするものですが、大抵はこの中のSQLを直すだけで対応できると思います。
(1) MDBファイルをオープン(接続=Connectする、と表現します)。ここでは同じディレクトリにあるNorthWind.mdbに接続。このData Source=のところをフルパスで書けば任意の場所のMDBファイルを読めます。Connection1オブジェクトは上記コード内に定義が見当たりませんが、フォームにConnectionコンポーネントを載せた時点で、TForm1内に定義されてるはずです。
(2) SQL文を実行して結果をレコードセットオブジェクトRcStに格納
(3) レコードセットの末尾までループ
(4) '運送会社'フィールドを読んで値を表示。RcSt.Fields[フィールド名]はRcSt.Get_Item(フィールド名)とも書けます。
(5) 次のレコードに移動
(6) 使い終わったレコードセットを破棄
(7) 接続をクローズ。(=MDBファイルを閉じる) 
なお、このままコンパイルすると「警告: 定数式が範囲を越えました」という警告が出ます。これは(1)で第4引数がIntegerとして定義されてるのにadConnectUnspecifiedは$ffffffffと定義されていて、見かけ上範囲を超えてしまうからです(Integerは-$80000000〜$7fffffff)。もちろん'-1'と定義してやれば実際の値としては同じだし、警告も出なくなりますが、それだと定義ファイル側(ADODB_TLB.PAS)をいじらないといけないのでいやらしいです。ここは簡単にInteger(adConnectUnspecified)と型キャストすればいいでしょう。もっとも対処しなくても別に平気なんですが。(無効値の時は結局デフォルトのadConnectUnspecifiedになっちゃうから)

変更系SQLの場合は
       :
 Connection1.Open('Provider=Microsoft.Jet.OLEDB.4.0;Data Source=NorthWind.mdb','','',adConnectUnspecified);
 Connection1.BeginTrans; //なくても平気みたい...
 Connection1.Execute('INSERT INTO 運送会社 VALUES(4,''ダックスフント'',''(03) 3123-45xx'')', RcA, 0);
 Connection1.CommitTrans; //これもなくても...
 Connection1.Close;
          :
となります。(レコードセットは格別使わない)AccessのMDBの場合、競合防止のトランザクション命令はたぶん無意味なんだろうと思います。なくても動作に支障はないみたいでした。ロールバックするときには効果があるのかも(未確認です)。

ADOそのものの使い方が知りたいときには
実際にプログラミングする時には、さらにADOの仕様がわからないとどうにもならないでしょう。その際に
MSDNのADO APIレファレンス
・タイプライブラリ取り込み時にできるADODB_TLB.pas
の2つを参照すれば、手元にマニュアルがなくても大体のことがわかります。
前者は各メソッドやプロパティの意味が、後者からはそれらのdelphiでの書き方がわかります。文法エラーが出たら後者を見てみましょう。

2.全テーブル名の取得
procedure DispAllTableNameADO();
var
Connection2:TConnection;
RcSt: Recordset;
begin
 Connection2 := TConnection.Create(nil);
 Connection2.Open('Provider=Microsoft.Jet.OLEDB.4.0;
  Data Source=Northwind.mdb','','',adConnectUnspecified);
 try
  RcSt := Connection2.OpenSchema(adSchemaTables); //(1)
  while not RcSt.EOF do begin
   showmessage(RcSt.Fields['TABLE_NAME'].Value); //(2)
   RcSt.MoveNext;
  end;
 finally
  RcSt.Close;
  Connection2.Close;
  Connection2.Free;
 end;
end; 
解説
SQLそのものにはテーブル名を取得する、という構文がないので、テーブル名を取得するときは何らかのSQLより踏み込んだことをしないとできないはずです。それをADOでやってみました。他の方法でも同じ事を試みてますので比較してみてください。
実行ファイルと同じディレクトリに置かれたNorthwind.mdbの中の全テーブル名をOKを押すたび順番に表示するコードになってます。
このコードではフォームにConnectionコンポーネントを載せてない場合を想定して、あらためてConnection2オブジェクトを生成させてます。
前半は定石の通りだが、後半レコードセットを取得するときOpenSchemaというのを使うのがキモ。このOpenSchemaから色々なシステム上のデータにアクセスできます。
実際に実行してみるとわかりますが、TABLE_NAMEというキーワードで引っかかってくるのは、純粋なテーブルだけでなく、クエリも含まれてます。クエリは全てではなく、どうやらテーブル表示されるものだけ、のようです。純粋なテーブルだけを抜き出したい時には後述のDAOを使った方法のほうがいいみたいです。
(1)OpenSchemaメソッドを使ってファイル情報を入れたレコードセットを取得。
(2)ファイル情報レコードの中からテーブル名フィールド'TABLE_NAME'を読み出す。
蛇足 MsysObjectsは使えない
MDBファイルをオペレートする話によくMSysObjectsというテーブルを使うといい、という話が出てきます。このMSysObjectsなどはシステムテーブルと呼ばれて、全テーブルとその属性とか処理上有用な情報が入ってます。しかし、このMSysObjectsはユーザの権限のうち「データの読み取り」がデフォルトで不可になってる。
なので今回みたいな目的で使おうと思ったら、この属性をあらかじめAccessなどで変更しておいてやる必要があります。しかも一回読み取りOKにしてやると、なぜか元に戻らなくなったり、使い方が難しくて色々気を使うテーブルみたいです。うーん、あまり有用でないですね。AccessVBAから使うと、そういうこともなく結構便利みたいなんですが...。 

3.バージョン2.0以降の機能を使うには
procedure PrintRow();
var
RcSt: Recordset20;
strCnct: WideString;
begin
 RcSt := RecordSet20(CoRecordSet.Create); //(1)
 RcSt.Open('SELECT * FROM 社員','Provider=Microsoft.Jet.OLEDB.4.0;
  Data Source=Northwind.mdb'
             ,adOpenForwardOnly,adLockReadOnly,adConnectUnspecified); //(2)
 try
  while not RcSt.EOF do begin
   ShowMessage(RcSt.GetString(adClipString,1,';','','')); //(3)
  end;
 finally
  RcSt.Close;
 end;
end; 
解説
ADOにはいくつかのバージョンがあります。前述のMDACのバージョンと対応関係がありますが、どれとどれが対応してるかはよく知りません。
こうしてプログラムを組んでるときは暗黙でADO1.5という古いバージョンを使ってることになってます。だけどADO2.0とかADO2.1とかの新しいバージョンにしかないような機能もあって、例えばここで使ってるGetStringというメソッドもADO2.0以降のレコードセットにしか用意されてないので、ただ普通にコードを書くと「そんなメソッドないよ」とDelphiに怒られてしまいます。
上記はそんな時にレコードセットのバージョンを強引にADO2.0として扱ってしまうやり方です。オープンの仕方なんかも微妙に違うので注意してください。今は大抵のPCでADOは最新になってますのでこれでちゃんと結果が返ってきますが、古いADOのPCで動かすと問題がおきるでしょう。本来であればまずADOのバージョンを確かめるコードを入れて使うべきでしょう。
(1)ADO2.0のレコードセットに型キャスト
(2)ConnectとOpenを一度に実行する
(3)1番目のフィールドを「文字列として」読み出します。なお、GetString自身で次のRecordSetを読むのでこのループにはMoveNextは不要です。


■DAO編
DAO(Data Access Object)もADO同様Microsoft Jet データベース エンジンを使ってデータベースにアクセスする方法なんですが、ADOとの違いは、よりシステム寄り、というか細かいとこまで指定できる、逆に言うと汎用性(つまり他のSQLServerとか他のDBMSを使う場合)に欠ける、ということらしいです。ADOのほうが新しくて、MSとしては「いまさら何でもかんでもAccessでもないでしょう。より高度なDBMS使いましょうよ。そのためになるべくADOで書いてね」ってとこみたいです。だけど相手がMDBファイルの場合には当然、より細かく指定できるDAOでなければできないケースってのがあるわけで、そういうケースに限定するならDAOでもいいのかも。私はどっちでもいいのですが。
(参考)MSDN JAPANのDAOについての解説箇所

【やり方】
ADOと同じようにタイプライブラリの取り込み(Microsoft DAO xxxx)をしてやります。xxxxは何種類かあるはずで、どれがいいのかよくわかりませんが"2.5/3.51 Compatibility Library(Version3.5)" あたりでしょうか。途中まで順調に行くのですが、コンパイルのところでエラーが22個くらい出てその先に進めなくなるでしょう。原因は"読み込み専用プロパティに書き込むことはできません"ってことで、タイプライブラリを取り込んでできるDAO_TLB.pasの中でDefaultInterfaceというプロパティに何か値をセットしようとする箇所で発生してます。ですのでこれらを全部コメントアウトしてやればエラーは出なくなってコンパイルが通るようになります。"空白以外の行頭がDefaultInterfaceで始まる行の先頭に//を入れる"置換(sedだと"s/\(^[]*\)DefaultInterface/\1\/\/DefaultInterface/"かな。DelphiのIDEでやる場合は各自工夫してみてください。正規表現が完全サポートでないので、ちょっとひねりと妥協が必要と思います。)をしましょう。それでいいのか。って?まあ理想にはほど遠いですが、それらはすべて.Set〜っていうDefaultInterfaceの何かのプロパティに値をセットするメソッドの本体ですので、それらのプロパティさえ使わなければいいでしょう。プロパティに値をセットしようとしてはいけません。あくまでDefaultInterfaceのプロパティは読むだけです。
あとComObjユニットをUseしておきます。

リファレンスは
http://www.accessclub.jp/dao/
などをどうぞ。コードはVBAだけど使い方を調べるには十分と思います。

【サンプルコード】
1.SQL文の実行
uses
 ComObj,Variants;
const
dbOpenSnapshot = 4;
dbOpenDynaset = 2;

procedure DispFieldDAO();
var
 i:integer;
 objDBEngine  :  variant;
 objDatabase  :  variant;
 objRecordset :  variant;
begin
 objDBEngine := CreateOleObject('DAO.DBEngine.36'); //(1)
 objDatabase := objDBEngine.Workspaces[0].OpenDatabase('Northwind.mdb',False, False); //(2)
 objRecordset := objDatabase.OpenRecordset('SELECT * FROM 運送会社',dbOpenSnapshot); //(3)
 while not objRecordset.EOF do begin //(4)
  showmessage(objRecordset.Fields['運送会社'].Value); //(5)
  objRecordset.MoveNext; //(6)
 end;
 objRecordset.Close; //(7)
 objDatabase.Close; //(8)
end;
解説
ADOの1のサンプルコードをDAOでやるとこうなる、って言う例です。ほんとはSQLなんか使わなくてもいろんなDB操作ができるように関数がとりそろえられてるんで、そっち使うべきかも知れませんが。
(1)のクラス文字列が「おまじない」なのが、不安と言えば不安で、他のシステム(=この場合パソコン)でも通用するかどうか保証がないです。実際、Web上で例を探すとここが単に'DAO.DBEngine'となってるケースもありますが、私のシステムでは'.36'をつけないと動作しませんでした。
(1)DAOオブジェクトの生成
(2)MDBファイルのオープン
(3)SQL文を実行し結果をレコードセットオブジェクトに格納
(4)レコードセットがEOFを返すまでループ
(5)レコードセットの'運送会社'フィールドを読んで表示
(6)次のレコードに移動
(7)使い終わったレコードセットの破棄
(8)MDBファイルを閉じる
変更系SQLの場合は
var
             :
objWorkSpace :  variant;
begin
             :
 objWorkSpace := objDBEngine.Workspaces[0];
 objDatabase := objWorkSpace.OpenDatabase('Northwind.mdb',False, False);
             :
 objWorkSpace.BeginTrans;
 objDatabase.Execute('INSERT INTO 運送会社 VALUES(4,''ダックスフント'',''(03) 3123-45xx'')');
 objWorkSpace.CommitTrans;
             :
となります。ADOの時同様レコードセットは使いません。トランザクション系のためにWorkSpaceなるものを使うことになってますが、ADO同様無くても動作に支障はありませんでした。(もちろんMDBの場合は、ですが)

2.全テーブル名の取得
uses
 ComObj,Variants;

procedure DispAllTableNameDAO();
var
i:integer;
objDBEngine  :  variant;
objDatabase  :  variant;
begin
 objDBEngine := CreateOleObject('DAO.DBEngine.36');
 objDatabase := objDBEngine.Workspaces[0].OpenDatabase('Northwind.mdb',False, False);
 for i :=0 to objDatabase.TableDefs.Count - 1 do begin //(1)
  if objDatabase.TableDefs[i].Attributes = 0 then begin //(2)
   showmessage(objDatabase.TableDefs[i].Name);
  end;
 end;
 objDatabase.Close;
end;
解説
前述のADOでもやった「全テーブル名」の取得のDAO版。TableDefsプロパティのAttributesプロパティが0のものを拾ってます。実際に動かして見るとわかるけど、このページのサンプルの中では、これだけが「いわゆるテーブル」だけを忠実に表示してくれます。他のADOやODBCではだめと判断してはいけません。工夫次第ではうまく行くかも知れません。あくまで未熟な小生がやったらこうなったというだけです。
(1)開いたMDBファイルを表すデータベースオブジェクトのTableDefsコレクション?の個数文だけループ
(2)TableDefsコレクションのNameプロパティを表示

■ODBC編
Delphiでプログラムする時は、全体的にADOやDAOに較べて、事前に必要ないろんな定義が多くて準備が面倒です。これはADOのほうが(ActiveXなので?)タイプライブラリの取り込みができて、楽ができてるからに過ぎないというのが本当でしょう。C、特にVisualC++などMicrosoft製品でやる場合にはこのあたりの定義や宣言をまとめたsql.hがついてくるので、何も手間がかからないようなのですが、Delphiでやろうとするとその辺は自助努力ってことになるみたいです。
それらの定義について調べたい場合は
米国のMSサイトにあるODBCのAPIレファレンス(英語)
 日本のMSサイトでは読めません。MSとしても「いまさらODBCなんか使わないでよ」ってとこなんでしょう。
FreePascalプロジェクト(肝心なとこはほぼ英語)
 ここには関数宣言そのものずばりがPascalコードで置かれています。
  後述のサンプルコードのとこでもう少し詳しく書きましょう。
などが有用と思いました。
それと、そもそもPCに何らかのODBCドライバが入ってないと使えません。大抵は最新版が入ってるもんですが、動かない場合にはコントロールパネルの(XPの場合「管理ツール」の)「データ ソース (ODBC)」を確認してみてください。"Microsoft Access Driver (*.mdb)"という項目があればいいんですが...。だめな場合はやっぱり前述のMDAC_TYP.EXEを使います。

他にもODBCに関する参考資料は色々なところにころがってますが、
ばぁばのODBC実験室
書籍ですが「SQLポケットリファレンス(技術評論社)」
はとても参考になりました。っていうか私のサンプルコードはほとんどそこのパクリです。(一応 Pascalに書き換えてますが...)
あとDelphiのコンポーネントとしてODBC Access for delphi(ODBCCall)というのがあります(個人使用に限り無償)。これを使えば(ちょっとブラックボックスなんで勉強としてはナニですが)Personalでもオブジェクト指向感覚でODBCが使えます。
なおODBCはOpen DataBase Connectivityの略だそうです。

【やり方】
DAO同様、Delphi側に調整はいりませんが、その代わり自分で書くコードにちょっと長めの前準備が必要になります。具体的には下のサンプルコードを読んでください。

【サンプルコード】
1.SQL文の実行
uses
StrUtils,SysUtils,Classes,Windows,Dialogs,ComObj;
//定義宣言部分(下記のコード生成に必要なものだけ)
type
 SQLHANDLE    = Pointer;
 SQLSMALLINT  = SHORT;
 SQLINTEGER   = LongInt;
 PSQLINTEGER   = ^SQLINTEGER;
 PSQLHANDLE   = ^SQLHANDLE;
 SQLHENV      = SQLHANDLE;
 SQLHDBC      = SQLHANDLE;
 SQLHWND      = SQLHANDLE;
 SQLHSTMT     = SQLHANDLE;
 SQLRETURN    = SQLSMALLINT;
 SQLCHAR      = UCHAR;
 PSQLCHAR     = ^SQLCHAR;
 SQLPOINTER   = Pointer;
 PSQLSMALLINT = ^SQLSMALLINT;

function SQLAllocHandle(HandleType: SQLSMALLINT; InputHandle: SQLHANDLE;OutputHandle: PSQLHANDLE): SQLRETURN; stdcall; external 'odbc32.dll' name 'SQLAllocHandle';
function SQLDisconnect(ConnectionHandle: SQLHDBC): SQLRETURN;stdcall; external 'odbc32.dll' name 'SQLDisconnect';
function SQLFreeHandle(HandleType: SQLSMALLINT; Handle: SQLHANDLE): SQLRETURN; stdcall;external  'odbc32.dll' name 'SQLFreeHandle';
function SQLDriverConnect(ConnectionHandle: SQLHDBC;WindowHandle: SQLHWND;InConnectionString: PSQLCHAR;StringLength1: SQLSMALLINT;OutConnectionString: PSQLCHAR;BufferLength: SQLSMALLINT;StringLength2Ptr: PSQLSMALLINT;DriverCompletion: SQLSMALLINT): SQLRETURN;stdcall; external 'odbc32.dll' name 'SQLDriverConnect';
function SQLAllocEnv(phstmt: PSQLHANDLE): SQLRETURN;stdcall; external 'odbc32.dll' name 'SQLAllocEnv';
function SQLFreeEnv(phstmt: PSQLHANDLE): SQLRETURN;stdcall; external 'odbc32.dll' name 'SQLFreeEnv';
function SQLAllocConnect(hdbc: SQLHDBC;phenv: PSQLHANDLE): SQLRETURN;stdcall; external 'odbc32.dll' name 'SQLAllocConnect';
function SQLFreeConnect(phenv: PSQLHANDLE): SQLRETURN;stdcall; external 'odbc32.dll' name 'SQLFreeConnect';
function SQLFetch(hstmt: SQLHDBC): SQLRETURN;stdcall; external 'odbc32.dll' name 'SQLFetch';
function SQLGetData(hstmt: SQLHDBC;num: SQLSMALLINT;param_type: SQLSMALLINT;Buffer: PCHAR;Buffer_len: SQLSMALLINT;Buffer_Len2Ptr: PSQLINTEGER): SQLRETURN;stdcall; external 'odbc32.dll' name 'SQLGetData';
function SQLAllocStmt(hstmt: SQLHDBC;phstmt: PSQLHANDLE): SQLRETURN;stdcall; external 'odbc32.dll' name 'SQLAllocStmt';
function SQLFreeStmt(phstmt: PSQLHANDLE;TranCompletion: SQLSMALLINT): SQLRETURN;stdcall; external 'odbc32.dll' name 'SQLFreeStmt';
function SQLExecDirect(hstmt: SQLHDBC;Buffer: PCHAR;Buffer_len: SQLSMALLINT): SQLRETURN;stdcall; external 'odbc32.dll' name 'SQLExecDirect';
function SQLEndTran(endtran: SQLSMALLINT;henv: SQLHENV;TranCompletion: SQLSMALLINT): SQLRETURN;stdcall; external 'odbc32.dll' name 'SQLEndTran';

const
SQL_HANDLE_ENV        = 1;
SQL_HANDLE_DBC        = 2;
SQL_SUCCESS           = 0;
SQL_ERROR             = -1;
SQL_NTS               = -3;
SQL_DRIVER_NOPROMPT = 0; //
SQL_C_CHAR = 1;
SQL_NO_DATA = 100;
SQL_COMMIT = 0;
SQL_DROP = 1;

//実際のコード部分
procedure SampleODBC();
const
MAXstrCnO = 4824;
MAXstrDB = 200;
var
hENV: SQLHENV;
hDBC: SQLHDBC;
hSTMT: SQLHSTMT;
codeRet: SQLRETURN;
strCnI: string;
strCnO: array[0..(MAXstrCnO-1)] of Char;
lenCnO: SQLINTEGER;
strSQL:string;
strDB: array[0..(MAXstrDB-1)] of Char;
lenDB: SQLINTEGER;
begin
 try
  SQLAllocEnv(@hENV);  //(1)
  SQLAllocConnect(hENV,@hDBC);  //(2)
  strCnI := 'Driver={Microsoft Access Driver (*.mdb)};DBQ=Northwind.mdb';
    SQLDriverConnect(hDBC, Nil,@strCnI[1],SQL_NTS,
    @strCnO,MAXstrCnO,@lenCnO,SQL_DRIVER_NOPROMPT);  //(3)
  SQLAllocStmt(hDBC,@hSTMT);  //(4)
  strSQL := 'SELECT 運送会社 FROM 運送会社';
    SQLExecDirect(hSTMT,@strSQL[1],SQL_NTS);  //(5)
  while true do begin
   codeRet := SQLFetch(hSTMT);  //(6)
   Case codeRet of
    SQL_NO_DATA:Break;
    SQL_ERROR:Break;
   end;
   SQLGetData(hSTMT,1,SQL_C_CHAR,@strDB,MAXstrDB,@lenDB);  //(7)
   showmessage(string(strDB));
  end;
  SQLEndTran(SQL_HANDLE_ENV,hENV,SQL_COMMIT); //(8)
 finally
  SQLFreeStmt(hSTMT,SQL_DROP);  //(9)
  SQLDisconnect(hDBC);  //(10)
  SQLFreeConnect(hDBC);  //(11)
  SQLFreeEnv(hENV);  //(12)
 end;
end;
解説
ふぅ、長かったですね。ご覧の通り、宣言などの前準備がやたらあります。ここでは最低限必要なものだけ書いてますが、使いたい関数や変数、定数が増えるたびに宣言を追加してやらなければなりません。ズルする方法→後述。あと変数の型宣言が全部ODBC独自の型になっていて(PSQLINTEGERとか)、これらが微妙にトリッキーなもんですから(SQLCHARがUCHAR(=BYTE)型だったり)、型チェックにうるさいDelphiでは引数に渡すとき、ちょっと工夫が必要です。strSQLやstrDBの変数の宣言の仕方や(5),(7)での渡し方を見てください。

(1)  環境ハンドルを確保。よくわかりませんが、色んな環境変数みたいなのを納めておく領域が必要なんでしょう。
(2)  上の環境ハンドルとは別に接続ハンドルを確保する。
     同じくよくわかりませんが、きっと1つの環境ハンドルに複数の接続ハンドルが確保できたりするんでしょう。
(3)  接続ハンドルで接続する。今回みたいにDSNを使わずに直接MDBファイルを指定する場合はSQLDriverConnectを使います。Connectする関数は他にも複数あるけど、それらは用途が違います。第3引数のstrCnは、各種DBMSへつなげるためのおまじないで、その前の行での代入のように'Driver={Microsoft Access Driver (*.mdb)};DBQ=Northwind.mdb'と書きます。もちろん、これはあくまで相手がMDBファイルの場合です。ちなみにDBQはフルパス指定できますが、このようにパスなしで書けば本アプリと同フォルダのMDBを指します。
(4)  ステートメントハンドルを確保する。またまたよくわかりませんが、SQL命令の結果を入れとく領域のようです。
(5)  SQL命令の発行。なお、第3引数はSQLの命令長。ヌルで終わる文字列はSQL_NTSでOK
(6)  SQLFetchでレコードの次のアクセス位置に移動。最初は当然レコード頭にセットされます。
(7)  結果を読む。第3引数は読みたいデータの型指定で、ほんとはMDB内の定義通りに(SQL_C_SMALLINTなど)正しく指定すべきなんだけど、文字列型(SQL_C_CHAR)と決めうちしても平気みたいです(もちろん結果は文字列になるけど)。
(8)  SQL命令のコミット。INSERTやDELETEなどデータの変更を伴う命令に。不要って話もあるようですが。
(9)  (4)の解放
(10) (3)の解放
(11) (2)の解放
(12) (1)の解放

変更系SQLの場合は

          :
 strSQL := 'INSERT INTO 運送会社 VALUES(4,''ダックスフント'',''(03) 3123-45xx'')';
 SQLExecDirect(hSTMT,@strSQL[1],SQL_NTS);
 SQLEndTran(SQL_HANDLE_ENV,hENV,SQL_COMMIT);//なくてもいいみたいです。
          :

となります。ADO,DAO同様、最後のEndTranはなくても動作に支障はありませんでした。

ODBC関連の宣言でズルをするいい方法
前述のFreePascalのサイトにodbc.zipというファイルが置かれてます。この中にあるsql.pp,sqltypes.pp,sqlext.ppは、ODBC関連の宣言集で、かつそのままpascalになってます。ですので拡張子を.pasに直してやればいいのですが、実はそのままではDelphiではコンパイルが通りません。FreePascalはDelphi上位互換を標榜してるようですが、あくまで上位互換であって下位互換性はないからです。いやPascalの文法そのものは特に問題ないようのですが、コンパイラ指令に特殊性があります。要するにより高機能になってるわけですが、この場合には困ります。ですのでそのコンパイラ指令のところを無効にしてやればいいのです。コツとしては、{$SMARTLINK ON}、{$PACKRECORDS C}、{$if (ODBCVER >= 0x0nnn)}(nは数字)と対応する{$endif}をコメントアウトなどで無効にすることと、必要に応じてtypesユニットをuseしてやることです。特に{$if (ODBCVER >= 0x0nnn)}のコメントアウトは場所が多いし、対応する{$endif}を探すのが間違いやすいので工夫しないと苦労するでしょう。私は{$if (ODBCVER >= 0x0nnn)}を全部{$ifdef ODBCVER_0nnn}に置き換えてしまってファイルの頭でODBCVER_0nnnをdefineしてやりました。ほんとはこうして私が直した.pasやコンパイル結果の.dcuを公開しても、個人的には全然かまわないのですが、権利面などが怪しいですし、それはやめておくことにします。


2.全テーブル名の取得
uses
StrUtils,SysUtils,Classes,Windows,Dialogs,ComObj;

//定義宣言部分(下記のコード生成に必要なものだけ)
type
 SQLHANDLE    = Pointer;
 SQLSMALLINT  = SHORT;
 SQLINTEGER   = LongInt;
 PSQLINTEGER   = ^SQLINTEGER;
 PSQLHANDLE   = ^SQLHANDLE;//
 SQLHENV      = SQLHANDLE;
 SQLHDBC      = SQLHANDLE;
 SQLHWND      = SQLHANDLE;//
 SQLHSTMT     = SQLHANDLE;//
 SQLRETURN    = SQLSMALLINT;
 SQLCHAR      = UCHAR;
 PSQLCHAR     = ^SQLCHAR;
 SQLPOINTER   = Pointer;//
 PSQLSMALLINT = ^SQLSMALLINT;

 function SQLTables(StatementHandle:SQLHSTMT; CatalogName:PSQLCHAR; NameLength1:SQLSMALLINT; SchemaName:PSQLCHAR; NameLength2:SQLSMALLINT;TableName:PSQLCHAR; NameLength3:SQLSMALLINT; TableType:PSQLCHAR; NameLength4:SQLSMALLINT):SQLRETURN; stdcall; external 'odbc32.dll';

procedure DispAllTableNameODBC();
const
MAXstrCnO = 4824;
MAXstrDB = 200;
var
hENV: SQLHENV;
hDBC: SQLHDBC;
hSTMT: SQLHSTMT;
codeRet: SQLRETURN;
strCnI: string;
strCnO: array[0..(MAXstrCnO-1)] of Char;
lenCnO: SQLINTEGER;
strSQL:string;
strDB: array[0..(MAXstrDB-1)] of Char;
lenDB: SQLINTEGER;
begin
 try
  SQLAllocEnv(@hENV);
  SQLAllocConnect(hENV,@hDBC);
  strCnI := 'Driver={Microsoft Access Driver (*.mdb)};DBQ=Northwind.mdb';
            SQLDriverConnect(hDBC, Nil,@strCnI[1],SQL_NTS,@strCnO,
              MAXstrCnO,@lenCnO,SQL_DRIVER_NOPROMPT);
  SQLAllocStmt(hDBC,@hSTMT);
  codeRet := SQLTables(hSTMT,nil,0,nil,0,nil,0,nil,0);  //(1)
  while true do begin
   codeRet := SQLFetch(hSTMT);
   Case codeRet of
    SQL_NO_DATA:Break;
    SQL_ERROR:Break;
   end;
   SQLGetData(hSTMT,3,SQL_C_CHAR,@strDB,MAXstrDB,@lenDB);  //(2)
   showmessage(string(strDB));
  end;
 finally
  SQLFreeStmt(hSTMT,SQL_DROP);
  SQLDisconnect(hDBC);
  SQLFreeConnect(hDBC);
  SQLFreeEnv(hENV);
 end;
end;

解説
ほとんど典型パターン通りですが、SQLの発行の代わりに(1)を使います。結果は(2)で読み出しますが3列目にテーブル名が入ってくるので、第2引数に'3'を指定します。実行結果はこれまたADOともDAOとも違ってまして、システムものも含んだテーブルとクエリのほぼすべてが返ってきます。(1)あたりでパラメータ(特に第4〜7引数あたり)を色々いじるともっとよくなるのかも知れません。

3.エラーの診断
uses
StrUtils,SysUtils,Classes,Windows,Dialogs,ComObj;

//定義宣言部分(下記のコード生成に必要なものだけ)
type
 SQLHANDLE    = Pointer;
 SQLSMALLINT  = SHORT;
 SQLINTEGER   = LongInt;
 PSQLINTEGER   = ^SQLINTEGER;
 PSQLHANDLE   = ^SQLHANDLE;//
 SQLHENV      = SQLHANDLE;
 SQLHDBC      = SQLHANDLE;
 SQLHWND      = SQLHANDLE;//
 SQLHSTMT     = SQLHANDLE;//
 SQLRETURN    = SQLSMALLINT;
 SQLCHAR      = UCHAR;
 PSQLCHAR     = ^SQLCHAR;
 SQLPOINTER   = Pointer;//
 PSQLSMALLINT = ^SQLSMALLINT;
function SQLGetDiagRec(HandleType: SQLSMALLINT; Handle: SQLHANDLE;RecNumber: SQLSMALLINT; Sqlstate: PSQLCHAR;NativeErrorPtr: PSQLINTEGER;MessageText: PSQLCHAR;BufferLength: SQLSMALLINT;TextLengthPtr:PSQLSMALLINT): SQLRETURN;stdcall; external 'odbc32.dll' name 'SQLGetDiagRec';

procedure DiagODBC(var hDBC: SQLHDBC);
const
SQLMSG_LEN = 200;
SQLSTATE_LEN = 10;
var
codeRet: SQLRETURN;
strState: array[0..(SQLSTATE_LEN-1)] of CHAR;
strMsg: array[0..(SQLMSG_LEN-1)] of CHAR;
codeErr: SQLSMALLINT;
lenstrMsg:SQLSMALLINT;
begin
 codeRet := SQLGetDiagRec(SQL_HANDLE_DBC,hDBC,1,@strState,@codeErr,@strMsg,SQLMSG_LEN,@lenstrMsg);
 ShowMessage('RC   :' +IntToStr(codeRet) + Chr(10)+Chr(13)+
     'State:' + string(strState) + Chr(10)+Chr(13)+
     'Msg  :' + LeftStr(string(strMsg),lenstrMsg));
end;

解説
一連のODBC関数(SQL〜)はすべてリターンコードを返します。動作に問題がなければSQL_SUCCESS(=0)が返るのですが、何らかの理由でしくじるとSQL_ERRORとかを返します。しかし、リターンコードだけではいったい何が悪かったのか、結構困ります。そんな時は問題箇所の直後でSQLGetDiagRecを呼んでやるとより詳細な情報が得られます。上記のような関数を一個作っておいてデバッグ時に適宜呼んでやるといいでしょう。
呼び方は
  :
codeRet := SQLDriverConnect(hDBC, Nil,@strCnI[1],SQL_NTS,@strCnO,
                    MAXstrCnO,@lenCnO,SQL_DRIVER_NOPROMPT);
if codeRet = SQL_ERROR then DiagODBC(hDBC);//***** For Debug ****
  :
 といった感じです。


続編「本格データベースの場合」についても探求してみる
(未着手。書かないかも知れません。と言うかきっと書かないと思います。なぜかって、もうモチベーションがかなり下がってるから。)
MSDE編
MySQL、Firebird、PostgreSQL、Oracle編

戻る