« MyBaseを試してみる。(CloneCursorでOnFilterRecord) | トップページ | MyBaseを試してみる。(Index、FilterでAggregateが変!?) »

MyBaseを試してみる。(インデックスを使わずに、行の追加、挿入、削除編)

前にも書きましたが、私がBDE(TTable)+Paradoxを使っていて一番便利だと思っていたところはインデックスを設定しなくてもAppendとInsertを使い分けることによって、レコードの順番が保持されるところでした。「データベースなんだからインデックスは、ちゃんと作ろうよ」と言われそうですが、本当なら、リスト構造+TStringGrid(or TListView)のところをDBGridを使うためだけのParadoxだったので、そんな点をメリットに感じていました。まあDBGridも複数行のDrag&Drop、Cut&Pasteを実装したりして、データベースというより表計算のようなソフトになっていましたけど。

だから私はMyBaseにもそんな仕様?を求めていました。同じように動作するならTTable(BDE)との置き換えが簡単かも知れないと期待しながら、そしてメモリ内なんだからインデックスはあったら便利だけど、無くても動くよね~なんて気軽に考えていました。しかしこれがなかなか難しく、結論としてインデックスがない場合のフィルターには対応できませんでした。
(フィルターについては、前回紹介したCloneCursor+2段DBGridで妥協しました)

インデックスを使わないMyBaseで困ったところ等を挙げます。
その1
ClientDataSetは、Append、Insertメソッドを持っており、単純に追加、挿入の処理が
できます。しかし、DBGrid上で、追加、挿入の処理が正しくできているように見えて
も、そのままファイルに保存して、再度読み込むと挿入も追加扱いになっています。
MergeChangeLogメソッドを呼び出した後、ファイルに保存すると正しいレコードの
順番で書き込まれます。

その2
ClientDataSetは、Deltaというプロパティを持っており、そこに変更されたデータを
保存しています。そのため、元に戻す処理も簡単にできるようになっています。
しかしながら、Insertで挿入したレコードやDeleteで削除したレコードは、最終行に
戻されるだけです。

その3
フィルター処理し解除した場合、挿入したレコードは、追加扱いになっています。
MergeChangeLogメソッドを呼び出して、ファイルに保存すれば正しく動作すること
から、フィルター前にいろんなことを試してみましたが、一度目は成功してもなぜか
二度目以降は正しく動作しませんでした。
(この記事の最後にどのような処理をしたか、書き残しておきますね)

その4
複数行の処理を元に戻すためにSavePointを利用しています。一度に処理した内
容は、一度に元に戻したいからです。
そのため、元に戻す処理は1度しか使えない仕様でプログラムしています。
デザイン画面

Design_3



var
  RecNoList: TList<Integer>; // Undoに使用
  SP: Integer = -1;          // Undoの処理に使用
  SN: Integer = 0;

// サンプルデータ
procedure MakeSample(DataSet: TDataSet);
var
  F: Boolean;
begin
  F := DataSet.Active;
  if not F then
    DataSet.Open;
  try
    // サンプルデータの追加
    with DataSet do
    begin
      Appendrecord(['iPod Touch']);
      Appendrecord(['Zaurus C-860']);
      Appendrecord(['Delphi 2009 Handbook']);
      Appendrecord(['GEORGIA BLACK']);
      CheckBrowseMode;
    end;
  finally
    if not F then
      DataSet.Close;
  end;
end;

// データベースの作成
procedure CreateDB(CDS: TClientDataSet; UsingIndex: Boolean);
var
  I: Integer;
begin
  CDS.Close;
  CDS.FieldDefs.Add('ITEM',ftWideString,20);
  CDS.CreateDataSet;
  CDS.Close;

  for I := 0 to CDS.FieldDefs.Count - 1 do
    CDS.FieldDefs[I].CreateField(CDS);

  CDS.Open;
  MakeSample(CDS);
end;

// DataSetの有効/無効
procedure DataSetEnabled(DataSet: TDataSet; Value: Boolean);
begin
  if Value then
    DataSet.EnableControls
  else
    DataSet.DisableControls;
end;

// SaveToFile
procedure cdsSaveToFile(CDS: TClientDataSet;
  const FileName: String);
begin
  if CDS.ChangeCount > 0 then
    CDS.MergeChangeLog;
  CDS.SaveToFile(FileName);
end;

// LoadFromFile
procedure cdsLoadFromFile(CDS: TClientDataSet;
  const FileName: String);
begin
  CDS.LoadFromFile(FileName);
end;

// 通し番号を返します。別に要らない処理です。
function GetSN: String;
begin
  Inc(SN);
  Result := IntToStr(SN);
end;

// 現在の状態を保存
procedure SaveUndoPoint(CDS: TClientDataSet);
begin
  // 変更があれば更新させます。
  if CDS.ChangeCount > 0 then
    CDS.MergeChangeLog;
  SP := (CDS as TClientDataSet).SavePoint;
