TQConverter 在 QDB 中占据了重要的位置,它是数据与数据流之间的格式转换器,它的用途在于:
- 利用 TQDataSet 的 LoadFromStream/LoadFromFile 函数将数据从文件或流中加载到 TQDataSet 数据集对象
- 利用 TQDataSet 的 SaveToStream/SaveToFile 函数将数据从 TQDataSet 数据集输出到文件或流中
- 利用 TQProvider 的 ApplyChanges 将变更内容从文件或流中提交到 TQProvider 对应的数据库中
- 利用 TQProvider 的 OpenDataStream 函数将从数据库取出的数据直接保存到文件或流中
考虑到数据保存到文件或流时,需要加密和压缩数据的处理,因此 TQConverter 对此提供了支持,可以为其关联多个 TQStreamProcessor (流处理器)子类型的实例,每一步都对目标数据流进行必要的处理。
TQConverter、TQProvider、TQStreamProcessor、TQDataSet 四者之间的关系大致可用下图表示:
下面我们将结合 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 函数来了解,为了节省篇幅,我们在此就不直接转载源码。
- 准备导出阶段:
- 设置 TQConverter 转换器实例的 FStream 和 FDataSet 属性为目标数据流和当前源数据集;
- 根据 TQConverter.ExportRanges 属性,确定要导出的什么状态的记录( AcceptStatus 用来保存这个状态的集合);
- 调用 TQConverter 转换器的 BeforeExport 函数,以便让 TQConverter 在真正执行导出操作之前做一些需要的处理(TQConverter 本身的实现里,如果你指定了流处理器,它会创建一个临时内存流来存放导出结果,而不是直接写到目标流里,以方便后续处理。子类可以重载以增加额外的控制);
- 导出数据阶段 : 此阶段需要区分下当前数据集是单一数据集还是多个数据集,如果是单一数据集,那么直接导出当前数据集;反之,则循环导出每个子数据集(前提是 TQConverter 的子类支持导出多个数据集)。在导出时,TQConverter 实例可以通过 DataSetCount 属性,了解需要导出多少个数据集,通过 ActiveDataSet 属性来知道当前导出的是第几个数据集(索引从 0 开始)。导出每个数据集的过程如下:
- 首先调用 TQConverter 的 BeginExport 来通知转换器要导出的数据集索引;
- 如果导出范围包含 merMeta ,则调用 TQConverter 实例的 SaveFieldDefs 来导出当前数据表的字段定义信息;
- 针对每条符合条件的记录,调用 TQConverter 的 WriteRecord 方法来完成单条记录数据的导出操作直到全部完成;
- 调用 TQConverter 的 EndExport 来通知转换器指定索引的数据集导出结束;
- 结束导出阶段
- 调用 TQConverter.AfterExport 函数来完成导出过程的清理工作(在 TQConverter 本身的实现里,如果你指定了流处理器,则会顺序调用流处理器,对导出的内容进行处理,并将处理结果写回到目标数据流);
反过来,当我们从流或文件中加载数据到数据集里时,我们结合LoadFromStream方法,来看它都执行了那些步骤:
- 准备导入阶段
- 关闭当前数据集;
- 将打开方式设置为 dsomByConverter,以通知后面打开数据集操作调用 InternalOpen 函数时,按从转换器导入数据的方式来初始化相关操作;
- 设置 TQConverter 转换器实例的 FStream 和 FDataSet 属性为源数据流和当前数据集(目标);
- 触发 TQConverter.BeforeImport 函数,以便让 TQConverter 实例来做一些导入前的准备工作(在 TQConverter 自身的实现里,如果指定了流处理器,则逆序调用这些流处理器的 BeforeLoad 方法,进行数据处理。这和 AfterExport 中的实现顺序正好相反);
- 导入数据阶段:此阶段需要区分下当前流中包含的是单一数据集还是多个数据集,如果是单一数据集,那么直接导入到当前数据集;反之,则循环导入到每个子数据集。在前面调用 TQConverter.BeforeImport 函数时,子类应重载设置好 DataSetCount 属性来告诉导入过程总共有多少个数据集。而在导入时,导入过程通过设置 ActiveDataSet 属性来告诉 TQConverter 实例当前导入的是第几个数据集(索引从 0 开始)。导入每个数据集的过程如下:
- 首先调用 TQConverter 的 BeginImport 方法来告诉转换器要导入的数据集索引号;
- 调用 TQConverter 实例的 LoadFieldDefs 来导入当前数据表的字段定义信息;
- 调用 TQConverter 的 ReadRecord 方法来完成单条记录数据的读取操作直到全部读取完成,函数返回 False;
- 调用 TDataSet.Open 方法数据集对象。如果有多个数据集,则通过设置当前活动数据集为第一个数据集来打开数据集;
- 调用 TQConverter 的 EndImport 方法来告诉转换器导入指定索引的数据集完成;
- 结束导入阶段
- 调用 TQConverter.AfterImport 函数,以便让 TQConverter 实例来做一些清理工作;
上面就是 TQConverter 导出和导入到数据集的过程,而 TQProvider 调用 ApplyChanges 应用更新的过程与些类似,只是中间状态的检测略有不同,但那些都是由 TQProvider 来完成的。具体到 TQConverter 上来说,它并没有任何不同。
那么接下来,我们就看看 TQJsonConverter (位于 qconverter_stds.pas ) 都重载干了点啥?
在继续之前,我们需要了解下 TQJsonConverter 约定的 JSON 数据包格式,下图是直接截取自 MsgPackView :
- 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:数组,用于保存记录的新值,对于未修改或已删除状态的记录,它不存在;数组的每一个成员对应于字段定义相对应位置的字段的值;
- Fields :对象类型,它包含了了每一个字段的定义信息,每个字段是一个对象,字段名做为对象的名称。下面的列出了其中的各个项目名称,其中,除了 FieldType 和 Flags 是必然存在的,其它都有可能不存在,此时它们取默认值。
好了,定义完事,我们看实现:
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。
- BeforeExport
- 导入函数
- BeforeImport
- 调用父对象的BeforeImport;
- 创建 FJson 对象的实例;
- 检查 Type 结点的值,以确定是否是自己创建的格式;
- 找到 DataSets 根结点,保存到 FDSRoot 变量中;
- 更新 DataSetCount 的值,以便通知导入过程实际的数据集数量;
- LoadFieldDefs :
- 将活动的数据集根结点赋给 FActiveDS;
- 然后字段加载定义;
- 找到 Records 结点,并赋给 FActiveRecs;
- 设置当前记录索引 FRecordIndex 为 -1;
- ReadRecord :根据 FRecordIndex 读取下一条记录的内容,成功,返回 True,失败,返回 False;
- AfterImport:调用父对象的 AfterExport 和释放 FJson 对象;
- BeforeImport
好了,关于 TQConverter 的说明,暂时就到这儿,有不明白的朋友可以在群里直接问。详细的代码示例,可以参考 qconverter_xxx.pas 的源码,以 qconverter 打头的源码为各种转换器。