TQConverter – 数据集与数据流之间的转接桥

TQConverter 在 QDB 中占据了重要的位置,它是数据与数据流之间的格式转换器,它的用途在于:

  • 利用 TQDataSet 的 LoadFromStream/LoadFromFile 函数将数据从文件或流中加载到 TQDataSet 数据集对象
  • 利用 TQDataSet 的 SaveToStream/SaveToFile 函数将数据从 TQDataSet 数据集输出到文件或流中
  • 利用 TQProvider 的 ApplyChanges 将变更内容从文件或流中提交到 TQProvider 对应的数据库中
  • 利用 TQProvider 的 OpenDataStream 函数将从数据库取出的数据直接保存到文件或流中

考虑到数据保存到文件或流时,需要加密和压缩数据的处理,因此 TQConverter 对此提供了支持,可以为其关联多个 TQStreamProcessor (流处理器)子类型的实例,每一步都对目标数据流进行必要的处理。

TQConverter、TQProvider、TQStreamProcessor、TQDataSet 四者之间的关系大致可用下图表示:

QDB下面我们将结合 TQJsonConverter 来说明一下如何编写自己的格式转换器。首先,我们看一下 TQConverter 的接口:

TQConverter = class(TComponent)
  protected
    FDataSet: TQDataSet;
    FExportRanges: TQExportRanges;
    FStream: TStream;
    FOriginStream: TStream;
    FDataSetCount: Integer;
    FActiveDataSet: Integer;
    FOnProgress: TQDataConveterProgress;
    FStreamProcessors: TQStreamProcessors;
    procedure SetDataSet(const Value: TQDataSet);
    // 导出接口
    procedure BeforeExport; virtual;
    procedure BeginExport(AIndex:Integer);virtual;
    procedure SaveFieldDefs(ADefs: TQFieldDefs); virtual; abstract;
    function WriteRecord(ARec: TQRecord): Boolean; virtual; abstract; // 写出数据
    function EndExport(AIndex:Integer);virtual;
    procedure AfterExport; virtual;
    // 导入接口
    procedure BeforeImport; virtual;
    procedure BeginImport(AIndex:Integer);virtual;
    procedure LoadFieldDefs(AFieldDefs: TQFieldDefs); virtual; abstract;
    function ReadRecord(ARec: TQRecord): Boolean; virtual; abstract; // 导入数据内容
    procedure EndImport(AIndex:Integer);virtual;
    procedure AfterImport; virtual;
    // 进度
    procedure DoProgress(AStep: TQConvertStep; AProgress, ATotal: Integer);
    property ActiveDataSet:Integer read FActiveDataSet write FActiveDataSet;
    property DataSetCount: Integer read FDataSetCount write FDataSetCount;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    property ExportRanges: TQExportRanges read FExportRanges
      write FExportRanges;
    property OnProgress: TQDataConveterProgress read FOnProgress
      write FOnProgress;
    property DataSet: TQDataSet read FDataSet write SetDataSet;
    property StreamProcessors: TQStreamProcessors read FStreamProcessors;
  end;

然后我们先介绍一下保存数据到流时,到底发生什么,这块大家可以结合 TQDataSet.SaveToStream 函数来了解,为了节省篇幅,我们在此就不直接转载源码。

  1. 准备导出阶段:
    • 设置 TQConverter 转换器实例的 FStream 和 FDataSet 属性为目标数据流和当前源数据集;
    • 根据 TQConverter.ExportRanges 属性,确定要导出的什么状态的记录( AcceptStatus 用来保存这个状态的集合);
    • 调用 TQConverter 转换器的 BeforeExport 函数,以便让 TQConverter 在真正执行导出操作之前做一些需要的处理(TQConverter 本身的实现里,如果你指定了流处理器,它会创建一个临时内存流来存放导出结果,而不是直接写到目标流里,以方便后续处理。子类可以重载以增加额外的控制);
  2. 导出数据阶段 : 此阶段需要区分下当前数据集是单一数据集还是多个数据集,如果是单一数据集,那么直接导出当前数据集;反之,则循环导出每个子数据集(前提是 TQConverter 的子类支持导出多个数据集)。在导出时,TQConverter 实例可以通过 DataSetCount 属性,了解需要导出多少个数据集,通过 ActiveDataSet 属性来知道当前导出的是第几个数据集(索引从 0 开始)。导出每个数据集的过程如下:
    • 首先调用 TQConverter 的 BeginExport 来通知转换器要导出的数据集索引;
    • 如果导出范围包含 merMeta ,则调用 TQConverter 实例的 SaveFieldDefs 来导出当前数据表的字段定义信息;
    • 针对每条符合条件的记录,调用 TQConverter 的 WriteRecord 方法来完成单条记录数据的导出操作直到全部完成;
    • 调用 TQConverter 的 EndExport 来通知转换器指定索引的数据集导出结束;
  3. 结束导出阶段
    1. 调用 TQConverter.AfterExport 函数来完成导出过程的清理工作(在 TQConverter 本身的实现里,如果你指定了流处理器,则会顺序调用流处理器,对导出的内容进行处理,并将处理结果写回到目标数据流);