end;

// SavePointから元に戻す処理
procedure Undo(CDS: TClientDataSet);
begin
  if SP > -1 then
    CDS.SavePoint := SP;
  SP := -1;
end;

// 削除されたレコードを元に戻す処理 ※コテコテ(^-^)
procedure UndoDelete(CDS: TClientDataSet);
var
  I, RC, RecNo: Integer;
  Delta: TClientDataSet;
begin
  if (RecNoList.Count <> 0) and (CDS.ChangeCount > 0) then
  begin
    Delta := TClientDataSet.Create(nil);
    try
      Delta.Data := CDS.Delta;
      Delta.Open;
      Delta.Last;
      I := -1;
      RC := CDS.RecordCount;

      // Deltaから順番に戻していきます。
      while not Delta.Bof do
      begin
        Inc(I);
        if I < RecNoList.Count then
        begin
          RecNo := RecNoList[I];
          if RecNo > RC then
          begin
            CDS.Append;
            Inc(RC);
          end
          else
          begin
            CDS.RecNo := RecNo;
            CDS.Insert;
            Inc(RC);
          end;
          CDS.FieldByName('Item').AsString :=
            Delta.FieldByName('Item').AsString;
        end;
        Delta.Prior;
      end;
      CDS.CheckBrowseMode;
      CDS.MergeChangeLog;
      RecNoList.Clear;
    finally
      Delta.Free;
    end;
  end;
end;

// Undo
procedure cdsUndoNoIndex(CDS: TClientDataSet);
begin
    if RecNoList.Count > 0 then
    UndoDelete(CDS)            // 削除
  else if (SP > -1)  then
    Undo(CDS)                  // 追加/挿入-SavePoint
  else if CDS.ChangeCount > 0 then
    CDS.UndoLastChange(True);  // 上記以外
end;

// 追加
procedure cdsAppendNoIndex(CDS: TClientDataSet;
  Count: Integer = 1);
var
  I: Integer;
begin
  SaveUndoPoint(CDS);
  DataSetEnabled(CDS, False);
  try
    // Count分空白行を追加します。
    for I := 0 to Count - 1 do
    begin
      // 追加します。
      CDS.Append;
      CDS.FieldByName('ITEM').AsString := '追加'+ GetSN;
    end;
    CDS.CheckBrowseMode;
    // 今回追加した空白行の先頭レコードに移動させます。
    CDS.MoveBy(1-Count);
  finally
    DataSetEnabled(CDS, True);
  end;
end;

// 挿入
procedure cdsInsertNoIndex(CDS: TClientDataSet;
  Count: Integer = 1);
var
  I: Integer;
begin
  SaveUndoPoint(CDS);
  DataSetEnabled(CDS, False);
  try
    for I := 0 to Count -1 do
    begin
      // 挿入します。
      CDS.Insert;
      CDS.FieldByName('ITEM').AsString := '挿入'+ GetSN;
    end;
      CDS.CheckBrowseMode;
  finally
    DataSetEnabled(CDS, True);
  end;
end;

// 削除
procedure cdsDeleteNoIndex(CDS: TClientDataSet;
  BMList: TBookmarkList = nil);
type
  TIntegerComparer = TComparer<Integer>>;
var
  I: Integer;
begin
  // セル編集時は、BMList.Countは0です。
  // その場合の処理は含まれていないです。
  if not Assigned(BMList) then Exit;

  RecNoList.Clear;
  SaveUndoPoint(CDS);
  DataSetEnabled(CDS, False);
  try
    if Assigned(BMList) and (BMList.Count > 0) then
    begin
     for I := BMList.Count -1 downto 0 do
      begin
        CDS.GotoBookmark(BMList[I]);
        RecNoList.Add(CDS.RecNo);
        CDS.Delete;
      end;
      BMList.Clear;
    end;

    // RecNoListのソート
    RecNoList.Sort(TComparer<Integer>.Default);
  finally
    DataSetEnabled(CDS, True);
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  // Undo用
  RecNoList:= TList<Integer>.Create;
  // データベースの作成
  CreateDB(ClientDataSet1, False);
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  FreeAndNil(RecNoList);
end;

procedure TForm1.ClientDataSet2FilterRecord(DataSet: TDataSet;
  var Accept: Boolean);
begin
  Accept :=
    AnsiPos(Edit2.Text, DataSet.FieldByName('ITEM').AsString) > 0
end;

// SaveToFile
procedure TForm1.SpeedButton1Click(Sender: TObject);
var
  FileName: String;
begin
  FileName := ExtractFilePath(Application.ExeName) + 'MyBase.cds';
  cdsSaveToFile(ClientDataSet1,FileName);
end;

