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 など以ての外だと思う。

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

C++Builder 4 / 6 で MySQL を直に操る ~その1 設定編~

C++Builder 4 / 6 で MySQL を直に操る ~その1 設定編~



今更だが、

C++ で MySQL API でガリガリやっていた頃の覚え書きを載せておく。

C++ と言っても VC++ ではなく、Borland C++Builder 4/6 である。

ODBC は使わず、libmysql.lib を直接参照して、C API でコーディングする。



DLL / LIB 辺りが VC++ と全く違うところだが、コーディングは殆ど同じにできるので、移植可能だが「コードまんま」は到底無理なのでやめましょう。

そんなこんなもあって、VCL はなるべく使わないようにしているが、ところどころ出現するかもなので気をつけましょう。



以上をよく理解してから突入してください。



MySQL はフリーのデータベースにしてはあまりにも出来すぎだ。

「ストレージエンジン」という考え方が既にクールなんだが、そのストレージエンジンも、MyISAM だけならまだしも、トランザクション機能が付いた InnoDB の出現はまさに奇跡と言っても過言ではないかもしれない。

この2つは、「お互いに無いものを持っている」とよく言われるが、その通りだと思う。

どちらのストレージエンジンが良いかという質問は、ぶっきらぼうに「フェラーリとベンツ、どちらが良いですか?」と質問するのと同じくらいベタな質問だということにいい加減気付くべきだと思う。

ケースバイケースの最たるものだ。

この話はまた別の機会に、または詳細なベンチマークを行っているサイトに行って見極めるべし。



私は、Borland C++Builder 1 の頃からのユーザだが、Delphi のアメリカでの衝撃的なデビューはよく知らないが、C++Builder 1 を初めて使ったときの衝撃は今でもよく覚えている。

Borland の初期の Delphi、C++Builder 1 という名器を生んだ人達の多くが、Microsoft に移籍し、.NET や C# が生まれた。

という話を聞くと、何だか鼻高々な気分になったものだ。



それにもかかわらず、C++Builder の VC++ に対するアドバンテージは限りなく「0」に近い...

いや、厳密に言うと、.NET が出てくるまでは結構な位置にあった。

ということは、VC++ に負けたというより、C# や VB.NET に負けたといった方が良いのかもしれない。



.NET や C# を生んだ旧 Borland の人達は「ざまぁみろ!」と言った感じかもしれない。



現に私は、C++Builder は「6」を最後に使っていない。

今は、ご多分に漏れず UNICODE な「2010」や「XE」なんだが、「2010」を持っているがイマイチな使い勝手でして...

使い勝手の悪い一例を挙げると、「コード補完機能」のレスポンスの悪さたるや半端ない! (まぁ伝統的なんだが)

Visual Studio 特に VB.NET 2010 のコード補完機能を一度味わってしまうと、Eclipse でさえ不満に感じるが、Eclipse はフリーなので何とでも言い訳できる。

なので私は昔から Builder のコード補完機能は使わないようにしている。

たかが「コード補完機能」だが、デスクトップアプリケーション程度のツールの生産性を問題にするとき、両方を使ったことのある人ならおそらく10人中10人が VB.NET の方が生産性は高いと答えるだろう。



話が長くなってしまった。

今回は、インストールと C++Builder 用の LIB ファイルの作成とプロジェクト作成方法までを。



一応環境を。



OS:     Windows XP Pro (sp3) / 7 Ultimate
C++Builder:     4 / 6
MySQL:     5.1.x


Builder だけ古風な感じになっているが、今回 Win7 と MySQL 5.1.x で改めて確認したため。



なお、以下のパス文字列内にある $(MySQL) は MySQL をインストールしたルートフォルダ、

$(BCB) は C++Builder がインストールされているルートフォルダを意味する。



また、ファイアウォールソフトが常駐している場合は、念のため以下を検討すること。

ポート単位での例外設定が可能なら、TCP の 3306 (MySQL Server のポート番号で変更した場合はその番号) を送受信許可に設定する。



MySQL のインストール


まずは MySQL のインストールから

MySQL はバイナリでインストールする

・ essential 版は、Typical ではなく Custom を選択して、include ファイルもインストールする。

・ zip 版は、include ファイルを抽出する。



※なお、include ファイルなどのライブラリは、それだけで提供されているが、バージョンは合わせた方が良いので、出来るだけバラバラにインストールするのは避け、バイナリのパッケージ版でインストールする。



libmysql.lib の作成


libmysql.lib はデフォルトで VC 用に最適化されているため、implib.exe を使用して Builder 用に変換する。

・ コマンドプロンプトで以下を入力

   implib "[適当なディレクトリ]\libmysql.lib" "$(MySQL)\bin\libmysql.dll"

  上記を実行して、Builder 用の libmysql.lib を作成する。



・ 上記で作成した libmysql.lib を $(BCB)\lib へコピーする



※作成先を $(MySQL)\lib にしても良いが、その場合は当然上書きになるので注意。



プロジェクトを作成する前に


C++Builder の [プロジェクト] - [オプション] - [ディレクトリ/条件] タブで、

