2011年10月19日水曜日

C++Builder 4 / 6 で MySQL を直に操る ~その2 コーディング編~

C++Builder 4 / 6 で MySQL を直に操る ~その2 コーディング編~



大層な感じになってしまったが、そんなに詳細に説明しないのでご容赦を。



前回、プロジェクトを作成したので、今回は実際にプログラミングして MySQL を動かす。



インクルード


・ ヘッダーまたはソースファイルに以下を追加

#include <mysql.h>


これは基本で、他にも色々あるので確認すること。

※なお、確認には libmysql.def を閲覧することをお勧めするが、無い場合は impdef.exe を使用して作成できる。

impdef "[適当なディレクトリ]\libmysql.def" "$(MySQL)\bin\libmysql.dll"
上記を実行すると、libmysql.def が出力される。

この def ファイルには libmysql.dll に登録されている関数の一覧が格納される。普通のテキストなのでメモ帳などで開ける。


・ この後のコンパイル時に、関数云々のエラーが出て叱られる場合は、

#include <mysql.h> 宣言の前に、以下を追加してみること。

#include <windows.h> または #include <winsock.h> または両方


ありそうな失敗


ここで、コンパイル時や実行時にありそうな失敗をいくつか。

・ libmysql.dll のエラー
実行すると、libmysql.dll の何とかエラーと表示され、coff2omf を使用してはどうかと勧められる。
大きなお世話なんだが、libmysql.dll にパスが通ってない可能性がある。またバージョン違い(?)の可能性も否定できない。

そもそも、coff2omf は失敗する可能性があって、使用は勧めない (DLL 自体を変換するため注意が必要)

 また、取り敢えずアプリケーションパスに置けばコンパイラを通ることもあるが、根本的な解決にはなってないので、前回の設定編をもう一度チェックされたし。
・ コンパイラは通るが、初期化関数 mysql_init() でアドレス違反のエラーが出る。
先ず最初に、既知の問題である初期化関数呼び出しのコーディングを確認する。
MYSQL *mysql = mysql_init(NULL);
引数は必ず NULL にする。つまり、常に新しいオブジェクトにメモリを割り当てる (既存のオブジェクトを初期化しない) MYSQL 変数をグローバルにしている場合でも、mysql_close(MYSQL*) でメモリを解放してから、再度初期化する。



これ以外では、やはり libmysql.dll のエラーで、バージョン違いの可能性が高い。

MySQL のインストール/アンインストールを繰り返していると、以前のファイルが削除しきれなかったりする。

アンインストールする場合は、必ず先にサービスの停止・削除を実行する (サービスとしてインストールしている場合)

削除方法は、コマンドプロンプトで以下を実行する (もちろんサービスを停止してから)

$(MySQL)\bin> mysqld -remove mysql

その後、アプリケーションの追加と削除で MySQL を削除する。 MySQL 関連のフォルダを全て削除 (もちろんだが、データベースのバックアップはお忘れ無く)

そしてここが一番忘れやすいところで、$(BCB)\lib\libmysql.lib の削除と、Builder で設定しているインクルードパスなどの確認 (libmysql.dll をリンクしている箇所)

それと、libmysql.lib と .dll を他の場所に (アプリフォルダとか) コピーしている場合はそれらの削除。

以上を確実に実行してから再インストールを行う。

その後、前回の設定編をもう一度実行するべし!

・ 初期化関数は通るが、接続関数 mysql_real_connect() で接続できない

先ず最初にコーディングを確認する
if (mysql_real_connect(mysql,host,user,passwd,db,port,unix_socket,client_flg) == NULL)
当然だが、ホスト (localhost ?)、ユーザ (root ?)、パスワード、データベース名、ポート番号が間違っていないかチェックする。(データベース名は NULL でも構わない) あと、最後の2つの引数は NULL と 0 にする

間違っていなければ、ファイアウォールが怪しい。

つまり、ポート番号 3306 (既定値。変更しているならその番号) がブロックされている可能性が高い。


当然だが、MySQL はホストを localhost にしていて、スタンドアロンで動かしていても、ポートは開いていないとダメ。



実際のコーディングでの注意


それでは、実際のコーディングの際の注意点をいくつか。

・ C API の既存の関数を使用するのはなるべく避ける
例えば、mysql_create_db(MYSQL *mysql, const char *db) 関数 リファレンスにもあるように、この関数は現在廃止されている。 何故廃止されたのかを考える。

リファレンスには、「廃止されている」の後にこう続く、

”代わりに、mysql_query() を使用して CREATE DATABASE ステートメントを発行してください”