// LoadFromFile
procedure TForm1.SpeedButton2Click(Sender: TObject);
var
  FileName: String;
begin
  FileName := ExtractFilePath(Application.ExeName) + 'MyBase.cds';
  if FileExists(FileName) then
    cdsLoadFromFile(ClientDataSet1, FileName);
end;

// 追加
procedure TForm1.SpeedButton3Click(Sender: TObject);
begin
  cdsAppendNoIndex(ClientDataSet1, StrToIntDef(Edit1.Text,1));
end;

// 挿入
procedure TForm1.SpeedButton4Click(Sender: TObject);
begin
  cdsInsertNoIndex(ClientDataSet1, StrToIntDef(Edit1.Text,1));
end;

// 削除
procedure TForm1.SpeedButton5Click(Sender: TObject);
begin
  cdsDeleteNoIndex(ClientDataSet1, DBGrid1.SelectedRows);
end;

// Undo
procedure TForm1.SpeedButton6Click(Sender: TObject);
begin
  cdsUndoNoIndex(ClientDataSet1);
end;

// Filter
procedure TForm1.SpeedButton7Click(Sender: TObject);
begin
  ClientDataSet2.Filtered := not ClientDataSet2.Filtered;
  if ClientDataSet2.Filtered then
  begin
    ClientDataSet2.CloneCursor(ClientDataSet1, TRUE, False);
    ClientDataSet2.OnFilterRecord := ClientDataSet2FilterRecord;
    ClientDataSet2.Filtered := True;
  end
  else
    ClientDataSet2.Close;
end;

実行時画面

Runtime



フィルター処理の前に試したこと(^-^)
// 次のcdsUpdate0~cdsUpdate3までいずれも役に
// 立ちませんでした。
// いずれもClientDataSet1.Filtered := Trueの前に
// 入れてテストしたものです。

// ファイルの読み書きが正しくできるなら
// 採用するかどうかは別として
// この処理で動作すると考えた。
// 2度目以降使えない(T_T)
procedure cdsUpdate0(CDS: TClientDataSet);
begin
  Form1.SpeedButton1Click(nil);
  Form1.SpeedButton2Click(nil);
end;

procedure cdsUpdate1(CDS: TClientDataSet);
var
  Data: OleVariant;
begin
  // 2度目以降使えない(T_T)
  if CDS.ChangeCount > 0 then
    CDS.MergeChangeLog;
  Data := CDS.Data;
  CDS.Data := Data;
end;

procedure cdsUpdate2(CDS: TClientDataSet);
var
   MS: TMemoryStream;
begin
  // 2度目以降使えない(T_T)
  if CDS.ChangeCount > 0 then
    CDS.MergeChangeLog;
  MS := TMemoryStream.Create;
  try
    CDS.SaveToStream(MS, dfBinary);
    MS.Position := 0;
    CDS.LoadFromStream(MS);
  finally
   MS.Free;
  end;
end;

procedure cdsUpdate3(CDS: TClientDataSet);
var
   SL1, SL2: TStringList;
   I, J: Integer;
   FieldName, Value: String;
begin
  // こんなコテコテの処理をしたところで、
  // フィルターで編集されたデータは、
  // 一番最後に移動してしまいます。
  if CDS.ChangeCount > 0 then
    CDS.MergeChangeLog;
  SL1 := TStringList.Create;
  SL2 := TStringList.Create;
  try
    SL1.StrictDelimiter := True;
    SL1.Delimiter := ',';
    CDS.DisableControls;
    try
      CDS.First;
      while not CDS.EOF do
      begin
        SL1.Clear;
        for I := 0 to CDS.Fields.Count -1 do
          if (CDS.Fields[I].FieldKind = fkData) then
          SL1.Add(CDS.Fields[I].FieldName +'='+  CDS.Fields[I].AsString);

        SL2.Add(SL1.CommaText);
        CDS.Next;
      end;
      
      // 次の処理はEmptyDataSetを使う方が簡単
      CDS.Last;
      while not CDS.BOF do
        CDS.Delete;

      for I := 0 to SL2.Count-1 do
      begin
        SL1.CommaText := SL2[I];
        CDS.Append;
        for J := 0 to SL1.Count-1 do
        begin
          FieldName := SL1.Names[J];
          Value := SL1.Values[FieldName];
          CDS.FieldByName(FieldName).AsString := Value;
        end;
      end;
      CDS.CheckBrowseMode;

    finally
      CDS.EnableControls;
    end;

  finally
    SL2.Free;
    SL1.Free;
  end;
end;


MyBaseを試してみる。(インデックスを使わずに、行の追加、挿入、削除編) REVENGE(^o^)もご覧下さい。

|

« MyBaseを試してみる。(CloneCursorでOnFilterRecord) | トップページ | MyBaseを試してみる。(Index、FilterでAggregateが変!?) »