反过来,当我们从流或文件中加载数据到数据集里时,我们结合LoadFromStream方法,来看它都执行了那些步骤:

  1. 准备导入阶段
    • 关闭当前数据集;
    • 将打开方式设置为 dsomByConverter,以通知后面打开数据集操作调用 InternalOpen 函数时,按从转换器导入数据的方式来初始化相关操作;
    • 设置 TQConverter 转换器实例的 FStream 和 FDataSet 属性为源数据流和当前数据集(目标);
    • 触发 TQConverter.BeforeImport 函数,以便让 TQConverter 实例来做一些导入前的准备工作(在 TQConverter 自身的实现里,如果指定了流处理器,则逆序调用这些流处理器的 BeforeLoad 方法,进行数据处理。这和 AfterExport 中的实现顺序正好相反);
  2. 导入数据阶段:此阶段需要区分下当前流中包含的是单一数据集还是多个数据集,如果是单一数据集,那么直接导入到当前数据集;反之,则循环导入到每个子数据集。在前面调用 TQConverter.BeforeImport 函数时,子类应重载设置好 DataSetCount 属性来告诉导入过程总共有多少个数据集。而在导入时,导入过程通过设置 ActiveDataSet 属性来告诉 TQConverter 实例当前导入的是第几个数据集(索引从 0 开始)。导入每个数据集的过程如下:
    • 首先调用 TQConverter 的 BeginImport 方法来告诉转换器要导入的数据集索引号;
    • 调用 TQConverter 实例的 LoadFieldDefs 来导入当前数据表的字段定义信息;
    • 调用 TQConverter 的 ReadRecord 方法来完成单条记录数据的读取操作直到全部读取完成,函数返回 False;
    • 调用 TDataSet.Open 方法数据集对象。如果有多个数据集,则通过设置当前活动数据集为第一个数据集来打开数据集;
    • 调用 TQConverter 的 EndImport 方法来告诉转换器导入指定索引的数据集完成;
  3. 结束导入阶段
    • 调用 TQConverter.AfterImport 函数,以便让 TQConverter 实例来做一些清理工作;

上面就是 TQConverter 导出和导入到数据集的过程,而 TQProvider 调用 ApplyChanges 应用更新的过程与些类似,只是中间状态的检测略有不同,但那些都是由 TQProvider 来完成的。具体到 TQConverter 上来说,它并没有任何不同。

那么接下来,我们就看看 TQJsonConverter (位于 qconverter_stds.pas ) 都重载干了点啥?

在继续之前,我们需要了解下 TQJsonConverter 约定的 JSON 数据包格式,下图是直接截取自 MsgPackView :