つまり、通常コンソールで打ち込むクエリをプログラムから発行しろと言っている。


SQL 文をせっせと作っていた昔を思い出すではないか!

後戻りのようだが、実はこれが一番速くて賢いやり方なのだ!

賢いとは、どの言語でも、他のクエリ型データベースでも、同等の事が可能だし移植もそんなに手間は掛からないだろう。

現在私は、PHP を経て Java、C# で MySQL に接続しているが、例えば、Connector/Net でもコマンド発行メソッドがあり、戻り値をチェックしてユーザに提示するといった仕組みは、今回と何ら変わらない。

.NET には DataTable というメモリに保持しておくような便利なものがあったりするが、正に付加価値であって基本的な部分は全くと言って良いほど変わらない。

ただし、このクエリ発行メソッドの他に、レスポンスを受け取るメソッド (仕組み) にはそれぞれの言語間で違いがあるので、そこに留意しなければならない。

C API なら、

mysql_init()、mysql_real_connect()、mysql_close() の初期化、接続、解放関数、

mysql_use_result()、mysql_store_result() のクエリ結果を受け取る関数、

mysql_fetch_row()、mysql_fetch_field() のようなクエリ結果を1つずつ受け取る関数

しかし、これらくらいのものだ...

・ データベース物のプログラミング鉄則を守る
何やら怪しげだが、

これはデータベースに限ったことではないが、例えばループ内には出来るだけ条件文を入れるな!というのがある。

例えばどうしてもフィールド値を毎回チェックしないといけないような状況下では仕方ないと思われがちだが、そのような場合は逆に、テーブルの構造を再検討する。

MySQL には、not null、unique、auto_increment などのフィールドオプションの他、enum、set といった列挙体フィールドがある。差し支えなければこれらを使う。

また、フィールド値のチェックは入力時 (insert、update) に厳重に行うようにする。

そうすれば、MySQL の「えっ!?もう?」という驚異的な速さを実感できるだろう。(下ネタじゃないぞ)



実際のコーディング


それでは、クエリを発行してレスポンスを表示する簡単なアプリのソースから核心部分を。

MySQL への接続や切断などは各自でやってみる。

流れ的には、

接続する際に必要なパラメータ (ホストやユーザなど) をユーザから受け取るコントロールがあるのが普通か。

接続するための関数を自前で用意し、その中でこれらのパラメータを事前にチェックしてOKなら、



// mysqlConn はグローバル変数 (MYSQL*)



if (mysqlConn)

{

    mysql_close(mysqlConn);

    mysqlConn = NULL;

}



try

{

    mysqlConn = mysql_init(NULL);

}

catch (...)

{

    _stprintf(buf,_T("initialize error: %s"),mysql_error(mysqlConn));

    ShowMessage(buf);

    mysqlConn = NULL;

    return false;

}



if (mysql_real_connect(mysqlConn,host,user,passwd,db,port,NULL,0) == NULL)

{

    _stprintf(buf,_T("connect error: %s"),mysql_error(mysqlConn));

    ShowMessage(buf);

    mysqlConn = NULL;

    return false;

}



mysql_set_character_set(mysqlConn,charset);

return true;



てな感じで。

これは接続する処理だが、最初の方で MYSQL* 型のグローバル変数をチェックして NULL じゃなかったら mysql_close() で切断しているが、これを関数化して中で NULL にするようにするのが常套か。

ちなみに、C / C++ では、MFC はよく知らないが、上記の場合 mysql_close(mysqlConn); でクローズしても、mysqlConn 変数は自動で NULL になったりしてくれない。(笑)

なので必ずプログラマが NULL にする。また、ここではイニシャライズに失敗したり、コネクションに失敗したら NULL にしている。このやり方がイヤなら、接続関数内で接続したらフラグとしてのグローバル変数を立てて、クローズしたら 0 にするというセマフォチックな方法もあるが、もしあなたが毛がフサフサな素人ならお勧めしない。

なお、接続する際に指定する「db」データベースだが、NULL でも構わない。というより、今回のようなコマンド→レスポンスの流れを再現するようなアカデミック(?)なサンプルアプリの場合は NULL の方が自然だろう。

ただし、実際のアプリケーションでは、接続だけするような行為はダメで、必ず目的を持って (例えば、あるテーブルからフィールド値を検索するみたいな) ロジックを走らせるようにする。そして目的を達成したら切断する。



で、フォームにクエリ入力用の Edit (他でいうところの TextBox) と、クエリ発行用の Button と、レスポンス表示用の Memo (ListBox や .NET では MultiLine の TextBox でも可) を貼り付けといて、



