TClientDataSet:如何在更改数据库结构时保留本地数据并使其可用

时间:2013-11-15 09:08:36

标签: database delphi interbase tclientdataset

Hello代码爱好者!

我有一个问题,这无疑是由于我的Delphi XE2知识缺乏经验。 我试着在这里解释一下。

简介

我有一个包含数据的Interbase数据库。该数据库位于远程计算机上。 我正在开发的客户端应用程序使用此数据库。由于必须在没有可用的网络连接时使用该应用程序,我必须使用公文包模型。这就是为什么我使用ClientDataSet来检索数据并以XML格式本地存储它。在我看来,使用本地数据库代替XML文件会更容易,但我还是不允许改变它。因此我仍然使用XML :(

由于数据的重要性,我希望尽可能保持安全。即使其他开发人员正在更改数据库的内部结构(例如,表中的字段被添加,重命名甚至删除),本地存储的数据仍然必须可用。

我现在所做的是,我使用ClientDataSet从数据库中检索元数据。它分别存储在磁盘上。我计划做的下一件事是将数据库中的元数据与存储在本地数据集中的元数据进行比较。当我在字段中发现差异时,我在代码中创建了一个新的数据集,我建立了字段定义并在之后添加数据。换句话说,我只是创建一个新的本地数据集,它符合远程数据库中表的结构。

当我找到列(字段)删除或添加时,这很容易,但是当字段名称或数据类型发生更改时,它会变得更加困难。

我还没有考虑主键,外键和唯一键,但我也觉得必须这样做。

问题:

我的问题主要是,我想知道这是否是正确的方法。实现这一点是相当有意义的,在我开始实现这一切之前,我想知道是否还有其他(更方便,更简单)的方法来实现我上面描述的内容。

在我看来,本地可用数据的优先级高于远程数据库中存储的数据。仅仅因为用户正在处理本地数据而不是直接处理远程数据。

对此有何想法?我希望我能够充分澄清我的问题,如果没有,请询​​问,我会提供更多细节。我正在使用Interbase XE(SP5)和Delphi XE 2。

1 个答案:

答案 0 :(得分:5)

嗯我花了很长时间,但我现在正在努力。虽然到目前为止我仍然对我的解决方案有点怀疑(我现在正在测试它,但到目前为止仍然没有问题)我现在也很高兴让它现在正常工作。

我必须为这个答案的长度道歉,我认为这对我的帖子的整个可读性没有好处,但我没有看到另外提供关于这个主题的详细信息的可能性。

如果其他人正在处理类似的事情,我决定将我的解决方案作为答案发布。我希望它有所帮助,当然我很想知道我是否可能错过了什么。

我编写了一个函数,尝试在找到差异时更新元数据。 由于本地数据集以XML格式存储(因此本地存储的所有内容都可以被视为字符串),我可以将它们视为变体。事实上,在添加数据方面,这是一个巨大的好处:

procedure TdmDatabase.UpdateMetaDataFor( cds : TCustomClientDataSet; folder : String);

现在嵌套的过程和函数如下。这可能会在以后发生变化,因为我仍然不太确定如何使用这种方法......

    procedure AddInLocalData( local, newCds : TCustomClientDataSet );
    var i : Integer;
    begin
      try
         (* Assume that the new dataset is still closed... *)
         newCds.CreateDataSet;
         newCds.Insert;
         for i := 0 to Pred(local.Fields.Count) do
         begin
           if ( i < newCds.FieldCount ) then
             newCds.Fields[i].AsVariant := local.Fields[i].AsVariant;
         end;

       newCds.Post;

       newCds.SaveToFile( folder + newCds.TableName + '_updated.xml', dfXMLUTF8 );
     except on E: Exception do
       raise Exception.Create( _Translate(RS_ERROR_UNABLE_TO_SYNC_LOCAL_AND_REMOTE));
     end;
   end;

添加字段很容易,尤其是在没有约束的情况下。额外的字段 从远程数据集中找到(实际上来自数据库本身),数据在那里并不重要。因此,我们可以在不打扰必须插入的数据的情况下插入新字段。如果是这种情况(对于我们的项目没有必要),那么这个功能肯定需要更新:

   function AddFieldsLocally( remote, local, newCds : TCustomClientDataSet ) :  TCustomClientDataSet;
   var i        : Integer;
       fieldDef : TFieldDef;
   begin
     try
       (* Local provider is leading... *)
       newCds.SetProvider(local);
       newCds.FieldDefs.Update;
       (* Find the already existing fields and add them *)
       for i := 0 to Pred(newCds.FieldDefs.Count) do
       begin
         with newCds.FieldDefs[i].CreateField(cds) do
         begin
           FieldName := local.Fields[i].FieldName;
           Calculated := local.Fields[i].Calculated;
           Required := local.Fields[i].Required;
           Size := local.Fields[i].Size; //TODO: Add checking here!
         end;
       end;
       (* Check for additional fields that exist remotely and add them *)
       for i  := newCds.fieldDefs.Count  to Pred(remote.FieldDefs.Count) do
       begin
         fieldDef := remote.FieldDefs.Items[i];
         if (fieldDef <> nil)  then
         begin
           newCds.FieldDefs.Add(fieldDef.Name, fieldDef.DataType, fieldDef.Size, fieldDef.Required);
           newCds.FieldDefs[ Pred( newCds.FieldDefs.Count )].CreateField(newCds);
         end;
       end;

     (* Finally, add the existing local data to the newly created dataset *)
     AddInLocalData(local, newCds);
     result := newCds;
     except on E:Exception
       raise E;
     end;
   end;

删除字段更具体一些。首先,如果需要删除的字段有限制,仍需要进行验证。如果是,则方法不应该继续,并且应该从数据库中删除并重建包含所有表的整个本地数据集,以确保正确的功能。目前,这些变化被认为是重大变化。我检查是否应用了主要更改,如果有,则很可能还需要新版本的应用程序。

  function RemoveFieldsLocally( remote, local, newCds : TCustomClientDataSet ) : TCustomClientDataSet;
  var i        : Integer;
      fieldDef : TFieldDef;
      field    : TField;
  begin
    try
      (* Remote provider has lead here! *)
      newCds.SetProvider(remote);
      newCds.FieldDefs.Update;

      (* Find the already existing fields and add them *)
      for i := 0 to Pred(newCds.FieldDefs.Count) do
      begin
        field := newCds.FieldDefs[i].CreateField(cds);

        if assigned(field) then
        begin
          field.FieldName := local.Fields[i].FieldName;
          field.Calculated := local.Fields[i].Calculated;
          field.Required := local.Fields[i].Required;
          (* Necessary for compatibility with for example StringFields, BlobFields, etc *)
          if ( HasProperty( field, 'Size') ) then
          Field.Size := local.FIelds[i].Size;
        end;
      end;

     (* Now add in the existing data from the local dataset.
        Warning: since fields have been removed in the remote dataset, these
        will not be added as well. If constraints were put up, these become
        lost *)
     AddInLocalData(local, newCds);
     result := newCds;
  except on E:Exception do
    raise E;
  end;
end;

下面的函数检查字段之间的相等性。如果在涉及DataType(FieldType),FieldName等时检测到差异,它将尝试根据远程数据集的元数据更新它。

function VerifyInternalStructuresAndFields( remote, local, newCds : TCustomClientDataSet ) : boolean;
var i, equalityCounter : Integer;
    equal : boolean;
begin
  try
    (* We know that both datasets (local and remote) are equal for when it comes to
       the fieldcount. In this case, the structure of the dataset from the remote     dataset is leading. *)
    newCds.SetProvider(remote);
    newCds.FieldDefs.Update;

    equal := false;
    equalityCounter := 0;
    for i := 0 to Pred(newCds.FieldDefs.Count) do
    begin
      (* 1. Fielddefinitions which are exactly equal, can be copied *)
      equal := (remote.Fields[i].FieldName = local.Fields[i].FieldName ) and
               (remote.Fields[i].Required = local.Fields[i].Required ) and
               (remote.Fields[i].Calculated = local.Fields[i].Calculated ) and
               (remote.Fields[i].DataType = local.Fields[i].DataType) and
             (remote.Fields[i].Size = local.Fields[i].Size );

      if ( equal ) then
      begin
        inc(equalityCounter);
        with newCds.FieldDefs[i].CreateField(cds) do
        begin
          FieldName := local.Fields[i].FieldName;
          Calculated := local.Fields[i].Calculated;
          Required := local.Fields[i].Required;
          Size := local.FIelds[i].Size;
        end;
      end
      else (* fields differ, try to update it, here the remote fields are leading! *)
      begin
        if ( MessageDlg( _Translate( RS_WARNING_DIFFERENCES_IN_FIELDS), mtWarning, mbYesNo, 0) = IDYES ) then
        begin
          with newCds.FieldDefs[i].CreateField(cds) do
          begin
            FieldName := remote.Fields[i].FieldName;
            Calculated := remote.Fields[i].Calculated;
            Required := remote.Fields[i].Required;
            if ( HasProperty( remote, 'Size') ) then
              Size := remote.Fields[i].Size;
            SetFieldType( remote.Fields[i].DataType );  //TODO: If this turns out to be unnecessary, remove it.
          end;
        end
        else
        begin
          result := false;
          exit;
        end;
      end;
    end;

    if ( equalityCounter = local.FieldCount ) then
    begin
      result := false;
    end else 
    begin
      AddInLocalData(local, newCds);
      result := true;
    end;
  except on E:Exception do
    raise E;
  end;
end;

这是尝试检测字段和字段之间差异的主要功能 远程和本地数据集的定义。

function  FindDifferencesInFields( remote, local: TCustomClientDataSet ) : TCustomClientDataSet;
var i, k     : Integer;
    fieldDef : TFieldDef;
    newCds   : TKLAClientDataSet;
begin
  try
    newCds := TCustomClientDataSet.Create(nil);
    newCds.FileName := local.FileName;
    newCds.Name := local.Name;
    newCds.TableName := local.TableName;

    (* First check if the remote dataset has added fields. *)
    if ( remote.FieldDefs.Count > local.FieldDefs.Count ) then
    begin
      result := AddFieldsLocally(remote, local, newCds);
    end
    (* If no added fields could be found, check for removed fields *)
    else if (remote.FieldDefs.Count < local.FieldDefs.Count ) then
    begin
      result := RemoveFieldsLocally(remote, local, newCds);
    end
    (* Finally, check if the fieldcounts are equal and if renames have taken place *)
    else if (remote.FieldDefs.Count = local.FieldDefs.Count ) then
    begin
      if ( VerifyInternalStructuresAndFields(remote, local, newCds) ) then
        result := newCds
      else result := local;
    end;
  except on E:Exception do
    raise Exception.Create('Could not verify remote and local dataset: ' + E.Message);
  end;
end;

由于上面使用的所有函数和过程都非常关键,我决定将它们嵌套在名为UpdateMetaDataFor的主过程中。我可能会稍后改变它,但现在它已经足够了。

var fieldDefs : TFieldDefs;
remotecds     : TCustomClientDataSet;
constraints   : TCheckConstraints;
fileName      : String;
k             : integer;
begin
  try
    try
      ConnectDB(false);
      fileName := folder + cds.TableName + '_metadata_update.xml';

      (* Retrieve the latest metadata if applicable *)
      remotecds := CreateDataset;
      remotecds.SQLConnection := SQLConnection;
      remotecds.TableName := cds.TableName;
      remotecds.SQL.Text := Format('SELECT * from %s where id=-1', [cds.TableName]);
      remotecds.Open;

      remotecds.SaveToFile( fileName , dfXMLUTF8 );

      (* Load the local dataset with data for comparison *)
      cds.LoadFromFile( folder + cds.FileName );

      SyncProgress( _Translate(RS_SYNC_INTEGRITY_CHECK) + ' ' + cds.TableName);

      cds := FindDifferencesInFields( remotecds, cds );
      cds.SaveToFile( folder + cds.FileName, dfXMLUTF8 );
    except on E: Exception do
      ShowMessage( E.Message );
    end;
 finally
   if assigned(remotecds) then
     remotecds.Free;
   if FileExists( fileName ) then
     SysUtils.DeleteFile( fileName );
   end;
 end;

这总结了我非常全面的答案,这仍然留有一些可供考虑的事情。例如,必须使用约束(这些在局部数据集上不直接可见)。

另一种方法可能是向数据库本身添加一个表,该表将包含所有更改,具体取决于版本号。如果这些更改被认为是次要的(对于没有约束或任何限制的字段的名称更改),则仍然可以使用此半自动方法。

与往常一样,我仍然非常好奇在应用数据库公文包模型时确保数据库完整性的其他方法。