qds_jsonformat

 

  • Type :字符串类型,固定为 QDAC.DataSet,以便标记这由于 TQJsonConverter 生成的数据,用于加载时进行格式检测;
  • DataSets : 数组类型,用于保存一到多个数据集的内容,每个数据集对象为一项。对于每一个数据集对象,其它义如下:
    • Fields :对象类型,它包含了了每一个字段的定义信息,每个字段是一个对象,字段名做为对象的名称。下面的列出了其中的各个项目名称,其中,除了 FieldType 和 Flags 是必然存在的,其它都有可能不存在,此时它们取默认值。
      • FieldType :整数,用于保存字段类型对应的整数值;
      • Flags : 整数,内部标志,参考源码中数据列属性定义中的 SCHEMA_XXX 标志位定义;
      • Size :整数,用于保存内容长度,对于内部保留类型,该字段可能不存在或为0;
      • Prec :整数,用于保存数值精度;
      • Scale :整数,用于保存小数点后位置;
      • Schema : 字符串,用于保存字段所隶属的架构(如:database.public.table 中的 public),一般数据库对应的是用户名;
      • Catalog : 字符串,用于保存字段所隶属的数据库名;
      • Table : 字符串,用于保存字段隶属的物理表名
      • Base : 字符串,用于保存数据库中原始的字段名(如:select a as b from tab 中的 a 即为原始字段名);
      • DBType : 整数值,用于存贮数据库中的物理字段类型,对于内存表,它始终为 0 或不存在;
      • DBNo : 整数值,记录数据库中字段的原始顺序号;
    • Records : 数组类型,用于保存每一条记录的内容。每一条记录是一个对象,它的定义如下:
      • Status : 整数值,记录数据的状态(参考 TUpdateStatus 定义,取枚举值的整数值);
      • Old : 数组,用于保存记录的原始值,对于新增的记录,它不存在。数组的每一个成员对应于字段定义相对应位置的字段的值;
      • New:数组,用于保存记录的新值,对于未修改或已删除状态的记录,它不存在;数组的每一个成员对应于字段定义相对应位置的字段的值;

好了,定义完事,我们看实现:

  TQJsonConverter = class(TQConverter)
  protected
    FJson, FDSRoot, FActiveDS, FActiveRecs: TQJson;
    FRecordIndex: Integer;
    FLoadingDefs: TQFieldDefs;
    procedure BeforeExport; override;
    procedure SaveFieldDefs(ADefs: TQFieldDefs); override;
    function WriteRecord(ARec: TQRecord): Boolean; override;
    procedure AfterExport; override;
    procedure BeforeImport; override;
    procedure LoadFieldDefs(AFieldDefs: TQFieldDefs); override;
    function ReadRecord(ARec: TQRecord): Boolean; override;
    procedure AfterImport; override;
  end;

可以看到,它重载了 TQConverter 中与导入导出相关的几个函数:

  • 导出函数:BeforeExport 、SaveFieldDefs 、 WriteRecord 、 AfterExport
  • 导入函数:BeforeImport、 LoadFieldDefs 、 ReadRecord 、 AfterImport

同时,它定义了几个变量:

  • FJson 为导入导出时的 QJson 的根结点;
  • FDSRoot 为所有导入或导出数据集的根结点;
  • FActiveDS 为当前正在导入或导出的数据集根结点;
  • FActiveRecs 为当前导入或导出数据集的记录根结点;

剩下的步骤就是按照格式在上面重载的函数中实现必要的操作了:

  • 导出函数
    • BeforeExport
      • 调用父对象的 BeforeExport;
      • 创建 FJson 对象的实例;
      • 设置 Type 节点的值;
      • 添加 DataSets 根结点,并保存到 FDSRoot 变量中;
    • SaveFieldDefs :
      • 添加了 FActiveDS 结点来记录新的数据集;
      • 保存字段信息定义到 Fields 结点;
      • 如果不是只保存元数据,则创建 Records 结点;
    • WriteRecord
      •  写入指定记录的内容到 Json 中;
    • AfterExport
      • 将 FJson 的内容保存到目标数据流中;
      • 释放 FJson 对象;
      • 调用父对象的 AfterExport。
  • 导入函数
    • BeforeImport
      • 调用父对象的BeforeImport;
      • 创建 FJson 对象的实例;
      • 检查 Type 结点的值,以确定是否是自己创建的格式;
      • 找到 DataSets 根结点,保存到 FDSRoot 变量中;
      • 更新 DataSetCount 的值,以便通知导入过程实际的数据集数量;
    • LoadFieldDefs :
      • 将活动的数据集根结点赋给 FActiveDS;
      • 然后字段加载定义;
      • 找到 Records 结点,并赋给 FActiveRecs;
      • 设置当前记录索引 FRecordIndex 为 -1;
    • ReadRecord :根据 FRecordIndex 读取下一条记录的内容,成功,返回 True,失败,返回 False;
    • AfterImport:调用父对象的 AfterExport 和释放 FJson 对象;

好了,关于 TQConverter 的说明,暂时就到这儿,有不明白的朋友可以在群里直接问。详细的代码示例,可以参考 qconverter_xxx.pas 的源码,以 qconverter 打头的源码为各种转换器。

分享到: