DBGridにSELECT結果を表示させるには

解決


yTake  2013-02-05 04:26:07  No: 43725

yTakeです。度々、お世話になっています。

今回はデータベースに関してご教授頂ければと思います。

DELPHI XEでFirebirdをDBとして使用する事とします。
コンポーネントは、IBTable, IBQuery, IBTransaction, IBDatabase, DataSource に DBGrid, DBNavigaor を使用しています。

コンポーネントを配置し関係性を設定しただけで、Firebirdデータベースからその内容が表示されレコード間移動や追加編集がコンポーネント操作だけでSQL分を一切用いず実現され、便利さに驚いていますが、少し複雑な処理をさせる場合のプログラミング手法がよくわかりません。

具体的には、検索条件を設定してその結果をDBGrid上に表示させたいのですが、SQL文を作成しExecuteしてその結果まで得られているのですが、DBGridへの表示の仕方がわかりません。

ソース抜粋:
Form2.IBQuery2.SQL.Clear();
Form2.IBQuery2.SQL.Add( 'SELECT  *  FROM  patient_tbl ' );
Form2.IBQuery2.SQL.Add( '    WHERE  MU_CALC_DATE  BETWEEN ''' +        FormatDateTime( 'yyyy-mm-dd', begin_Date ) +  ''' AND ''' +        FormatDateTime( 'yyyy-mm-dd', end_Date ) + '''' );
Form2.IBQuery2.Open();
if Form2.IBQuery2.IsEmpty = True then
    ShowMessage( 'Empty' )
else
begin
    Form2.IBQuery2.RecordCount;
    Form2.IBQuery2.FieldCount;
    while Form2.IBQuery2.Eof = False do
    begin
        for i := 0 to Form2.IBQuery2.FieldCount - 1 do
        begin
//            Form2.DBGrid2.Fields[ i ] ;

            Form2.IBTable2.FieldValues[ 'INFO5' ] := Form2.IBQuery2.FieldByName( 'INFO5' ).Value;
        end;
        Form2.IBQuery2.Next();
    end;
end;
Form2.IBQuery2.Close();
ここまで、

試しに一つのフィールドのみ代入させてみましたが、EIBClientError:’編集中ではありません’が出ます。
DBGrid2.Fields[ i ]へ直接代入しようとも思いましたが、読み出し専用なので不可です。

なお、IBQuery2.RecordCountで検索に掛かったレコード数が反映されると思いましたが、違う様です。複数ヒットさせても、常に'1'でした。

どの様に、DBGridに結果を表示させるべきでしょうか?
参考になるサイトがあればそこをご紹介頂けるだけでも構いません。
これまでも色々と参照しましたが、よく分かりませんでした。

以上です。よろしくお願いします。


DEKO  2013-02-05 05:44:09  No: 43726

1.空のプロジェクトを開いて以下をコピーし、フォームデザイナに〔Ctrl〕+〔V〕で貼り付けて下さい。

-------------------- ここから --------------------
object DBGrid1: TDBGrid
  Left = 8
  Top = 8
  Width = 661
  Height = 317
  DataSource = DataSource1
  TabOrder = 0
  TitleFont.Charset = DEFAULT_CHARSET
  TitleFont.Color = clWindowText
  TitleFont.Height = -11
  TitleFont.Name = 'Tahoma'
  TitleFont.Style = []
end
object IBDatabase1: TIBDatabase
  Left = 20
  Top = 340
end
object IBTransaction1: TIBTransaction
  DefaultDatabase = IBDatabase1
  Left = 80
  Top = 340
end
object IBQuery1: TIBQuery
  Database = IBDatabase1
  Transaction = IBTransaction1
  Left = 160
  Top = 340
end
object DataSource1: TDataSource
  DataSet = IBQuery1
  Left = 240
  Top = 340
end
object Button1: TButton
  Left = 513
  Top = 344
  Width = 75
  Height = 25
  Caption = 'Connect'
  TabOrder = 1
end
object Button2: TButton
  Left = 594
  Top = 344
  Width = 75
  Height = 25
  Caption = 'DisConnect'
  TabOrder = 2
end
-------------------- ここまで --------------------

2.Button1 (Connect) のイベントハンドラに以下を記述します。

procedure TForm1.Button1Click(Sender: TObject);
begin
  with IBDatabase1 do
    begin
      LoginPrompt := False;
      Params.Values['user_name'] := 'SYSDBA';      // User Name
      Params.Values['password' ] := 'masterke';    // Password(IBならmasterkey)
      Params.Values['lc_ctype' ] := 'UNICODE_FSS'; // CharSet
      DatabaseName               := 'localhost:C:\TEST\DATA.FDB';
    end;
  with IBQuery1 do
    begin
      SQL.Clear;
      SQL.Add('Select * From TBL_EMPLOYEE');
      Open;
    end;
end;

3.Button2 (Disconnect) のイベントハンドラに以下を記述します。

procedure TForm1.Button2Click(Sender: TObject);
begin
  IBQuery1.Close;
end;

4.実行してテストしてみてください。

> IBQuery2.RecordCountで検索に掛かったレコード数が反映されると思いましたが、違う様です。
違いますね。

IBQuery2.Last;
IBQuery2.First;

または、

IBQuery2.FetchAll;

した後だと RecordCount は総レコード数が返ります。

ただ、事前に Select Count(*) した SQL を投げて総レコード数を得たほうが
パフォーマンスがいい事が多いです。

詳しくは以下のページを参照して下さい。

[FAQ - InterBase & IBX ヘルプ (木村さんによる翻訳)]
http://www.geocities.jp/kimura804/rdb/InterBaseFAQ/IBandIBX_FAQandTutorials_j.htm#Top


DEKO  2013-02-05 05:50:00  No: 43727

以下は適宜書き換えてください。

・データベースの文字コードセット
・データベース名
・テーブル名を含む SQL 文


yTake  2013-02-05 09:55:06  No: 43728

DEKOさん
早速のREPLYをありがとうございます。

早速試してみました。すると、見事に検索結果が反映されています。

みたところ、特にDBGridに対して代入などは行なわれていません。
ご案内して頂いたサイトを参照しながら、理解してゆきたいと思います。


yTake  2013-02-05 21:14:33  No: 43729

少し分かって来ました。

IBTableは下位互換性の為で使用しない方が良く使う場合IBDataSetが代用可能とあります。
いずれも使わなくてもDEKOさんの例の様に実現できる事も分かりました。

ところが、実は現在設計中のDBソフトはデータベースファイルは一つですが、テーブルを3つ内包しています。
この内、二つは連携していて、一方の選択レコードに対応してもう一方のレコード内容を表示する様にしていました。
具体的には、
関連データの方の"IBTable"で、MasterSourceとして関連されるテーブル用の"DataSource"を指定し、"MasterFields"に関連されるテーブルから共通データが格納されているフィールドを、"IndexFieldName"に関連データのテーブルで共通データが格納されているフィールドを指定しています。

これで、曲がりなりにも、両者は想定通りに連携して内容を表示していました。
今回、IBTableに代えてIBDataSetを置いてみました。取り敢えず、親の方のテーブル(関連される方)はその内容を表示していますが、子のテーブル(関連データの方)は連携のさせ方が分かりません。
IBDataSet内には、IBTableの様な、MasterSource設定が見当たりません。連携させるためのフィールド設定の様な箇所も無い様に思います。
コンポーネントが別なので、全く同様とは思いませんが、設定項目が少なく
根本的にやり方が違うのでしょうか?

少々、分かり難い説明かもしれませんが、2つのテーブルを連携させてレコード内容を表示させたいと考えています。

以上です。よろしくお願いします。


yTake  2013-02-05 23:02:42  No: 43730

自己レスです。

IBTableの方はいわば常時接続的な感じなので、コンポーネントレベルで参照先の設定が可能だったのではと解釈しました。

IBDataSetの場合、よりC/S的になるので、SQL文を発行して連携データを毎回引っ張ってくる必要があると考えました。

IBDataSetの"SelectSQL"へ親レコードの参照フィールド値と同じフィールド値を持つレコードをSELECTするSQL文を登録すると考えます。

しかし、別テーブルのフィールド値を条件とするWHERE句の記述が分かりません。
”SELECT * FROM Table1 WHERE Field1 FROM Table1 = Field1 FROM Table2”当然ではダメでした。

方向性としては、正しいのでしょうか?


DEKO  2013-02-06 04:36:40  No: 43731

SQL の Left Join でいいのではないでしょうか?

[Firebird Wiki]
http://firebirdwiki.jp/index.php?SELECT


HOta  2013-02-06 06:25:42  No: 43732

このあたりは、SQL文の学習が必要になりますね。

SELECT T1.* FROM Table1 T1 
  Left Outer join Tabel2 T2 on (T1.Field1 = T2.Field1)

[T1,T2はAlias(別名)で無い場合はTable名.Field名で表す]

だと思います。

標準SQLだと、Table結合にはInner Join,Left outer join等があります。
調べればすぐに出てくると思います。


yTake  2013-02-06 10:31:09  No: 43733

DEKOさん、HOtaさん
ありがとうございます。
確かにSQLの話になってきていてここで教えて頂く事ではないかもしれません。
ただ、今回"Left Join", "Inner Join"でもうまく表示されていません。エラーは特に無いようです。

ここで、私の状況説明に不正確性があった為かもしれません。
この様なコンポーネントの使い方で良いでしょうか?
データベースファイルは一つでテーブルが二つである事は始めにお伝えしてたと思います。実は、DBGridやIBQuery等のコンポーネントも2組使用しています。
DBGrid1              DBGrid2
DBNavigator1         DBNavigator2
IBDataSet1(IBTable1) IBDataSet2(IBTable2)
IBDataSource1        IBDataSource2
IBQuery1             IBQuery2
IBTrabsaction1       IBTransaction2
IBDatabase1          IBDatabase2
また、テーブルは例えばクラス名簿と試験結果表の様なものと考えて下さい。
試験結果も外部模試の様な個人によって受験数などが異なる様なテーブルを想定しています。
つまり、クラス名簿の方は重複がなく、行を選択する(個人を選ぶ)と対応する模試結果が表示され、これは個人によって連携して表示される行数が変わる事になります。

つまり、”JOIN”の様なテーブル結合では実現できない様な気がします。
JOINだと、結果は一つのDBGridにまとめて表示される気がします。
DBGrid1-IBDataSet1 と DBGrid2-IBDataSet2 の間で、
テーブル1のアクティブレコードのIDをキーにTable2で対応するIDを全てDBGrid2上に表示する方法があるでしょうか?

この様な場合でも、JOIN系を用いれば良いのでしょうか?

Table1のアクティブレコードの指定が分かりません。(Firebird入門を参照)

質問の意図は伝わったでしょうか?


DEKO  2013-02-06 12:45:49  No: 43734

質問の趣旨と変わっているような...まぁいいか。
以下のようなテーブルがあって CODE で連動する場合を考えてみます。

/* 親データ */
CREATE TABLE TBL_MASTER 
(
  CODE  INTEGER NOT NULL,
  NAME  VARCHAR(100),
 PRIMARY KEY (CODE)
);
INSERT INTO TBL_MASTER (CODE, NAME) VALUES (1, 'AAAAA');
INSERT INTO TBL_MASTER (CODE, NAME) VALUES (2, 'BBBBB');
INSERT INTO TBL_MASTER (CODE, NAME) VALUES (3, 'CCCCC');
INSERT INTO TBL_MASTER (CODE, NAME) VALUES (4, 'DDDDD');
INSERT INTO TBL_MASTER (CODE, NAME) VALUES (5, 'EEEEE');
commit;

/* 子データ */
CREATE TABLE TBL_DETAIL 
(
  CODE  INTEGER NOT NULL,
  SEQ  INTEGER NOT NULL,
  POINT  DOUBLE PRECISION,
 PRIMARY KEY (CODE, SEQ)
);
INSERT INTO TBL_DETAIL (CODE, SEQ, POINT) VALUES (1, 1, 10);
INSERT INTO TBL_DETAIL (CODE, SEQ, POINT) VALUES (1, 2, 20);
INSERT INTO TBL_DETAIL (CODE, SEQ, POINT) VALUES (2, 1, 30);
INSERT INTO TBL_DETAIL (CODE, SEQ, POINT) VALUES (2, 2, 40);
INSERT INTO TBL_DETAIL (CODE, SEQ, POINT) VALUES (3, 1, 50);
INSERT INTO TBL_DETAIL (CODE, SEQ, POINT) VALUES (3, 2, 60);
INSERT INTO TBL_DETAIL (CODE, SEQ, POINT) VALUES (4, 1, 70);
INSERT INTO TBL_DETAIL (CODE, SEQ, POINT) VALUES (4, 2, 80);
INSERT INTO TBL_DETAIL (CODE, SEQ, POINT) VALUES (5, 1, 90);
INSERT INTO TBL_DETAIL (CODE, SEQ, POINT) VALUES (5, 2, 100);
commit;

言わんとする事は、恐らく DBGrid1 に親データ (TBL_MASTER) を表示し、
DBGrid1 で選択されたレコードに関連する子データ (TBL_DETAIL) を
DBGrid2 に表示したいという事なのでしょう。

1.空のプロジェクトを開いて以下をコピーし、フォームデザイナに〔Ctrl〕+〔V〕で貼り付けて下さい。

-------------------- ここから --------------------
object IBDatabase1: TIBDatabase
  Left = 24
  Top = 216
end
object IBTransaction1: TIBTransaction
  DefaultDatabase = IBDatabase1
  Left = 84
  Top = 216
end
object IBQuery1: TIBQuery
  Database = IBDatabase1
  Transaction = IBTransaction1
  Left = 160
  Top = 216
end
object DataSource1: TDataSource
  DataSet = IBQuery1
  Left = 240
  Top = 216
end
object DBGrid1: TDBGrid
  Left = 8
  Top = 8
  Width = 300
  Height = 200
  DataSource = DataSource1
  TabOrder = 0
end
object IBQuery2: TIBQuery
  Database = IBDatabase1
  Transaction = IBTransaction1
  Left = 160
  Top = 276
end
object DataSource2: TDataSource
  DataSet = IBQuery2
  Left = 240
  Top = 276
end
object DBGrid2: TDBGrid
  Left = 316
  Top = 8
  Width = 300
  Height = 200
  DataSource = DataSource2
  TabOrder = 1
end
object Button1: TButton
  Left = 447
  Top = 215
  Width = 75
  Height = 25
  Caption = 'Connect'
  TabOrder = 2
end
object Button2: TButton
  Left = 538
  Top = 215
  Width = 75
  Height = 25
  Caption = 'DisConnect'
  TabOrder = 3
end
-------------------- ここまで --------------------

2.Form1 の OnCreate イベントハンドラに以下を記述します。

procedure TForm1.FormCreate(Sender: TObject);
begin
  with IBDatabase1 do
    begin
      LoginPrompt := False;
      Params.Values['user_name'] := 'SYSDBA';      // User Name
      Params.Values['password' ] := 'masterke';    // Password(IBならmasterkey)
      Params.Values['lc_ctype' ] := 'UNICODE_FSS'; // CharSet
      DatabaseName               := 'localhost:C:\TEST\DATA.FDB';
    end;
  with IBQuery1 do
    begin
      SQL.Clear;
      SQL.Add('Select * From TBL_MASTER');
    end;
  with IBQuery2 do
    begin
      SQL.Clear;
      SQL.Add('Select * From TBL_DETAIL');
      SQL.Add('Where                   ');
      SQL.Add('  CODE = :_CODE         ');
      SQL.Add('ORDER BY                ');
      SQL.Add('  SEQ                   ');
    end;
end;

3.Button1 (Connect) の OnClick イベントハンドラに以下を記述します。
procedure TForm1.Button1Click(Sender: TObject);
begin
  IBQuery1.Open;
end;

4.Button2 (DIsconnect) の OnClick イベントハンドラに以下を記述します。
procedure TForm1.Button2Click(Sender: TObject);
begin
  IBQuery2.Close;
  IBQuery1.Close;
end;

5.DataSource1 の OnDataChange イベントハンドラに以下を記述します。
procedure TForm1.DataSource1DataChange(Sender: TObject; Field: TField);
begin
  IBQuery2.Close;
  IBQuery2.ParamByName('_CODE').AsInteger := IBQuery1.FieldByName('CODE').AsInteger;
  IBQuery2.Open;
end;

6.実行してテストしてみてください。

# もっと効率的なやり方があったような気がしましたが、
# DBGrid は殆ど使った事がないので忘れているのかもしれません。


maru03  2013-02-07 01:08:50  No: 43735

yTake さん

>また、テーブルは例えばクラス名簿と試験結果表の様なものと考えて下さい。
>試験結果も外部模試の様な個人によって受験数などが異なる様なテーブルを想定しています。
>つまり、クラス名簿の方は重複がなく、行を選択する(個人を選ぶ)と対応する模試結果が表示され、
>これは個人によって連携して表示される行数が変わる事になります。

やりたいことについての確認ですが、

------------------
|N_CODE |NAME  |
------------------
|1   |A君  |
|2   |B君  |
|3   |C君  |
|4   |D君  |
|5   |E君  |
------------------

--------------------------
|N_CODE|KYOUKA|TESUU  |
--------------------------
|1  |数学  |100  |
|2  |数学  |80  |
|2  |英語  |90  |
|3  |数学  |80  |
|3  |英語  |95  |
|3  |社会  |75  |
|4  |数学  |70  |
|4  |英語  |90  |
|5  |数学  |95  |
--------------------------

のようなクラス名簿と試験データの連動と理解すればよいのですね?

A君は一つの試験「数学」
B君は2つの試験「数学」「英語」
・・・・・のようなことを表示したい。ですか?

このような目的であれば、もっと簡単にアプローチが可能と思われますが・・・


Quest  2013-02-07 05:06:12  No: 43736

多分、ノンコーディング(SQLを除く^^;)でそれらしいことができます。

フォームにTIBDatabase、TIBTransaction、TIBQueryを2つ、TDataSourceを2つ、
TDBGridを2つ載せます。
IBDatabaseとIBTransactionを関連付け、それぞれ必要なパラメータを設定し
データベースに接続できるようにします。
IBQuery1、DataSource1、DBGrid1ともIBQuery2、DataSource2、DBGrid2も
それぞれ関連付けます。
で、IBQuery1をマスター側、IBQuery2をディテール(詳細、子)側とするなら
IBQuery2のDataSourceプロパティにDataSource1を設定します。
それぞれのSQLですが、DEKOさんが例として提示されたテーブル定義を借りるなら
IBQuery1のSQLに
SELECT * FROM TBL_MASTER
と設定し
IBQuery2のSQLに
SELECT * FROM TBL_DETAIL WHERE CODE=:CODE
と設定します。
これで、IBQuery1、IBQuery2のActiveプロパティをTrueにすれば
お望みの表示になると思います。
とりあえずそのまま実行すれば、DBGrid1で選択される行を移動すると
それに対応したデータがDBGrid2に表示されます。
要は、ディテール側IBQueryのDataSourceプロパティにマスター側の
DataSourceを設定する事と、ディテール側のSQLのWHERE区で
<ディテール側テーブルのキーフィールド名>=:<マスター側テーブルのキーフィールド名>
を設定する事です。
ただ、この方法のパフォーマンスについては検証してないのであしからず ^^;


DEKO  2013-02-07 11:44:53  No: 43737

>> もっと効率的なやり方があったような気がしましたが、
> 多分、ノンコーディング(SQLを除く^^;)でそれらしいことができます。
それです!

私の二度目の投稿のコードをちょっと修正すると、
Quest さんの仰っている動作になります。

1.IBQuery2 の SQL を変更
  with IBQuery2 do
    begin
      SQL.Clear;
      SQL.Add('Select * From TBL_DETAIL');
      SQL.Add('Where                   ');
      SQL.Add('  CODE = :CODE          '); // <- 修正
      SQL.Add('ORDER BY                ');
      SQL.Add('  SEQ                   ');
    end;
end;

2.Button1 (Connect) の OnClick イベントハンドラを修正
procedure TForm1.Button1Click(Sender: TObject);
begin
  IBQuery1.Open;
  IBQuery2.Open; // <- 追加
end;

3.DataSource1 の OnDataChange イベントハンドラを削除

4.IBQuery2 の DataSource プロパティに DataSource1 を指定


DEKO  2013-02-07 11:57:07  No: 43738

思い出しました。

[Delphi によるデータベースアプリケーションの作成方法 (Support KB)]
http://support.embarcadero.com/article/36434

[Delphi Tips - Query を使ったデータベースアプリケーション開発  (Support KB)]
http://support.embarcadero.com/article/35960

上が TTable、下が  TQuery を使った例ですが、
ほぼそのまま TIBTable、TIBQuery で読み替える事が出来ます。


yTake  2013-02-07 18:23:36  No: 43739

DEKOさん、maru03さん、Questさん
ありがとうございます
yTakeです
少し間が空いてしまった間に皆さまありがとうございました。
順次確認しています。

DEKOさんから指示頂けたやり方は確認できました。
IBTable, IBDataSet共に使わなくても良い事が分かります。
また、DBGridを使用されないとありますが、表に表示される場合はStringGrid等にFor文でまわしてCellへ入れて行くと言う様なやり方でしょうか?
その方が、小回りが利くと言う利点があるのでしょうか。

maru03さん
はいそうです。その様に考えています。また、クラス名簿に相当する方は例えば誕生日も記載され、”期間を区切ると該当する誕生日の生徒が検索される”と言うところが、そもそもの問い合わせの発端でした。

Questさん
ご指摘の点、これから確認してみます。

DEKOさん
何度もありがとうございます。
サイトのご紹介をありがとうございます。
参照した事はあったのですが、上記を踏まえて再読してみます。

取り敢えず、以上です。


maru03  2013-02-08 02:15:46  No: 43740

テーブルの構造について
>はいそうです。その様に考えています。また、クラス名簿に相当する方は例えば誕生日も記載され、
>”期間を区切ると該当する誕生日の生徒が検索される”と言うところが、そもそもの問い合わせの発端でした。

上記の目的から、下記のようなことを考えてみました。

その前に、DBファイル「XXXX.FDB」(拡張子はFDBとしていますが・・)に
「名簿テーブル」「成績テーブル」ともに入っているとすると、

IBDatabase1          (IBDatabase2==>不要)
IBTrabsaction1       (IBTransaction2==>不要)

でいいと思います。

名簿データ、成績データの一部削除や追加を考慮すると、SQLの使用がいいと思います。
結果は、StringGrid等にFor文,While文でまわしてCellへ入れて行きます。

StringGridでダブルクリックすると「名前のCode番号」が取得できるので、
(StringGridのデータは、String型データですので注意が必要です)

    名前のCode番号 := StrToInt(StringGrid1.Cells[0,StringGrid1.row]);

再度、SQL  で必要な成績を取得すればいいのでは。

つまり  

SQL_1  で  名簿テーブルから「条件」で抜き出す
StringGrid_1  に表示  (ここは  ListBox  などでも代用可能でしょう)
StringGrid_1  上でダブルクリックなどで、名前を確定

取得した「名前のCode番号」で  SQL_2  にて必要な成績データを取得
DBGrid  や  StringGrid_2  に表示

  DBGrid  の場合は  TIBQuery  =>  TDataSource  =>  DBGrid  でOK
  StringGrid  の場合はFor文、while not(SQL.Eof) do  でまわしてCellへ入れる  

となるでしょう。

また、StringGrid  なら、色々な表現ができます。
参考  Mr.XRAY  さんのHP
http://mrxray.on.coocan.jp/Delphi/plSamples/050_TStringGrid_OnDrawCell.htm


yTake  2013-02-08 19:03:46  No: 43741

皆さん、maru03さん
yTakeです。ありがとうございます。
おかげさまで希望の動作を実現できました。

maru03さんにご指摘頂いたIBDabase2, IBTransaction2が不要である点、分かってきました。

DBGridを始めに使ってしまて、あまりに簡単に表示できてしまったので、それを使うべきだと思い込んでしまった点が理解の妨げだったかもしれません。

StrinGrid等を使うとデザインの自由度があがりますが、その表示は自分でCellへ書き込む必要があるわけですね。

SQLの応答を受け取る為に、Openして、

抜粋:
j := 1;
while IBQuery2.Eof = False do
begin
   for i := 0 to IBQuery2.FieldCount - 1 do
   begin
      if j = 0 then
         StringGrid1.Cells[ i, j ] :=  IBQuery2.Fields[ i ].FieldName
      else
         StringGrid1.Cells[ i, j ] :=  IBQuery2.Fields[ i ].AsString;
   end;
   IBQuery2.Next();
   inc( j );
end;

などとすると考えます。

StringGridの更なる使い方に関しても、XRrayさんのサイトを参考にさせて頂きます。

ありがとうございました。


※返信する前に利用規約をご確認ください。








  このエントリーをはてなブックマークに追加