それらのコントロールの名前も一応。

Edit が Command_Edit

Button が Command_Btn

Memo が Memo1

とする。そしてクエリ発行用のボタンクリックイベントで、



if (Command_Edit->Text == _T("")) return;



int lp, len, buf_len;

int rec_cnt, field_cnt;

TCHAR command[280];

TCHAR buf[1050];



MYSQL_RES *res;

MYSQL_ROW row;



// ARRAYSIZE マクロは無かったら自前で #define する

// #ifndef

// #define ARRAYSIZE(s) (sizeof(s) / sizeof(s[0]))

// #endif // ARRAYSIZE



buf_len = ARRAYSIZE(buf);



Memo1->Clear();



if (!mysqlConn)

{

    ShowMessage(_T("MySQL に接続していません  "));

    return;

}



if ((int)_tcslen(Command_Edit->Text.c_str()) >= ARRAYSIZE(command))

{

    ShowMessage(_T("クエリ文が長すぎます  "));

    return;

}



_tcscpy(command,Command_Edit->Text.c_str());



// クエリ発行

if (mysql_query(mysqlConn,command) != 0)

{

    _stprintf(buf,_T("query error: %s"),mysql_error(mysqlConn));

    ShowMessage(buf);

    return;

}



// レスポンスを取得

if ((res = mysql_use_result(mysqlConn)) == NULL)

{

    // エラーではない (レスポンスが返ってこないだけ)

    Memo1->Lines->Add(_T("Query OK!")); // ホンマか?

    goto END_COMMAND;

}



rec_cnt = 0; // レコードカウント用 (必要なら)



Memo1->Lines->BeginUpdate(); // 大きなテーブルだとこれをしないと悲惨な目に...



field_cnt = (int)mysql_num_fields(res);



while ((row = mysql_fetch_row(res)) != NULL)

{

    rec_cnt ++;



    len = 0;

    buf[0] = _T('\0');

    for (lp = 0; lp < field_cnt; lp++)

    {

        if (row[lp]) // C だとこれが通用する

        {

            len += (int)_tcslen(row[lp]);

            if (len < (buf_len-1)) _tcscat(buf,row[lp]);

            else break;

        }

        _tcscat(buf,_T("\t"));

        len = (int)_tcslen(buf);

    }



    ((int)_tcslen(buf) > 0) ? buf[_tcslen(buf)-1] = _T('\0') : buf[0] = _T('\0');

    Memo1->Lines->Add(buf);

}



mysql_free_result(res); // 必ずやる!



Memo1->Lines->EndUpdate(); // ここで描画指示を出す





END_COMMAND: ;



Command_Edit->Clear();

Command_Edit->SetFocus(); // フォーカスをセット



実際は、while 文の中ではあんまりゴチャついてはいけない。今回は何が来るか分からないということで...

ポイントは、Connector (ODBC) を使用した時とあまり変わらないところと、フィールド・ループの中の if (row[lp]) という記述。

「.NET で MySQL を ~」でも説明したように、C# だと、たとえインデックスがフィールドカウント内であっても、アクセス違反だと叱られる。IsDBNUll() というマクロ(?)があるのでそれを使用する。

C でOKだったので、ここで随分悩んだが、そういう時は本家サイトに行くのが一番手っ取り早い。ちゃんと書いてあった!「バカ!ハゲ!」って。



という若干言語による違いはあるが、どうでしょう?特に難しいようなところは無かったと思うが?

実際のアプリケーション開発では、データベース固定だろうし、テーブルの動的生成もテンポラリテーブル以外はあまり無いように思う。

要するに殆ど決め打ちで、ベタ書きも可能だということだ。だが実はデータベース物はこのベタ書きが一番速い。

上記のサンプルのように、ループ内で条件を色々書いたりしていると、こういう塵が積もり積もって「え~まだ~?」となる。

上記のループ内では、バッファ変数のサイズをチェックしているが、desc などで前もってテーブルサイズをチェックしておくのも手だ。

お気づきだと思うが可変長のフィールドは無用の長物で、varchar などは使わないようにするのが常套手段だ。また、TEXT や MEMO 項目など昔からあるが、そもそも長文をテーブルに保存するという考え方は止めた方が良いし、BLOB など以ての外だと思う。

そういうのは昔からテキストファイルに保存したり、マルチメディアファイルそのもののパスをテーブルに保存するやり方。遅くなるかもしれないが、これしかないと思う。

0 件のコメント:

コメントを投稿