7

你好代码爱好者!

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

介绍:

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

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

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

当我发现列(字段)删除或添加时,这很容易,但是当字段的名称或数据类型发生变化时,这会变得有点困难。

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

问题:

我的问题主要是,我想知道这是否是正确的方法。实现这一点需要做很多工作,在我开始实现这一切之前,我想知道是否有其他(更方便和更容易)的方法来实现我上面描述的事情。

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

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

4

1 回答 1

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;

我的非常全面的回答到此结束,仍然有一些事情可供考虑。例如,必须对约束做什么(这些在本地数据集上不直接可见)。

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

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

于 2013-11-20T11:02:27.697 回答