このドキュメントは、Oracle Objects for OLEのバウンド・ウィジェット・ライブラリ(MFC用)に関する情報と、ライブラリの使用法を説明するワークブックで構成されます。
次の内容の2部構成になっています。
A. OMFCバウンド・ウィジェット・ライブラリ
B. Oracle Objects for OLE C++クラス・ライブラリ・ワークブック
このドキュメントは、Oracle Objects C++クラス・ライブラリとともに提供されるOMFCライブラリの説明です。OMFCライブラリには、MicrosoftのMFCフレームワークを使ってGUIプログラムを構築するためのクラスが含まれています。これらのクラスはVisual C++ 4.0, 5.0, 6.0を使って作成されています。
注:Visual C++ 4.0用のOMFCライブラリはomfc40.lib ,Visual C++ 5.0用はomfc50.lib, Visual C++ 6.0用はomfc60.lib という名前です。
このドキュメントでは、「omfc.lib」という表記は、これらのすべてのコンパイラ用バウンド・ウィジェット・ライブラリのことを指します。
コンパイラに適した正しいライブラリ名かどうかプロジェクトの設定で確認してください。
重要:バウンドコントロールライブラリ(OMFC4x.LIB)のMSVC 4.2以降のサポート
現在のリリースのOMFC40.LIBは、MSVC 4.0コンパイラのみをサポートします。異なるバージョンのMSVC
4.xコンパイラは異なるOMFC4x.LIBライブラリが必要なので、このリリースにはOMFC4x.MAKが[ORACLE_HOME]\OO4O\CPP\MFCディレクトリの中に含まれています。これにより、使用するコンパイラのバージョンに応じたOMFCライブラリをビルドできます。
サンプル・プログラムのビルド方法
サンプル・プログラムは、[ORACLE_HOME]\oo4o\cpp\mfc\samples\empeditディレクトリにあります([ORACLE_HOME]
はOracle製品をインストールしたディレクトリで、通常c:\orawin95またはc:\orantです)。プロジェクト・ファイルは
vsdept32.dsp、プロジェクト・ワークスペースは vsdept32.dsw です。また、別のサンプルが[ORACLE_HOME]\oo4o\cpp\workbook\empedtにあります。プロジェクト・ファイルは
empedt32.dsp、プロジェクト・ワークスペースは empedt32.dsw です。Microsoftは、プロジェクトのためのヘッダー・ファイルおよびライブラリを探すためのパス情報をすべて1カ所に格納しています。インクルード・ファイルやライブラリのパス情報を変更するには、メニューで「プロジェクト」->「設定」を選んで変更するか、プロジェクト・ファイルを修正して変更してください。クラス・ライブラリのインクルード・ファイルとOMFCインクルード・ファイルは、それぞれ[ORACLE_HOME]\oo4o\cpp\includeおよび
[ORACLE_HOME]\oo4o\cpp\mfc\includeにインストールされています。全Oracle
Objectsインクルード・ファイルを一個所にコピーしてパスを追加するという方法でもかまいません。oraclm32.lib
および omfc.lib は、それぞれ [ORACLE_HOME]\oo4o\cpp\lib
および [ORACLE_HOME]\oo4o\cpp\mfc\lib にインストールされています。プロジェクト設定を変更して適切なバージョンの
omfc.lib
(omfc40.lib, pmfc50.lib, omfc60.lib) をインクルードするようにしてください。
サンプル(もしくは作成したアプリケーション)を実行するため、[ORACLE_HOME]\bin
ディレクトリにあるクラス・ライブラリ・ランタイムDLL oraclm32.dll
にアクセスする必要があります。
OMFCのクラスによって解決される問題
Oracle Objects C++クラス・ライブラリの基本クラスによって、Oracleデータベースのデータにアクセスできます。レコードのフェッチ、レコードの追加、レコードの編集、任意のSQL文の実行などが可能です。しかし、データベースのデータを表示するGUI プログラムを作成する場合は、自分で作成する必要があります。データをフェッチして、GUIウィジェットにそのデータを入れる必要があり、ダイナセットが別のレコードに移動するたびにこれを繰り返す必要があります。データの編集にウィジェットを使う場合は、StartEdit、SetValue、Updateというサイクルを実行しなければなりません。
OBinder および OBound クラスを使えば、この作業は容易になります。 OBinder インスタンスがダイナセットを管理します。OBound インスタンスは OBinder のダイナセットのフィールドに付加され、OBinder のインスタンスに「バインド」されます。これ以降は、 OBinder および OBound コードがプログラマにかわって面倒なデータの整合性管理をほとんど実行します。つまり OBound インスタンスの値は必要なときに変更され、OBound インスタンスを介して編集したときは、これらの値がOracleデータベースに保存されます。
C++クラス・ライブラリには OBinder がインプリメントされています。しかし純粋な仮想 OBound クラスしか提供されていません。OBinderの便利さを活用するには、OBoundの機能をインプリメントするOBoundのサブクラスが必要です。
OMFCライブラリのクラスは OBound のサブクラスです。このクラスは OBound の機能をインプリメントしたGUIウィジェットを提供しています。このため、Visual C++言語の App Studio のリソース・エディタを使用してフォームを作成することが可能で、ほんの数行のコードでこれらのウィジェットをデータベース・フィールドにフックできます。これでアプリケーションのできあがりです。
OBinder および OBound の詳細は、『Oracle Objects C++オンライン・ヘルプ』を参照してください。
Visual Basicユーザーへの注意: OBinderオブジェクトはデータ・コントロールのように作動します(ユーザー・インタフェースはありませんが、データの整合性管理はすべて実行します)。OBoundオブジェクトはバインド済みコントロールのように作動します。
使用可能なオブジェクトの種類
オブジェクトの使用方法
OMFCライブラリ (およびC++クラス・ライブラリ全体) のクラスのインスタンスを作成し設定するには、次のようないくつかのステップが必要です。
例
OMFCクラスを使った例を提供します。Oracle Objects for OLEをインストールするときに
(サンプル・コードをインストールするよう指示した場合)、OMFCディレクトリのSamplesサブディレクトリにサンプルが入れられます。サンプルでは、表emp2を編集できます。表emp2は、Oracleデータベースとともに提供されるサンプルの表empを拡張した表です。(フォーム・ビュー用のバインド済みコントロール変数を宣言する)
VSDEPVW.Hと (バインド済みコントロールを使う) VSDEPVW.CPPという2つのファイルが含まれています。
表emp2のダイナセットを制御するOBinderは、特殊なサブクラスのメンバーです。インスタンスがPostMoveおよびPostAddトリガー・メソッドを上書きできるように、サブクラスが作成されています。PostMoveトリガーの上書きにより、新入社員が選択されるときに部門用OBinderが適切な部門に更新されます。PostAddトリガーの上書きにより、新入社員のレコードが作成されるときに、部門番号と社員IDが自動的に埋められます。
すべてのクラスのメソッド
次に示すメソッドはすべてのクラスで使えます。OBoundから継承されるメソッド (特にBindToBinder)は、ここでは説明されていません。OBoundおよびそのメソッドの詳細は『Oracle Objects C++オンライン・ヘルプ』を参照してください。
operator=およびcopyコンストラクタ
すべてのクラスは代入演算子とcopyコンストラクタをクラス定義に定義しますが、インプリメントはしていません。これによりコンパイラのデフォルトの代入演算子
(またはcopyコンストラクタ) を間違って使うことがなくなります。代入演算とcopyの組立ては、どちらもこれらのクラス用には定義されません。これらのオブジェクトに対して代入演算やcopyの組立てを間違って使った場合、リンク・エラーになります。
BindToControl
このメソッドは、オブジェクトを特定のユーザー・インタフェース・ウィジェットに関連付けます。
個別のクラス
このクラスはよく使用されます。テキストのエディット・コントロールにデータベースの値を表示します。このクラスには他に次のメソッドがあります。
SetProperty
このメソッドはコントロールが読込み専用か読み書き両用かを設定します。
このクラスによりスタティック・テキスト・アイテムをバインドできます。このクラスのオブジェクトは常に読込み専用です。このクラスには他にメソッドはありません。
このクラスでデータベースの値をチェック・ボックスとして表示および編集できます。TRUEとFALSEのように2つの値しか持たないデータベース・フィールドには、このクラスが非常に役立ちます。
SetPropertyメソッドにより、どの値を「オン」として扱い、どの値を「オフ」として扱うかを指定できます。ユーザーがチェック・ボックスをチェックすると、フィールド・データは「オン」値に設定されます。ユーザーがチェック・ボックスをチェックしないと、フィールド・データは「オフ」値に設定されます。フィールドの値が「オフ」でも「オン」でもない場合、チェック・ボックスは次のように作動します。
ラジオ・ボタンは1つではデータベース・フィールドの値を表すことはできません。ラジオ・ボタンはグループで値を表します。それぞれのラジオ・ボタンは1つの値に対応し、オンになっているラジオ・ボタン1つが実際のデータベース・フィールドの値を示します。異なるラジオ・ボタンを選択すると、フィールドの値を変更します。
OBoundGroupButtonを使うには、通常のユーザー・インタフェース・ウィジェットを作成します。次にグループ内の各ラジオ・ボタンのOBoundGroupButtonインスタンスにBindToControlをコールします。
ラジオ・ボタンの値を設定するには、各ラジオ・ボタンのSetPropertyをコールする必要があります。
現フィールドの値がグループ内のラジオ・ボタンの値のどれとも対応しない場合は、どのラジオ・ボタ
ンも選択されません。
SetProperty
このメソッドで、コントロールが読込み専用か読み書き両用かを指定し、このラジオ・ボタンが表す値
を指定します。
スライダはスクロール・バーとスタティック・テキスト表示の組合せです。スライダは範囲が決まっている数値データの表示や編集に適しています。データはスクロール・バーを使って表示および編集されます。静的テキスト表示でも表示されます。
OBoundSliderは少し違ったBindToControlメソッドを持っています。このメソッドはスクロール・バーとスタティック・テキストという2つのコントロールを同時にバインドする必要があります。
BindToControl
このメソッドはオブジェクトをスクロール・バーとスタティック・テキストに関連付けます。
SetProperty
このメソッドで、コントロールが読込み専用か読み書き両用かを指定し、スクロール・バーの範囲を設
定します。
B. Oracle Objects C++クラス・ライブラリ・ワークブック
はじめに
このワークブックは、Oracle Objects C++クラス・ライブラリを使ったC++コードの例を提供します。『Oracle
Objects C++オンライン・ヘルプ』には、Oracle Objects for OLEの一般的な情報だけでなく、C++クラスおよびそのメソッドの詳細も説明されています。
このワークブックで説明する例の中には非常に単純なものもありますが、それらはコードの一部分を示しています。それ以外の例は複雑なので、別ファイルにコード全体を提供しています。複雑な例に関しては、コードを含むファイルの名前を例の説明に記述してあります。サンプル・ファイルはsamplesディレクトリのサブディレクトリに入っています。samplesディレクトリはworkbookディレクトリのサブディレクトリです。サンプル・ファイルはデフォルトでインストールされます。カスタム・インストールではサンプル・コードをインストールするように選択します。いくつかの例を見れば、全体像が見えてくるでしょう。
ここで挙げている例では、ライブラリの簡単で一般的な使用方法から始めて、だんだんに複雑な、普通ではあまり使用されない方法まで説明します。熱心な開発者なら
(時間さえあれば) 例を見ていくだけでクラス・ライブラリの使用方法がわかります。しかし、『Oracle
Objects C++オンライン・ヘルプ』にある関連項目を読んで理解すると、より簡単にライブラリを使用できるようになります。
このクラス・ライブラリは、Oracleデータベースに対して使われることを目的としています。Oracleデータベースは、SQL言語を一次インタフェースとして使うリレーショナル・データベースです。このワークブックの例では、できるだけ簡単なSQLを使います。また、それぞれのSQL文が何をしているかについて簡単な説明が付いています。しかし、このライブラリやリレーショナル・データベースの能力を最大限に活用するには、SQLをよく理解する必要があります。
ここで説明する多くの例では、Oracleデータベースと一緒に出荷される標準デモ用の表が使われています。ほとんどの例で、「ExampleDB」という名前のデータベース、「scott」というユーザー名、そのユーザー用の「tiger」というパスワードが使われています。例に使われているコードを実際のプロジェクトで使うには、それぞれのデータベースを参照するようにデータベース名を変更し、データベースの有効なユーザーを参照するようにユーザー情報を変更する必要があります。(または、scottとtigerというアカウントの付いたデータベースを意味するExampleDBというデータベース別名を作成することも可能です。)
クラス・ライブラリを正しく使うには、アプリケーションでOStartupとOShutdownメソッドをコールする必要があります。これらのルーチンはクラス・ライブラリに必要な初期化とクリーン・アップを実行します (たとえばOLEを初期化したり、初期化前の状態に戻します)。部分的なコードしか含まない例では、掲載されているコードの外でこれらのルーチンがコールされていると想定しています。
この必要最小限の例では、データベースから簡単なデータを取り出すという、もっとも単純で一般的なライブラリの使用方法を示します。この例では列をいくつか持つ「emp」という表を参照します。この例で対象にするのは「sal」という列だけですが、この列は社員に支払う給与を表します。データベースには在籍社員それぞれのレコードが含まれています。
ここでの作業は、支払われた給与の合計額、つまり全社員の給与の合計を検索することです。このためには次の操作が必要です。
double SumSalary(void)
{
// connect to the database
datab.Open("ExampleDB", "scott", "tiger");
// query the database
dyn.Open(datab, "select sal from emp");
// process all the records
while (!dyn.IsEOF()) // until we've gone past all the records
{
この例ではわかりやすくするためにエラー処理はすべて省いています。いろいろなエラーを処理する方法は後の例で説明します。
データベース・オブジェクトとダイナセット・オブジェクトを両方とも「オープン」しています。オープンされていないODatabaseはデータベースに接続されていません。またオープンされていないODynasetにはレコードがないため、両方ともオープンする必要があります。これらのオブジェクトはオープンされない限り役に立ちません。データベース名 (ExampleDB)、ユーザー名 (scott)、ユーザーのパスワード (tiger) を渡してODatabaseオブジェクトをオープンすることにより、データベースが接続されます。ODynasetは特定のODatabaseに対してオープンされます。ODynasetは常に、データベースに対してSQLのSelect文を発行した結果得られるレコード群を表します。データベースはODynasetのOpenメソッドに対するODatabaseの引数として表します。ODynasetのOpenメソッドには、データベースからどのレコードを取り出すかを示すSQL文を与えます。
この例では、SQL文は「select sal from emp」です (「emp」は問合せ対象の表の名前)。リレーショナル・データベースには多数の表が含まれます。1つの問合せで1つ以上の表を同時にアクセスできます。Oracleデータベースに対する問合せでは、ネットワーク上に分散された数多くのデータベースに存在する表をアクセスできます。表empには列がたくさんありますが、その中に「sal」、「ename」、「hiredate」という3種類の列があります。今対象とするのは給与だけなので、列「sal」だけを検索します。多くの列を対象にすることもできます (列のリストに特殊な記号「*」を指定すると、すべての列を取得できます)。このように指定してもコードを変更する必要はありませんが、データベースから (不要な) 多くのデータを取り出すことになります。
ODynasetをオープンすると、データベースのデータと一致する一連のレコードを取得できます。ODynasetにはどのレコードが「現在の」レコードかを示す概念が含まれています。ODynasetにナビゲーション・メソッドを使えば、レコードからレコードへと移動できます。ODynasetがオープンされているとき、デフォルトでは最初のレコードが現レコードになります。この後ODynasetのMoveNextメソッドを使えば、取り出されたすべてのレコードを横切って検索できます。
IsEOFメソッドを使えば、すべてのレコードの検索が終了した時点がわかります。このメソッドは「MoveNext」が最後のレコードを通過した時点でTRUEを戻します。
これでレコードをすべてナビゲートできるので、次はデータを取り出します。一番簡単なのは、ODynasetのGetFieldValueメソッドを使う方法です。いろいろなデータ型をサポートするために、このメソッドには重複した名前が付けられています。このメソッドは (可能な場合は) データベースからのデータを暗黙に指定した型に変換します。ここでは給与は小数点以下を含むドルの値として保管されているので、double型を指定します。列の名前を指定してGetFieldValueをコールし、現レコードの給与値に設定したいdouble型変数のアドレスを渡します。
次にすべての給与額を合計します。簡単なように聞こえるでしょう。実際とても簡単なのです。なぜならサーバーがほとんどの作業をしてくれるからです。レコードをすべてダウンロードする必要はありません (大企業では何千ものレコードになる場合があり、ダウンロードするとネットワークの通信量は膨大になります)。かわりに、合計の算出をサーバーに任せます。
例の中に次の行があります。
この文を例にあるようにそのまま使うと、戻される列をそれ以降参照するには、「sum(sal)」という名前を使う必要がありますが、これはカッコがあるために面倒です。SQL文に対して次のようにもう1つ修正を加えると、すっきりします。
double SumSalary(void)
{
// connect to the database
datab.Open("ExampleDB", "scott", "tiger");
// query the database
dyn.Open(datab, "select sum(sal) ""sumsal"" from emp");
// get the sum of the salaries
dyn.GetFieldValue("sumsal", &sum); // get the salary total
return(sum);
もう1つ注意することがあります。データベースに接続はしましたが、切断はしていません。これで正しいのです。ルーチンが存在する場合は、ルーチンがODynasetとODatabaseオブジェクトを破棄します。ODatabaseオブジェクトが破棄されると、データベース接続を正しく削除するので、ユーザーは考慮する必要はありません。
データベース操作でもう1つ典型的なものとしては、問合せではないSQL文の実行があります。たとえば、表の作成、ユーザーの追加、データベースの管理、一連のレコードの削除等のためにSQL文を実行する場合です。この例では、簡単な表を作成します。簡単なエラー処理の方法も示します。
// routine to create the states table.
// returns 0 on success, -1 on failure
// There is a bug in this code! (See below) Don't use this!
int CreateStatesTable(void)
{
// connect to the database
ores = datab.Open("ExampleDB", "scott", "tigers");
if (ores != O_SUCCESS)
{ // couldn't open the database connection
// create the table
const char *sqls = "create table states (name char(15), area number,
population number)";
ores = datab.ExecuteSQL(sqls);
if (ores != O_SUCCESS)
{ // couldn't create the table
// everything went just fine
return(0);
ODatabaseオブジェクトをオープンして、データベースに接続します。ライブラリにあるほとんどのメソッドは、型がoresultの結果を戻しますが、これはメソッドが正常終了したか失敗したかを示します。メソッドが失敗したと判断した場合は、実際のエラーを取得するために他のルーチンをコールする必要があります。この場合は、OSessionクラスのメソッドであるルーチンGetServerErrorTextで、Oracleデータベース・ソフトウェアからエラー・テキストを取得します。
Openメソッドからの結果をチェックします。ルーチンが正常に実行されなかった場合は、ODatabaseオブジェクトからOSessionオブジェクトを取得し、それを使ってエラー・テキストを取得します。GetSessionメソッドはOSessionを戻すので、インラインで使えます。クラス・ライブラリのC++オブジェクトは、基礎になる実現オブジェクトへのハンドルを表すだけなので、オブジェクトとしては身軽です。GetSessionで戻されるOSessionのような一時オブジェクトを使っても、いっこうにかまわないわけです。GetServerErrorTextが
(const char *) を戻しますが、これを一般的なエラー処理ルーチンに渡し、そのルーチンがユーザーに警告を出すことになります。
表を作成するには、まずSQL文を組み立てます。この例では小さい表を作成するので、静的文字列を使えます。一般に複雑なSQL文はコードで組み立てます。この例のSQL文は3つの列を持つ「states」という名前の表を作成します。3つの列は、「name」という15文字のテキスト列、「area」という数値の列、「population」という数値の列です。
次に、ExecuteSQLメソッドを使って、実行したいデータベースにSQL文を渡します。ここでも結果をチェックして、必要ならエラー・テキストを取得します。
クリーン・アップ作業はオブジェクト・デストラクタで処理されるので、データベース接続 (オープン作業が正常に終了したかどうか) を心配する必要はありません。クリーン・アップは自動的に行われます。
さて、ここでこの例を実行すると、失敗します (ユーザーscottに対して間違ったパスワードを指定しました)。GetServerErrorTextは、間違ったパスワードを示すエラー文字列ではなく、0を戻します。これは、GetSessionがオープンされていないOSessionオブジェクトを戻すからです。OSessionがオープンされていないので、データベース・オブジェクトがOSessionを戻せないからです。Openに対してエラーが出たのです。どうしたら正しいエラーを取得できるのでしょうか。
その答は、まずOSessionオブジェクトを明示的に作成してから、そのセッションを使ってデータベースをオープンします。こうすると、データベースのオープンに失敗した場合、OSessionオブジェクトを参照できます。コードを書き換えると、次のようになります。
// routine to create the states table.
// returns 0 on success, -1 on failure
// Corrected version
int CreateStatesTable(void)
{
// open the default (unnamed) session
ores = sess.Open();
if (ores != O_SUCCESS)
{
// connect to the database
ores = datab.Open(sess, "ExampleDB", "scott", "tigers");
if (ores != O_SUCCESS)
{ // couldn't open the database connection
// create the table
const char *sqls = "create table states (name char(15), area number,
population number) ";
ores = datab.ExecuteSQL(sqls);
if (ores != O_SUCCESS)
{ // couldn't create the table
// everything went just fine
return(0);
このコードは前のコードとほとんど同じですが、明示的なOSessionオブジェクトを使ってデータベース・エラーを取得しているということと、OSessionオブジェクトをオープンするコードがあるというところが違います。OSessionのオープンは非常に簡潔です。OSessionのオープンに失敗した場合、GetServerErrorTextではなくGetErrorTextをコールしてエラー・メッセージを取得していることに注意してください。GetServerErrorTextは、エラーがデータベースに関する問題である場合に使います。これはデータベースに実際に接続するまで、または接続しようとするまでは使えません。OSessionのオープンに失敗した場合は、メモリーが少ないなどの内部の問題に起因します。このため、ここではかわりにGetErrorTextを使います。
リレーショナル・データベースのもっとも重要かつ強力な機能の1つに、いくつもの表から同時にデータを問い合せられるという機能があります。いくつもの異なった表に対して複数の関連する問合せを作成したり調整するのではなく、問合せを結合できます。この例の大部分は、結合された問合せを作成するためのコードです。SQLを使い慣れている方は、この例にざっと目を通すだけでよいですが、一番最後の段落だけはきちんと読んでください。
Oracleのサンプル・データベースのほとんどにあるscottアカウントにインストールされているサンプルのempおよびdeptという表を考えてみましょう。表empには、社員名 (enameフィールド)、社員の仕事 (jobフィールド)、社員の給与 (salフィールド)、社員が所属する部門の番号 (deptnoフィールド) が含まれています。これに関連する表がdeptですが、これには部門番号 (deptno) だけでなく、部門名 (dnameフィールド) と部門の場所 (locフィールド) が含まれます。
それぞれの社員の働いている場所を検索するアプリケーションを書くとします。社員の名前と所属する部門の場所を問い合せたいわけです。可能な解決方法の1つとしては (良い方法ではありませんが)、次のように別々の問合せを2つ作成する方法があります。
// bad solution to employee & department join
ODatabase datab;
datab.Open("ExampleDB", "scott", "tiger"); // open the database
ODynaset empdyn; // employee query
ODynaset deptdyn; // department query
empdyn.Open(datab, "select ename from emp");
deptdyn.Open(datab, "select loc from dept");
これで社員名用の問合せと部門の場所用の問合せができたわけです。この場合は、2つの問合せを管理しなければならないだけでなく、部門リストのどのレコードがどの社員に対応するかがわかりません。2つの問合せはソートされていないので、最初の部門が最初の社員の部門であるとは限りません。
しかし、この2つの表は1つのフィールド、deptnoを共有しています。このフィールドが2つの表を関連付ける「鍵」になります。次のように書き換えられます。
// still not a good solution to employee & department join
ODatabase datab;
datab.Open("ExampleDB", "scott", "tiger"); // open the database
ODynaset empdyn; // employee query
ODynaset deptdyn; // department query
empdyn.Open(datab, "select ename, deptno from emp");
deptdyn.Open(datab, "select loc, deptno from dept");
これで社員のレコードを参照するときはいつでも部門番号を読み取れます。次に部門レコードを走査すると、その部門番号の部門を検索できます。これで場所がわかります。この解決方法の問題は、部門レコードを頻繁に走査しなければならないということです。クライアント/サーバー・アプリケーションを書く場合には、この種の処理はサーバーが得意とするところです。サーバーはデータの調整を得意とします。データを調整するためにクライアント・ワークステーションにデータをダウンロードする必要はありません。
ここでできるのは、2つの表のdeptnoフィールドを一致させるようにサーバーに指示することです。次のようになります。
// the joined query solution
ODatabase datab;
datab.Open("ExampleDB", "scott", "tiger"); // open the database
ODynaset jdyn; // joined query
jdyn.Open(datab, "select emp.ename, dept.loc from emp, dept \
where emp.deptno = dept.deptno");
enameとlocという2つのフィールドを持つダイナセットができました。この2つのフィールドが欲しかったフィールドです。ここでlocフィールドは社員が所属する部門の場所に対応しています。実は、クライアントに結果を戻すためにサーバーがレコードを準備しているときに、サーバーは実際に調整作業を行っていたのです。クライアントが行うよりはさらに効率的に調整しています (リレーショナル・データベースは本質的に構造がそうなっているからです)。
特記すべき点を次に示します。
まず、SQL文の構文です。問合せで使いたいフィールドを指定しました。フィールドがいくつかの表にあるため、「table.field」などの名前でコールして、フィールド名は完全に指定しました。問い合せる表を指定するときは、対象にするすべての表をカンマで区切ってリストを指定しました。最後に「結合条件」を指定しましたが、これはいろいろな表からのデータをどのように調整したいかを指定する単なる「where」句です。
データベース・オブジェクトをオープンするには、データベース名、ユーザー名、およびユーザーのパスワードを指定する必要があります。これらの値はコードに直接書き込むこともできますが、それでは別のユーザーや別のデータベースに対してプログラムが使えなくなってしまうので、賢い方法ではありません。接続情報は汎用的にする方が役に立ちます。
このための一番わかりやすい方法は、接続情報を取得するダイアログを作成する方法です。接続が可能な場合はオープンしているODatabaseオブジェクトを戻し、接続に失敗した場合は閉じたODatabaseを戻すルーチンが必要になります。つまり、異なるオプションでODatabaseを作成したいわけです。
sampleのサブディレクトリlogdlgにはファイルLOGDLG.CPPとLOGDLG.Hが含まれています。これらのファイルには、Visual C++のMFCフレームワークを使って、ログイン・ダイアログを実現するためのコードが含まれています。このコードはMFCフレームワークにはあまり依存していないので、コードの理解にはMFCの経験はそれほど必要ではありません。
logdlgクラスの重要な部分を次に示します。
class logdlg : public CDialog // subclass of a dialog
{
public:
// get a database login
ODatabase GetLogin(long options = ODATABASE_DEFAULT);
// more implementation details...
クラスのインスタンスはダイアログで、ここで作成されます。次に、クラスのクライアントが、有効でかつオープンしているODatabaseオブジェクトを欲しいときに、GetLoginメソッドをコールします。このメソッドはダイアログを実行し、データベースの接続を確立しようとし、ユーザーにエラーを通知して、最後に戻ります。
GetLoginのソース・コードを見れば、このメソッドはほとんど何もしていないことがわかります。このメソッドはいくつかの設定を実行して、ダイアログを実行するために「DoModal」ルーチンをコールします。次にODatabaseオブジェクトを戻しますが、このオブジェクトはオープンされている場合もされていない場合もあります。本当の作業は「OK」が押されたときに行われます。そのときにコールされるメソッドを次に示します。
void logdlg::OnOK()
{
// get the strings the user has entered
GetDlgItem(IDC_USERNAME)->GetWindowText(user);
GetDlgItem(IDC_PASSWORD)->GetWindowText(password);
GetDlgItem(IDC_DATABASE)->GetWindowText(dbname);
// try to open the database
if (m_database.Open(m_session, dbname, user, password, m_options) !=
OSUCCESS)
{ // some error
// get the oracle error message, to display to the user
const char *dberrs = m_session.GetServerErrorText();
ErrorMessage(dberrs); // tell user what went wrong
このメソッドはログイン・ダイアログからテキストを取得し、そのテキストを使ってODatabaseのオープンを試みます。このメソッドはODatabaseオブジェクトをオープンするときに、GetLoginに渡されたオプションを使います。エラーがあった場合、一般的な「ErrorMessage」(単純なメッセージ・ボックスを表示するだけです) がコールされます。オープンが正常に終了したときにだけ、親ダイアログ・クラスのルーチン「OnOk」がコールされます (このルーチンがダイアログを閉じます)。
ダイアログを終了するには2つの方法があります。1つは「OK」が正常終了した場合ですが、この場合は戻されるODatabaseオブジェクトがオープンされます。もう1つはユーザーが取消し操作を行った場合で、この場合は戻されるODatabaseオブジェクトが閉じられます。ユーザーが接続しようとして「OK」を選択し、接続に失敗した場合、上述のルーチンはユーザーに警告を出しますが、ダイアログは閉じません。
ここでも、ODatabaseインスタンスを渡すことが妥当です。ここではサブルーチンからの戻り値として使っています。コール元のコードは次のようになります。
// connect to the database
ODatabase database;
logdlg dblogin;
database = dblogin.GetLogin();
if (database.IsOpen())
; // success
else
; // user canceled for some reason
ダイアログのフィールドをあらかじめ設定できるようにしたり、ODatabase.Openにデータベース・オプションを設定できるようにしておけば、ダイアログはさらに改善されます。
ダイナセットの内容を読み込むだけでは済まないこともあります。ダイナセットの中のレコード、したがってデータベースそのものを変更したい場合もあります。
クラス・ライブラリを使えば、2つの方法でデータベースのレコードを更新できます。最初の方法はSQLのupdate文を実行する方法です。2番目の方法はダイナセットを使ってレコードを編集する方法です。
例として表empを使います。この表には社員の給与情報が含まれています。会社の業績が良かったので、社員全員の給与を1000ドル増やすとします。
// give all employees a raise of amount "raise"
oboolean GiveRaise(int raise)
{
// open a connection to the database
db = db_login.GetLogin();
if (!db.IsOpen())
// give everybody a raise
char sql[80]; // for constructing the sql statement
sprintf(sql, "update emp sal = sal + %d", raise);
if (db.ExecuteSQL(sql) != OSUCCESS)
このルーチンではログイン・ダイアログを実行して、次にルーチンを終了してODatabaseインスタンスを破棄することにより、データベース接続を削除していることに注意してください。一般にこのようなルーチンではODatabaseオブジェクトをパラメータとして渡し、他の多くの同じような操作でログインを共有できるようにするのが普通です。
サーバーが必要な作業をすべて行うため、この方法でレコードを更新すると非常に効率的です。しかし、もっと複雑な状況ではこのように単純なSQLのupdate文を使うことはできません。各社員の昇給額を何らかの方法で計算して、それを各レコードに適用する方が、より現実的な例になります。このためには、データベースにある各レコードを全部検索して、必要に応じてそのうちのいくつかを更新するということが必要になります。このためのルーチンを次に示します。
// Calculate and apply raises to all employees
oboolean GiveRaises(const ODatabase &db)
{
// create a dynaset referring to the employees
ODynaset dyn;
dyn.Open(db, "select ename, sal, hiredate from emp");
if (!dyn.IsOpen())
double salary, raise;
// go through all the records
while (!dyn.IsEOF())
{ // for every record
レコードを編集するには、次の3つのステップがあります。
最後に、ダイナセットまたはデータベースの更新には (またはどんな種類の変更でも)
微妙に難しいところがあります。つまり、ダイナセットのレコードに対する変更は即座にダイナセットに反映されますが、他のユーザーが加えた変更や、プログラムの中の他のダイナセットによる変更は、即座には反映されません。変更された行をダイナセットがまだ取り出していない場合は、その行を取り出すときに新しい値を取得します。しかしどの行が実際に取り出されたかを予測するのは困難です
(キャッシュのため)。ダイナセットにRefreshを実行すると、ダイナセットに最新のデータが入っていることを保証できます。
例5では、既存のレコードの編集を説明しました。多くの場合、レコードの編集だけでなく、新規レコードの追加や既存レコードの削除が必要になります。ODynasetのメソッド、DeleteRecordは現レコードを削除します。レコードを削除するには、対象のレコードまで移動して、DeleteRecordを実行します。
ある一定額以上の給与の社員全部を削除するルーチンを次に示します。
oboolean DeleteOverpaid(const ODatabase &db, double maxsal)
{
DeleteRecordでレコードを削除すると、現レコードが無効になります (削除されたからです)。このようなレコードに対しては、ほとんどの操作が失敗します。別のレコードに移動すると、有効なレコードが取得できます。削除された (したがって無効な) レコードに戻るようなナビゲーションはできません。
この例ではフィールドの値は、ODynasetオブジェクトによって直接取得するのではなく、OFieldオブジェクトを使って取得します。OFieldオブジェクトは基礎になるダイナセットと緊密にリンクされています。つまり、ダイナセット内で移動すると、フィールドの値は現レコードの値を含むように変化します。OFieldインスタンスは、ダイナセット全体を扱うよりも便利に使えます。これは上述の「(double) salary > maxsal」文に示すとおり、このインスタンスを変数として扱えるからです。
新規レコードの追加は、もう少し複雑です。ダイナセットにレコードを追加するには2つのメソッドが使えます。1つはAddNewRecordで、これはブランク・レコードを追加します。もう1つはDuplicateRecordで、これはブランク・レコードを追加し、現レコード (DuplicateRecordがコールされた時点の現レコード) で検索されたフィールド値で、そのブランク・レコードを埋め込みます。ブランク・レコードが作成されるときは、NULLで埋め込まれるか、またはデータベース・サーバーに送られて、デフォルト値で埋め込まれます。どちらになるかは、ODATABASE_PARTIAL_INSERTオプションがオンになっているかどうかで決まります。(詳細はODatabaseのドキュメンテーションを参照してください。)
レコードを追加した後は、そのレコードの値の一部を変更できます。AddNewRecordまたはDuplicateRecordコールにより、StartEditとよく似た方法で編集できます。変更したいところをすべて変更したら、Updateをコールして、データベースに変更を反映させます。
新規レコードを追加する場合は、編集する表の構造を理解しておくことが重要です。フィールドによっては一意の値を必要とします。NULL値を入れられないフィールドもあります。このような要件が満たされないと、Updateコールは失敗します。
表empに新規レコードを追加するルーチンを次に示します。
oboolean AddEmployee(ODynaset *empdyn, const char *ename,
double salary, int deptno)
{
// Add the new record
if (empdyn->AddNewRecord() != OSUCCESS)
// Set the values of the record
empdyn->SetFieldValue("ename", ename);
empdyn->SetFieldValue("sal", salary);
empdyn->SetFieldValue("deptno", deptno);
// the empno field is required and must be unique.
// We will find the current maximum empno and add 1 to it.
ODynaset empnodyn;
empnodyn.Open(empdyn->GetDatabase(),
"select max(empno)+1 newempno from emp");
int empno;
empnodyn.GetFieldValue("newempno", &empno);
// finish setting the employee record
empdyn->SetFieldValue("empno", empno);
// put this record in the database
if (empdyn->Update() != OSUCCESS)
return(TRUE);
ほとんどのフィールドの値はNULLのまま残されています。ルーチンに渡された引数に基づいて、ename、sal、deptnoの値を設定します。
empnoフィールドは表empの一意のキーとして使われるので、これは一意の値でNULL以外の値でなければなりません。これを指定する簡単な方法は、サーバーに現在の社員番号の一番高い値を聞いて、サーバーでその値に1を加えて、新しい一意の値を取得する方法です。
これは2つの理由で、実際には良い解決策ではありません。1つは、追加する各レコードごとに、データベースに対して1つ余分な問合せをするからです。プログラムの始めで何か妥当な社員番号を指定しておいて、その数値を使う (増分を加えるなど) 方が速くなります。2つ目の理由は、この方法で保証するのは、empnoフィールドが現レコードの中で一意であるということだけだからです。社員レコードが削除された場合、その社員番号フィールドを再利用する可能性があります。このため一般にはお勧めできません。
データベースにはこのような問題の解決を助けるために「順序」という種類のオブジェクトがあります。
ダイナセットの編集や更新と同じように、ダイナセット間の一貫性を保つのは困難です。ダイナセットに含まれているレコードがすでにサーバーから取り出されている場合に、別のダイナセットが同じレコードを削除すると、最初のダイナセットには「幽霊」のレコードができてしまいます。同様に、最初のダイナセットに追加されたレコードは、他の既存ダイナセットには表示されません。(追加レコードはダイナセットをリフレッシュすることで表示できます。)
データベースから削除されたレコードを編集しようとすると、データベースはStartEditメソッドで失敗します。
ダイナセットの操作でデータベースのデータを変更する場合、通常は加えた変更をデータベースに即座に反映します。レコードを追加したり、フィールド値を変更する場合は、Updateメソッドを実行するときにデータベースが変更されます。レコードを削除する場合は、DeleteRecordメソッドが実行されるときにデータベースが変更されます。レコードの変更が独立した操作の場合はこれで十分です。
しかし、ときには全部集めて一度に変更しなければならないこともあります。つまり、すべて変更するか、または全然変更しないかのどちらかというときがあります。この古典的な例としては、銀行預金口座の残高をメンテナンスするアプリケーションがあります。ある口座から別の口座に振込みがあったときに、2つの別々のレコードが編集されます。一方の口座の残高を記録するレコードは借り方に記入され、もう一方の口座のレコードは貸し方に記入されます。この2つの操作は、両方が一緒に正常終了するか、または両方が失敗するかのどちらかでなければなりません。一方が発生してもう一方が発生しないと、銀行の勘定簿には残高が正しく記入されないことになります。
これを解決するには、一連の変更を1つのトランザクションに包み込みます。最初の変更を加える前に、BeginTransactionメソッドをコールして、それから変更を加えます。変更を取り消したい場合は、Rollbackメソッドをコールします。変更を行いたい場合は、Commitメソッドをコールします。RollbackまたはCommitが現在のトランザクションを終了します。その後、デフォルトでは、UpdateまたはDeleteRecordメソッドのたびにデータベースを直接変更するという通常の動作に戻ります。
精通したOracle開発者への注意: クラス・ライブラリの標準的なトランザクション・モデルは、SQL*PlusまたはOracle Formsの自動コミット・モードとよく似ています。BeginTransactionの実行は、自動コミットをオフにするのと同じようなものです。RollbackまたはCommitの実行は、ロールバックまたはコミットと同じです。RollbackまたはCommitのデフォルトの動作は、自動コミット・モードに再び入るのと同じです。
この例では、DynasetMarkを使います。これはダイナセットの中の位置を記憶するオブジェクトです。現在位置のマークを取得しておいて、後でMoveToMarkメソッドを使ってそのレコードに再び位置を移します。
銀行勘定口座の例を次に示します。
// transfer money from credit to debit account
// we have a DynasetMark on the two accounts
void TransferMoney(ODynaset accounts, // dynaset of bank accounts
ODynasetMark credit, // mark on record to be credited
ODynasetMark debit, // mark on record to be debited
double amount // amount to transfer
)
{
// start the transaction
banksess.BeginTransaction();
// make the transfer
double balance; // an account's balance
// credit one account
accounts.MoveToMark(credit);
accounts.StartEdit();
accounts.GetFieldValue("balance", &balance);
accounts.SetFieldValue("balance", balance + amount);
if (accounts.Update() != OSUCCESS)
{ // couldn't change the first record
// debit the other account
accounts.MoveToMark(debit);
accounts.StartEdit();
accounts.GetFieldValue("balance", &balance);
accounts.SetFieldValue("balance", balance - amount);
if (accounts.Update() != OSUCCESS)
{ // couldn't change this record
// everything is fine - commit the transaction
banksess.Commit();
return;
(ここではUpdate文に関してだけエラーをチェックしています。実際のアプリケーションではもっと注意して、すべてのメソッドに対してエラーをチェックします。)
最初のupdateは正常に終了したのに、2番目のupdateが失敗するのはどうしてでしょう。(ネットワークのハードウェアが故障したために) データベースの接続が失われた可能性があります。新しい口座残高がデータベースの制約を侵していたかも知れません (たとえばトリガーがデータベースに格納されていて、口座残高が0以下になったためにそのトリガーがupdateを失敗させたのかも知れません)。
例8では、上記と同じ作業を実行するための別の方法を示します。例8ではダイナセットを使うのではなく、ExecuteSQLを直接コールしてデータベースを変更しています。ExecuteSQLを使って (update、insert、delete文によって) データベースに加えた変更は、BeginTransactionとCommitまたはRollbackによって管理されるトランザクションの一部です。
CommitおよびRollbackメソッドは、startnewという名前のoboolean引数を取りますが、この引数はデフォルトではFALSEです。この引数をTRUEに設定すると、(あたかもBeginTransactionを再度コールしたかのように)CommitまたはRollbackの直後に新しいトランザクションが始動します。
Oracle開発者への注意: セッションをオープンした直後にBeginTransactionをコールし、startnewを常にTRUEに設定する場合は、使い慣れたOracle環境とよく似たトランザクション環境になります。次のコードについて考えてみましょう。
// fragment illustrating Oracle transaction details
OSession sess;
sess.Open(); // open the default session
sess.BeginTransaction(); // start a transaction
ODatabase db;
db.Open(sess, "ExampleDB", "scott", "tiger");
ODynaset dyn1;
dyn1.Open(db, "select * from emp order by empno");
// change the first record
dyn1.MoveFirst();
dyn1.StartEdit();
dyn1.SetFieldValue("sal", 7500);
dyn1.Update();
// open another dynaset
ODynaset dyn2;
dyn2.Open(db, "select * from emp order by empno");
dyn2.MoveFirst();
long salary2;
dyn2.GetFieldValue("sal", &salary2);
// open yet another dynaset
// open a named session
OSession nameds;
nameds.Open(sess.GetClient(), "session2");
ODatabase db2;
db2.Open(nameds, "ExampleDB", "scott", "tiger");
ODynaset dyn3;
dyn3.Open(db2, "select * from emp order by empno");
dyn3.MoveFirst();
long salary3;
dyn3.GetFieldValue("sal", &salary3);
2つ目のODynasetは1つ目のODynasetと同じセッション内でオープンされます。1つ目のODynasetが変更を加えますが、この変更はデータベースに対してまだコミットされていません。3番目のODynasetは別のセッションでオープンされます。salary2とsalary3の値は何でしょう。
dyn2がdyn1と同じセッション内にあるため、dyn2は同じデータベース状態を検索します。7500という給与を検索します。しかしdyn3は別のセッションにあるため、dyn3は7500という給与を検索しません。dyn3はフィールドが変更される前にそのフィールドにあった給与を検索します。dyn3が同じプロセスの一部かどうかにかかわらず、こういう結果になります。dyn3が別のコンピュータ上にある可能性もあるのですから。
ここまでの例で使ったSQL文はすべてリテラルです。このため、部門20の社員レコードが必要な場合は、「select * from emp where deptno = 20」というSQL文を使います。そして次に部門10からのレコードが必要な場合には、「select * from emp where deptno = 10」というSQL文を使います。いろいろな意味でこれは効率的ではありません。「select * from emp where deptno =:deptno」(:deptnoは後で値を設定できるもの) と指定できれば、より効率的です。これがパラメータです。パラメータによってSQL文の処理に変数が導入できます。
パラメータはODatabaseオブジェクトに付加され、OParameterCollectionというオブジェクトを介してアクセスされます。OParameterCollectionオブジェクトのパラメータの数は可変で、パラメータの集合にパラメータを追加または削除できます。(FieldCollectionおよびOConnectionCollection、OSessionCollection等、他にもパラメータ集合に関するオブジェクトがありますが、これらは読込み専用です。) パラメータはいくつでも作成できます。自動的に使用可能になるパラメータは、どの時点でもSQL文にバインドされます。(詳細は、『Oracle Objects C++オンライン・ヘルプ』を参照してください。)
次に示すコードでは、社員レコードのいろいろな集合を選択するのにパラメータを使っています。
// main routine (processes departments)
ProcessCompany(ODatabase *db)
{
// set up a "dnumber" parameter on the database
OParameterCollection params = db->GetParameters();
// create parameter with initial value of 0
params.Add("dnumber", 0, OPARAMETER_INVAR, OTYPE_NUMBER);
OParameter dnumber = params.GetParameter("dnumber");
// process every department
deptnumbers.MoveFirst();
while (!deptnumbers.IsEOF())
{
// process that department
ProcessDepartment(db);
// go to next department
deptnumbers.MoveNext();
}
// all done. We don't need the dnumber parameter to be part of
// the collection anymore so get rid of it
params.Remove("dnumber");
return;
void ProcessDepartment(ODatabase *db)
{
// process them all
emps.MoveFirst();
while (!emps.IsEOF())
{
ProcessCompanyでパラメータを設定して、それをサブルーチンProcessDepartmentで使っています。これで、より一般的なProcessDepartmentルーチンを作成できます。このルーチンが処理する部門番号は、ルーチン内のどこにも直接には書き込まれていません。このルーチンはこのデータベースのために存在するパラメータ「dnumber」に依存していて、これを引数として取ります。
ODatabaseのパラメータ集合にAddメソッドを使うことにより、パラメータが作成されます。パラメータの値はその後いつでも変更できます。しかし、パラメータの値を変更しても、そのパラメータを使って作成されたダイナセットの内容はすぐには変更されません。パラメータの値はSQL文が実行されるときにだけ使われます。SQL文が実行されるのは、ExecuteSQLコールまたはODynasetのOpen、ODynasetのRefreshの時点です。最後にパラメータを使い終わったら、例にあるように、OParameterCollection::Removeメソッドを使って削除できます。
この例をもう少し詳しく説明します。ODatabaseはProcessCompanyルーチンにアドレスで渡されます。ODatabaseのアドレスを逆参照するのは不正ではなく、この例ではそうしています。ODatabaseは値で渡すこともできます。deptnumbersダイナセットから部門番号を取り出すために、OFieldオブジェクトが使われています。OFieldは常に、OFieldが付加されているフィールドの値を、現レコードから取り出します。このため、ダイナセット内でナビゲートすると、OFieldオブジェクトは異なる値を戻します。OFieldオブジェクト用に、いろいろなキャスト演算子に重複した名前を付けてあるため、列の値を取得するには、OFieldオブジェクトにキャストするだけでできます。
例7をより現実的なアプリケーションにしたものを次に示します。
// transfer money from one account to another
// SQL statement to change a balance in the accounts table
static const char *setbalance =
"update accounts set balance = balance + :amount where accno = :anum";
void Transfer(ODatabase *bankdb, // the bank database
int debitaccount, // account to debit
int creditaccount, // account to credit
double amount) // amount to transfer
{
// get the parameters
OParameter amount = bankdb->GetParameters().GetParameter("amount");
OParameter anum = bankdb->GetParameters().GetParameter("anum");
// credit the first account
anum.SetValue(creditaccount);
amount.SetValue(amount);
if (bankdb->ExecuteSQL(setbalance) != OSUCCESS)
{
// debit the second account
anum.SetValue(debitaccount);
amount.SetValue(-amount);
if (bankdb->ExecuteSQL(setbalance) != OSUCCESS)
{
// it all worked
bankdb->GetSession().Commit();
return;
ダイナセットで何かが起きているとき (ナビゲーションやある種の編集)、コードに対して通知が必要な場合があります。一般に、ダイナセットはコードのある一部分で制御し、それをコードの他の部分 (ある種の汎用アクセス管理パッケージなど) で監視します。
アドバイス・オブジェクトを使ってこの監視を行います。つまり、ODynasetにOAdviseオブジェクトを付加します。これにより、それ以降はダイナセットに何か起きるたびにメッセージがOAdviseオブジェクトに渡されます。アドバイス機能はダイナセットのアクションを制御でき (たとえばアクションの取消し)、何が起きているかを監視できます。
クラス・ライブラリの一部であるOAdviseクラスは何もしません。役に立つようなアドバイス・オブジェクトを使うためには、OAdviseのサブクラスを作成し、次にそのサブクラスのインスタンスを作成しなければなりません。仮想関数をいくつか上書きするだけで、必要な機能を得られます。
samplesディレクトリにはposadvというサブディレクトリが含まれています。posadvにはファイルPOSADV.CPPおよびPOSADV.Hが含まれています。これらのファイルには、PosAdviseというOAdviseのサブクラスがインプリメントされています。PosAdviseクラスはダイナセットが現在どのレコード上にあるかを追跡し記録します。最初のレコードが0、次のレコードが1というように記録します。DeleteRecordのように、データの整合性管理に障害となるようなアクションはPosAdviseで取り消せます。
PosAdviseの使用方法を次に示します。
// use of PosAdvise class
// open a dynaset
ODynaset dyn1;
dyn1.Open(db, "select * from emp");
// attach a PosAdvise advisory to the dynaset
PosAdvise dynposition;
dynposition.Open(dyn1);
// move to beginning of dynaset to get dynposition started
dyn1.MoveFirst();
// position the dynaset before we do some processing
PositionDynaset(dyn1);
// get the position
long startpos = dynposition.GetPosition();
// now do some processing
ProcessDynaset(dyn1);
// how many records did we process?
long nrecords = dynposition.GetPosition() - startpos;
アドバイス・オブジェクトはOAdviseのOpenメソッドを使ってダイナセットに付加します。破棄された場合またはCloseがコールされた場合には自動的に付加が解除されます。
ダイナセットにアクションが取られると、次のようにいくつかのことが発生します。
・ アクションの前に、付加されているすべてのアドバイス・オブジェクトのActionRequestメソッドがコールされます。アドバイス・オブジェクトはこの時点でアクションを取り消せます。(PosAdviseクラスでは、MoveFirst、MoveLast、MoveNext、MovePrev以外のすべてのアクションを取り消します。)
ここまでのすべての例では、ODynasetメソッドを介して、またはOFieldを使って、ダイナセット内のデータはほとんど直接アクセスしてきました。アクセス方法としてはこれらがいつも一番便利な方法とは限りません。新規レコードに移動したり、いろいろなフィールドの値に興味がある場合は、値を明示的に取り出す必要があります。さらに、レコードの編集や追加をする場合は、データの整合性を管理する作業もかなり発生します。「管理されたダイナセット」、つまり決まりきった作業をよりたくさんこなせるオブジェクトがあると便利です。これがOBoundとOBinderクラスの仕事です。
Visual Basicのユーザーなら、OBinderクラスはデータ・コントロールの役割を果たし
(ユーザー・インタフェースはありませんが)、OBoundのサブクラスがバインド済みコントロールの役割を果たすことに気付くでしょう。
OBinderオブジェクトは、ODynasetオブジェクトと同じように使います。たとえばOBinderオブジェクトはデータベースとSQL情報を使ってオープンされます。コール側でレコードの追加と削除ができるメソッドも持っています。ダイナセットでできること以外に、OBinderオブジェクトではフィールドに加えられた変更を追跡し記録することができ、データベースを自動的に更新することもできます。
OBoundオブジェクトはレコードの中の1つのフィールドの値を保持します。ダイナセットが変わると、OBoundインスタンスの値が変わります。そしてOBoundインスタンスの値が変わると、ダイナセットの中の対応するフィールドの値も変わります。このとき、データの整合性も適切に処理されます。
クラス・ライブラリとともに提供されるOMFCおよびOOWLライブラリでは、OBinderおよびOBoundが使われています。これらのライブラリには標準のウィジェットを作成する機構が含まれていますが、このウィジェットもOBoundのサブクラスです。その結果、特別にサブクラス作成されたこれらのウィジェットを使うGUIプログラムでは、ユーザーがウィジェットのテキストを編集して、次に別のレコードに移動すると、データベースが自動的に編集されます (例11を参照)。
この例では、OBinderにより管理されるダイナセットと一緒に作動するOValue変数が使用できるように、OBoundからサブクラスを作成しています。バインドされたOValueオブジェクトの値は、OValueが関連付けられたフィールドの現在の値に自動的に設定されます。OValueオブジェクトの値が変更されると、その変更を反映するようにデータベースが更新されます。このクラスをOBoundValと呼びます。このクラスをインプリメントしたものがsamplesディレクトリのサブディレクトリboundvalに格納されています。このワークブックでの目的は次に示すようなプログラムを作成できるようになることです。
// silly example of use of OBoundVal
void GiveRaises(int minsalary, int saladd)
{ // give everybody with salary below minsalary a raise of saladd
// construct the OBinder (the managed dynaset)
OBinder block;
// set up an OValue bound to the salary field
OBoundVal salary;
salary.BindToBinder(&block, "sal");
// Note that we are binding the "sal" column before opening the
// query. If the select list for the query does not contain the
// "sal" column, the OBinder.Open() call will fail.
// get the database data for the managed dynaset
block.Open(odb,"select ename, sal, empno from emp order by empno");
// If we do the binding here and the column we are attempting to
// bind does not exist, the following call will fail:
// salary.BindToBinder(&block, "sal");
// Note that IsLast() will return TRUE after MoveNext() attempts to
// move past the last record. Therefore the last record does get
// processed in the loop
while (!block.IsLast())
{
このサンプルで一番興味深い部分は、給与が変更される行です。変数の値を設定しているかのように見えます。しかし、ここではデータベースにあるフィールドの値を実際に変更しているのです。別のレコードに移動したときにデータベースが更新されます。
OBoundクラスには非常に重要なメソッドが3つあります。
1. フィールドの値が変わるたびに、OBoundに新しい値を与えるために、Refreshが
(OBinderにより) コールされます。
2. OBoundがその値をデータベースに保存するたびに、SaveChangeが
(OBinderにより) 常にコールされます。
3. 値が変わったことをOBoundとOBinderに通知するために、OBoundのサブクラスをインプリメントすることによりChangedがコールされます。
OBoundのサブクラスにはRefreshとSaveChangeを必ずインプリメントしなければなりません。OBoundサブクラスが基礎となるクラスのトリガー手順を上書きする場合は、重複した名前のトリガーは、正しく動作するように基礎となるクラスのデフォルトのトリガーをコールする必要があります。OBoundクラスの詳細は、オンライン・ドキュメンテーションを参照してください。OBoundValをインプリメントしたものを次に示します。
RefreshはデータベースからOBoundオブジェクトへ値を移送します。SaveChangeはOBoundオブジェクトからデータベースへ値を移送します。Changedはデータベースに変更を保存する必要があることをOBinderの整合性管理機能に通知します。
OBoundは一般に「混成」(mix-in) クラスとして使われ、他のクラス階層に機能を追加します。この例では、OValueクラスに機能を追加しています。OBoundValはOBoundとOValueから多重継承します。
OBoundValのRefreshメソッドを次に示します。
oresult OBoundVal::Refresh(const OValue &val)
{
新しい値を渡されたら、OValueの代入演算子を使ってその値を保存します。このルーチンはOBinderクラスによりコールされます。SaveChangeメソッドは次のとおりです。
oresult OBoundVal::SaveChange(void)
{
OBoundのヘルパー・ルーチン、OBound::SetValueを使って値を設定します。SaveChangeはOBinderクラスによりコールされます。
では、OBoundValの値はどのようにして設定するのでしょうか。OValue::SetValueを使うだけでは、値は変更されますが、変更されたことがOBoundValクラスにはわかりません。さらに、変更があったことをデータベースには通知しないので、データベースには変更が保存されません。整数の引数を使って値を設定する正しい実現方法を次に示します。
oresult OBoundVal::SetValue(int val)
{
if (!Changed())
return(OFAILURE); // couldn't start change
OValue::SetValue(val);
return(OSUCCESS);
}
まず最初に、Changed()メソッドをコールします。(デフォルトの引数はTRUEで、これは変更を加えようとしていることを示します。)
これでフラグが設定されます。このフラグは、このOBoundに対して後でSaveChangeをコールする必要があることを示し、ダイナセットにはStartEditを試みるよう指示します。いろいろな理由で、これは失敗する可能性があります。たとえば、このユーザーにはこのデータベースを編集する許可がないことや、レコードがロックされているなどの理由が考えられます。StartEditが正常に終了したときにだけ、親メソッドのOValue::SetValueを使ってOBoundValの値を設定します。
オブジェクトの値を変更するOValueメソッドは、すべて上書きされていることを確認する必要があります。これは、OBinderの整合性管理機能にオブジェクトの値が変更されたことを認識させるために、Changed()をコールする必要があるからです。このためClearおよびoperator=メソッドも上書きしなければなりません。また、より実用的にするために、追加のoperator=メソッドもたくさん提供して、OBoundValができるだけ通常の変数のように動作できるようにしています。
operator=を使う場合は、オブジェクトそのものではなく、OBoundValueの値をコピーしているということに注意してください。この2つには微妙な差があります。一般に、OBoundのサブクラスはオブジェクトそのものではなく、オブジェクトの値のコピーを必要とします。このため、コピー・コンストラクタや代入演算子は、OBound用にはインプリメントしません。
OBoundValの使用方法とOFieldオブジェクトの使用方法とでは、あまり差はありません。両方とも現レコードのフィールドの値を戻します。また両方ともフィールドの値の設定に使えます。しかし、OBoundValはダイナセットが別のレコードに移動するたびに値をコピーしますが、OFieldはコピーしません。OFieldは要求されたときにだけ、ダイナセットから値を取り出します。また、OFieldを編集に使う場合は、ダイナセットのStartEditとUpdateをユーザー自身が管理しなければなりません。
データベースのフィールドがユーザー・インタフェースに表されている場合は、OBoundが非常に役に立ちます。この場合、変更処理のコードがかなり分散されてしまうので、(たとえば)StartEditがコールされたかどうかを追跡し記録するのが面倒になります。
この例では、完全なアプリケーションの作成を示します。アプリケーションは、MFCフレームワーク・クラスを含むMicrosoftのVisual C++開発環境を使って作成しました。(他の開発環境を使っているユーザーも、この例で何が作成されているかは理解できるはずです。) サンプルのソース・コードは、EMPEDTサブディレクトリ (emp表エディタ) に入っています。
この例では、例4の接続ダイアログを使います。接続ダイアログは、ユーザーがOracleデータベースにログインするための便利なインタフェースを提供します。主画面は1つのウィンドウで、これにはemp表のすべてのフィールドを表示し、編集できるフォームが含まれています。このフォームをMFCのフォーム・ビューとして作成しました。表のレコードの編集には、クラス・ライブラリと一緒に提供されるOMFCのバインド済みコントロール・クラス
(OBinderおよびOBoundを使って作成) を使います。この例には、社員レコードが新しく追加されたときに、一意の社員番号を自動的に生成する機能も含まれています。このアプリケーションを作成するために必要なコード量は、おどろくほど少なくて済みます。
アプリケーション・レイアウト
元のアプリケーションはAppWizardで生成しました。ビュー・クラスをフォーム・ビューに変更して、App
Studioリソース・エディタを使ってユーザー・インタフェースを迅速に作成できるようにしました。アプリケーションのメモリー・モデルはラージに設定する必要があります。
oraclmおよびomfc、ole2、ole2dispの各ライブラリにリンクするようにプロジェクトを設定しました。クラス・ライブラリを使うためにOLEと対話する必要はありませんが、クラス・ライブラリがOLEを内部的に使っているため、OLEにリンクする必要があります。
例4の接続ダイアログを使います。ファイルLOGDLG.CPPおよびLOGDLG.Hをプロジェクトに追加しました。次にダイアログ・リソースIDD_LOGINDをLOGDLG.RCリソース・ファイルからEMPEDT.RCリソース・ファイルにコピーしました。
アプリケーション・クラスに簡単な変更を1つ加えました。つまり、CEmpedtApp::InitInstanceにクラス・ライブラリの初期化コール (OStartup) を入れました。アプリケーション・クラス (~CEmpedt) にデストラクタ・メソッドを追加し、その中にOShutdownへのコールを入れました。
フォームの作成
このアプリケーションの目的は表empの編集です。表empはOracleの標準的なデモ用の表の1つです。表empには次の8つのフィールドがあります。ename
(社員名)、empno (社員番号)、job (社員の仕事を記述するキーワード)、mgr (社員の所属長の社員ID)、sal
(社員の給与)、comm (社員に支払われるコミッション、NULLの場合がほとんどである)、hiredate
(社員の入社日付)、deptno (社員の所属部門の識別番号、表deptを参照するために使われる)
の8つです。
この表を編集するために、もっとも簡潔なフォームを作成しました。データベースのフィールド1つにつき、テキスト編集が1つです。フォームはApp Studioで編集しました。それぞれのエディット・コントロールに一意のIDを付けました。各エディット・コントロールの隣にラベルとしてスタティック・テキスト・アイテムを配置しました。エディット・コントロールのID、順序、ラベルのテキスト、ラベルとコントロールの関連はユーザーに一任されています。Class Wizardの想定によってではなく、ユーザーのコードによって、それぞれのエディット・コントロールがデータベースのフィールドに付加されます。
エディット・コントロールには変数もメッセージ・マップも作成されません。エディット・コントロールはOBoundEditクラスによって完全に管理されるものです。
「Connect」というボタンをフォームに追加しました。「Connect」ボタンは、ユーザーがOracleデータベースに接続するために押します。Class Wizardを使って、「Connect」ボタンをクリックしたときに実行されるメソッドを作成しました。このメソッドをOnConnectと呼びます。
バインディング
フォーム・ビュー・クラスCEmpedtViewがフォーム・ビューの操作を制御します。OBinderインスタンスを1つ、メンバー変数としてビュー・クラスに追加しました。このインスタンスの名前はm_empblockです。(実際にはOBinderEmpとして宣言されます。OBinderEmpはOBinderのサブクラスです。後述の「エラー処理」を参照してください。)
このOBinderインスタンスがデータベース接続およびダイナセット、データベースのデータを編集するために必要な整合性管理をすべて管理します。次に、データベース・フィールドにバインドしたいユーザー・インタフェースのウィジェットのそれぞれに対して、OBoundサブクラスのインスタンスが必要です。エディット・コントロールをユーザー・インタフェースで使っているため、OBoundEditインスタンスを使いました。それぞれのエディット・コントロール用に、型がOBoundEditのメンバー変数を1つ、CEmpedtViewに追加しました。(empnoのメンバー変数は、OBoundEditのサブクラスのインスタンスです。後述の「トリガーの使用」を参照してください。)
OBoundクラスおよびサブクラスのメソッドをコールすると、エディット・コントロールはデータベースのフィールドにバインドされます。このバインディングはデータベースが接続された後で行われます。簡便のため、バインディング用コードはすべてOnConnectメソッドの中に入れました。
OnConnectメソッドが最初に行うことは、データベースへの接続です。次のようにログイン・ダイアログ・クラスをコールして接続します。
// get an ODatabase object via the connection dialog
logdlg connd;
// get a database object
ODatabase odb = connd.GetLogin(ODATABASE_PARTIAL_INSERT | ODATABASE_EDIT_NOWAIT);
if (!odb.IsOpen())
{ // didn't get a connection - user must have canceled
ログイン・ダイアログは (ダイアログを介して) データベース名、ユーザー名、およびパスワードを入力するようユーザーにプロンプトを表示し、Oracleデータベースへの接続を試みます。接続できると、Oracleデータベースを参照する、オープンされたODatabaseオブジェクトを戻します。接続できない場合は、エラー・ダイアログの表示を試みます。ユーザーがログイン・ダイアログを取り消した場合は、オープンされていないODatabaseオブジェクトが戻されます。そこでOnConnectが戻されたODatabaseをチェックして、ODatabaseがオープンされているかどうかを調べます。これにより、ユーザーがログイン・ダイアログで接続を取り消したか、またはOracleデータベースに正常に接続したかをチェックします。
データベースは部分挿入オプションおよびノーウェイト・オプションでオープンされます。部分挿入オプションは、レコードが追加または編集されたときに、データベースによって設定されたデフォルトのフィールド値がダイナセットに正しく反映されるようにします。ノーウェイト・オプションは、編集したいレコードが他のユーザーによってロックされている場合、そのロックが解除されるまでは待たないようにします。かわりにエラーが発生します。
OBoundEditコントロールを2つの方法で付加する必要があります。まず、特定のOBinder内で使用可能になる特定のフィールドに付加します。次に、特定のユーザー・インタフェース・ウィジェットに付加する必要があります。最初の付加は、次に示すように、OBound::BindToBinderへのコールを使って行います。
m_ename.BindToBinder(&m_empblock, "ename");
このコールは、OBinder内で使用できる「ename」フィールドにm_enameメンバー変数をバインドします。バインダ・オブジェクト
(m_empblock) がオープンされると、enameフィールドが使用できるようになります。バインダのSQL文が実行される前に、コントロールをバインダにバインドしても有効です。
2番目の付加はBindToControlのコールで行えます。BindToControlはOMFCおよびOOWLライブラリのクラスで使用できるメソッドです。
m_ename.BindToControl(this, IDC_ENAME);
このコールは、「this」で識別されているウィンドウのIDC_ENAMEにより識別されるユーザー・インタフェース・ウィジェットに、m_enameメンバー変数をバインドします。OConnectはフォーム・ビュー・クラスのメソッドなので、「this」はフォーム・ウィンドウを指します。
OBoundEditコントロールがすべてOnConnectメソッドにフックされた後は、次のようにSQL文を実行してOBinderをオープンできます。
m_empblock.Open(odb, "select * from emp order by empno");
この文は、ダイナセットを作成し、データベースからレコードを取り出し、データベース・フィールドの正しい値をすべてのエディット・コントロールに入れます。
ボタン
ログイン・ダイアログとエディット・コントロールをインプリメントしたら、アプリケーションを作成し、実行できます。アプリケーションはきちんと作動して、最初のレコードの値を表示します。その他の機能をインプリメントできるように、フォームには他のボタンも追加しました。
「First」、「Prev」、「Next」、「Last」というボタンを追加して、ナビゲーションをインプリメントしました。Class Wizardを使って、ボタンをクリックして始動するメソッドをそれぞれのボタン用に作成しました。各ボタンはたった1行のコードでインプリメントしてあります。たとえば、Nextは次のようにインプリメントします。
m_empblock.MoveNext();
OBinder::MoveNextをコールすると、ダイナセットを次のレコードに移動し、(必要なら) データベースからそのレコードを取り出し、新しいレコードを表示するためにすべてのエディット・コントロールの値を更新します。実際はこれ以上のことが実行されます。レコード内のデータを変更するためにエディット・コントロールが使われた場合、OBinder::MoveNextは次のレコードに移動する前にその変更をデータベースに保存します。これがすべて「MoveNext」と書くだけで実行されます。
レコード・レベルの編集用にいくつかボタンを追加しました。「Add New」、「Duplicate」、「Delete」ボタンです。これらも簡単にインプリメントできて、それぞれコードを1行書くだけです。ここでもOBinderが必要な整合性の管理を行います。
トリガーの使用方法
表empのempnoフィールドには、それぞれのレコードに対して一意の番号を入れる必要があります。「Add
New」または「Duplicate」を使って新規レコードが追加されるたびに、ユーザーに手動で一意の番号を作成させるのではなく、ユーザーのために一意の番号を設定するコードを追加しました。
一意の番号を計算するのは難しくありません。1つの方法としては、すべてのレコードに渡ってempnoの現在の最大値をOracleデータベースに問い合せる方法があります。この結果の値に1を足せば、現在データベースに存在するレコードに関しては、一意の値が保証できます。(もっと良い方法は順序を使うことです。順序の詳細はOracleのドキュメンテーションを参照してください。) .
この計算はいつ行って、結果はどう処理したら良いのでしょうか。計算は新規レコードが追加されるたびに行う必要があります。「Add New」または「Duplicate」ボタンが押されるたびに計算しますが、その後でempnoエディット・コントロールの値を手動で変更することもできます。しかしそうすると、OBinderがデータベースを正しく更新できるように、OBound::Changedをコールして、OBoundEditインスタンスに値が変更されたことを通知する必要があります。この方法でもできますが、エラーが発生しやすくなります。たとえば、レコードを追加する他のメソッドを追加した場合、どうなるでしょう。
この方法のかわりに、empnoエディット・コントロールの動作を変更しました。OBinderダイナセットに新しくレコードが追加されるたびに、そのOBinderにバインドされているOBoundのインスタンスが、レコードが追加される前にPreAddメソッドをコールし、レコードが追加された後にはPostAddメソッドをコールします。OBoundEditのPostAddメソッドは何もしません。OBoundEditからサブクラスを作成して、OBoundEmpnoEditという新しいクラスを作成し、このクラスに対してデフォルトのPostAddを上書きするPostAddメソッドを定義しました。empnoメンバー変数をOBoundEmpnoEditのインスタンスとして宣言しました。OBoundEmpnoEditはOBoundEditのサブクラスで、EMPEDVW.HおよびEMPEDVW.CPPにインプリメントされています。
実行時に新規レコードがダイナセットに追加されるたびに、OBoundEmpnoEdit::PostAddメソッドがレコードが追加された後にコールされます。このルーチンは自身の値を設定しますが、これが計算した一意の番号にエディット・コントロールの値を設定します。
トリガー・メソッドはすべて仮想なので、OBinderはOBoundEmpnoEditをあたかも通常のOBoundであるかのように管理します。しかしこの例ではアプリケーションのニーズに合うように、OBoundのデフォルトの動作を上書きしました。
この例ではOBoundEmpnoEditはプログラムの残りの部分からの情報を何も必要としませんが、実際のプログラムでは、バインド済みコントロールの操作はプログラムの他の部分で何が起きているかに依存します。バインドされたオブジェクトにはコンテキストが必要です。その実行の一方法を示すために、サンプル・コードでは有用なコンテキストをバインドされたオブジェクトに渡すSetContextメソッドもインプリメントしています。このコンテキストがメソッドで使用できるようになります。
エラー処理
サンプル・アプリケーションでは最小限のエラー処理を実行しています。エラーが発生したら、ユーザーにエラー・メッセージを表示します。
発生する可能性のある最初のエラーとしては、クラス・ライブラリまたはクラス・ライブラリが依存するコンポーネントを、ロードまたは初期化できないというエラーがあります。データベースにログインする直前にこのエラーを捕捉します。次のコードを使います。
// get the default session
OSession defsess(0);
if (!defsess.IsOpen())
{ // couldn't get default session? Class library isn't working
クラス・ライブラリを初期化できないと、デフォルトのOSessionオブジェクトをオープンできません。
次に発生する可能性のあるエラーは、OBinderオブジェクトをオープンできないというものです。これには次のコードを使います。
// check for error
if (ores != OSUCCESS)
{ // we couldn't open the dynaset
// give the user a message
AfxMessageBox(msg);
ここで、oresはOBinder::Openコールの結果です。発生する可能性の高いエラーとしては、SQL文のエラー (表が存在しない、またはフィールド名のどれかが不適切である) か、またはユーザーが表を読み込む許可を持っていないというエラーです。これらは両方ともOracleのエラーです。
これらのエラーのどちらかが発生した場合、アプリケーションは全く作動しません。これらのエラーは一般にクライアント・プログラムのバグで、正しいコードを作成することで (たとえばSQL文を正しくすることで) 避けられます。OBinderをオープンしたら、表のレコードを参照します。そうなると、他のユーザーとの対話やシステム・リソースの不足などの他の種類のエラーも考慮する必要があります。
フィールドの値を編集しようとするときに、もっとも一般的なエラーが発生します。フィールドの値を変更しようとすると、OBinderインスタンスがダイナセットにStartEditを試みます。これはいろいろな理由で失敗する可能性があります。もっとも一般的な理由としては、サーバーへの接続が失われた、他のユーザーが行をロックしている、他のユーザーが行のデータを変更してしまった、などがあります。最初の2つはOracleのエラーとしてレポートされます。最後のエラーは、クラス・ライブラリにより特に生成されるエラーで、データベースのデータが間違って上書きされないようにするためのものです。
ここで難しいのは、OBoundEditコントロールを使った場合エディット・コントロールに入力するとデータが変更されてしまうということです。このプログラムではメソッド・コールは何も実行していません。コントロールにエラーをレポートさせるにはどうしたら良いでしょう。
OBinderクラスにはOnChangeErrorというメソッドがあり、これは上書きできます。ダイナセットにStartEditを処理している最中にエラーが発生したときに、これがコールされます。この例では、レコードの編集が始まったときに、このメソッドを使ってユーザーにエラーを通知します。OBinderクラスからOBinderEmpサブクラスを作成して、メンバー変数をその型であると宣言します。そうすると、ユーザーがレコードに値を入力してエラーが発生すると、OBinderEmp::OnChangeErrorルーチンがコールされます。
最後に、フォーム上のボタンで表されるいろいろな操作の最中にもエラーは発生する可能性があります。さまざまなレコードに移動したときや、レコードの追加や削除をしたときなどです。これらの操作の最中に発生するエラーはビューのHandleErrorメソッドに送られます。HandleErrorはナビゲーションまたはレコードの追加や削除、変更レコードのデータベースへの保存のときに発生するエラーを処理します。ナビゲーション・メソッドやレコードを追加するメソッドは、作業をする前に現レコードに変更を保存します (これは実際にはPreOperationトリガー関数を介して行われます)。
HandleErrorは正しいエラー・メッセージを取り出して (または計算して)、ユーザーにエラーをレポートします。