・ インクルードパスに、$(MySQL)\include を追加 (ヘッダファイルリンクのため)

・ インクルードパスに、$(MySQL)\bin を追加 (libmysql.dll をリンクするため (以下の※を参照) )

・ ライブラリパスに、$(MySQL)\lib を追加しない! (Builder 用に変換した libmysql.lib を $(BCB)\lib にコピーしているため)



※ libmysql.dll はアプリケーションパスに置いても良いが (その場合 $(MySQL)\bin を追加しなくても良い)、上記の設定をデフォルトにしておけば、いつでも MySQL アプリケーションを作成できる。



プロジェクトの作成


C++Builder で新規プロジェクトを作成したら、

・ [プロジェクト] - [プロジェクトに追加] で $(BCB)\lib\libmysql.lib を追加する



以上で、C++Builder プロジェクトが作成される。

実際のコーディングは次回に






2011年10月17日月曜日

.NET で MySQL を使う ( MySQLDataReader )

C#なんかでMySQLに接続するのは、検索すれば色々と出てくるので割愛するが、
要するに、MySQL本家のサイトから Connector/Net という ODBC をダウンロード&インストールで接続できるようになる。

んで、実際に接続して表示するプログラムのソースも結構な数が出てくるんだが、
MySQLDataReader クラスの記述がどれも似たようなものばかり??のような気がする。
ホントに気のせいかもしれないので、その時は毎度のごとくご容赦を。

ソースのコネクション部分は割愛するが、ポイントだけ。
MySQL は文字化けの問題が昔から言われているが、その殆どはプログラマなどのユーザの問題だ。例えば、以下の MySQL コマンドをどれほどの人が知っているだろうか。

mysql> status

--------------
mysql   Ver 14.14 Distrib 5.1.51, for Win32 (ia32)
・・・
というバージョンに続いて、様々な情報が表示され、最後の方に、
・・・
Servercharacterset:  sjis
Dbcharacterset:  sjis
Clientcharacterset:  sjis
Conn.characterset:  sjis

という、MySQL サーバー、カレントデータベース、クライアント、コネクションと、実に4つの文字コードが設定されていることがわかる。

つまり、C# などのような現在の .net は UNICODE なので、接続する際に接続文字列に、
charset=utf8;
を加えれば、コネクション単位で、アプリケーション(クライアント)の文字コードに合わせることができる。
まぁ、とは言うものの MySQL サーバー・クライアントともにデフォルトが utf8 で、アプリケーションも utf8 なのに文字化けするという難儀な事態も経験したことがあるが、あのときは元々 sjis で途中から変更して...みたいな感じだったと思うが、結論を言うと、コネクション時に sjis を指定すると何故か直った。
あと、ujis を Windows で使うには気をつけないとハマるよ。

今は、UNICODE 全盛時代に突入したそうなので、昔の膨大な sjis 資産を「えいっ!」と変換したほうが良さそう...あぁ昔の sjis しか扱えなかった(もちろんデフォルトでという意味) Builder が懐かしい。

というわけで、本題に。

// C# ソース(一部)

MySQLDataReader reader = null;
MySQLCommand cmd = new MySQLCommand("desc table_name;", mysqlCon);

try
{
    reader = cmd.ExecuteReader();

    while (reader.Read())
    {
        // この部分
        Console.WriteLine(reader.GetString(0) + ", " + reader.GetString(2));
    }
}
catch(MySqlException ex)
{
    Console.WriteLine(ex.Message);
}
finally
{
    if (reader != null) reader.Close();
}


よくもまぁ、あるかどうかもわからないインデックス(2)を指定してくれたもんだ。でもこういうのって一杯ありますよね。
で、素人に毛の生えた程度のプログラマなら、

    while (reader.Read())
    {
        // この部分を以下に変更
        for (int i = 0; i < reader.FieldCount; i++)
        {
            if (i > 0) Console.Write("\t");
            Console.Write(reader.GetString(i));
        }
        Console.Write("\n");
    }

これで完璧だ!と思うのは、まだまだ毛がフサフサなヒヨコで、
(実際、殆どのテーブル操作はこれでOKだったりするから始末が悪い)
MySQL フィールド値として独特の null 値があることを忘れてはならない。

    while (reader.Read())
    {
        // この部分をさらに以下に変更
        for (int i = 0; i < reader.FieldCount; i++)
        {
            if (i > 0) Console.Write("\t");
            if (reader.IsDBNull(i)) Console.Write("null");
            else Console.Write(reader.GetString(i));
        }
        Console.Write("\n");
    }

というふうに、毛の抜けたSEならする。
ここを以下のようにすると、実行時にエラーとなる。
一回味わってみるのも良いだろう...(desc コマンドだし...)

    while (reader.Read())
    {
        // これは悪い例
        for (int i = 0; i < reader.FieldCount; i++)
        {
            if (i > 0) Console.Write("\t");
            if (reader.GetString(i) == null) Console.Write("null");
            else Console.Write(reader.GetString(i));
        }
        Console.Write("\n");
    }

GetString() は、フィールド値を string 型として返すが、null の場合に慣れ親しんだ null は返ってこず、アクセス違反を叩き付けられるので注意が必要。