我需要将一些数据存储在一个文件中。每个记录(数据集)包括:
将所有这些内容保存在二进制文件中并不困难。但是,我确信(不幸的是)我的数据格式会随时间变化,我希望有可能为每个“记录”添加更多字段。所以,显然我的文件格式无法修复。我想最好的解决方案是将数据保存在(DB)表中,但我不想搞砸大枪(SQL,ADO,BDE,Nexus ......)。 我需要一个可以做到这一点的基础库(如果可能的话,单个PAS文件)。 由于这样做的目的是存储数据而不是使用数据,所以可以在没有数据库表的情况下完成吗?
此库的要求:
我使用D7
答案 0 :(得分:9)
通过最近的升级,它可以完全满足您的需求。
以下是创建字段布局的方法:
var Table: TSynBigTableRecord;
FieldText, FieldInt: TSynTableFieldProperties;
begin
Table := TSynBigTableRecord.Create('FileName.ext','TableName');
FieldText := Table.AddField('text',tftWinAnsi,[tfoIndex]);
FieldInt := Table.AddField('Int',tftInt32,[tfoIndex,tfoUnique]);
Table.AddFieldUpdate;
要存储字节或整数数组,只需使用tftWinAnsi
甚至更好的tftBlobInternal
种字段(这是一个真正的可变长度字段),然后将其映射到动态或从动态映射数组,就像RawByteString
。
您可以安全地稍后添加字段,将为您处理数据文件。
有几种处理数据的方法,但我已经实现了一种基于变体的使用记录的方式,具有真正的后期绑定:
var vari: Variant;
// initialize the variant
vari := Table.VariantVoid;
// create record content, and add it to the database
vari.text := 'Some text';
vari.int := 12345;
aID := Table.VariantAdd(vari);
if aID=0 then
ShowMessage('Error adding record');
// how to retrieve it
vari := Table.VariantGet(aID);
assert(vari.ID=aID);
assert(vari.INT=12345);
assert(vari.Text='Some text');
关于速度,you can't find anything faster IMHO。 使用一些文本和整数值创建1,000,000条记录,使用索引的两个字段和设置为唯一的整数字段在我的笔记本电脑上小于880毫秒。它将使用非常少的磁盘空间,因为所有存储都是可变长度编码的(类似于Google的协议缓冲区)。
它只需要两个单元,适用于Delphi 6到XE(并且已准备好Unicode,因此使用此单元可以在需要时安全地升级到更新的Delphi版本)。没有安装需求,只有几KB添加到您的可执行文件中。它只是一个用纯Delphi编写的小而强大的NoSQL引擎,但具有使用数据库的能力(即纯字段布局)和内存引擎的速度,没有大小限制。
它是完整的OpenSource,具有许可许可。
请注意,我们还提供了SQLite3 wrapper,但它是另一个项目。 SQL支持和集成的客户端/服务器ORM,速度更慢但功能更强大。
答案 1 :(得分:5)
使用Synopse BigTable,http://synopse.info/一个key =>值数据库,在这种情况下,值是数据的序列化(json,binary,xml,...)。
这是快速,轻巧和免费的疯狂。
答案 2 :(得分:3)
我认为你不需要这个数据库。如果使用数据库,我看不出它如何解决数据结构变化的问题。
我个人会存储YAML格式,这种格式非常容易扩展。这需要相当多的工作链接到一些LIBYAML所以一个非常轻量级的替代方案是存储到INI文件。它们易于扩展,同时保持与旧文件的兼容性。
您可以轻松地滚动自己的可扩展二进制格式。你所做的是将每条记录写入块。每个块都有一个包含其长度的短标题。
当您读取到块结束时读取的数据,然后如果您需要更多数据,则只需停止读取并使用数据的默认值。如果您已经读取了所有关于但不在块末尾的数据,则该文件必须来自程序的更高版本,并且您只需跳到块的末尾即可。也许您警告该文件包含您不知道的数据。
通过始终以与先前版本相同的顺序写出数据来实现可扩展性。任何新数据都会在每个块的末尾。
答案 3 :(得分:1)
按照您的努力程度,我建议按此顺序:
CSV或INI文件(TMemIniFile或TJvCsvDataSet)。这对你来说是最不起作用的。您可以在单个文件中支持数百万行,但消耗的内存将是巨大的。我设想了一个“数据写入器”组件来替换我的TJvCsvDataSet只能附加记录,并且不会将它们加载到内存中。这将允许您写出CSV文件,甚至逐行读取它们,但不能一次性加载它们。这种方法可能适合您。一个简单的CSV读取器/写入器类,它不是数据集对象。
每行一个XML-tag-line文件。这比INI文件更灵活,可以是分层的。 INI文件是非分层的。如果你只是打开一个文件流,并且附加一行文本,就是这样的形式,以cr + lf结尾,则不需要SAX或DOM:
< logitem attrib1 =“value1”attrib2 =“value2”/>
列出项目
答案 4 :(得分:1)
每当您需要将可变长度数据存储为二进制格式时,您应该将数据的长度存储在实际数据的前面。
由于您以后还需要添加新字段,因此您应该存储每条记录的字段数(或者至少存储结尾的记录结束标记),这样您就可以在文件中移动时保持正确的位置。阅读和寻求操作。
对于实际的记录数据,我建议为每个字段建立一个类型长度数据格式,以便您可以在不知道其数据类型将提前的情况下添加新字段,并允许代码识别和读取/无论内容如何,都可以根据需要跳过单个字段(例如,如果旧应用程序尝试使用较新的字段读取文件,则可以跳过它无法识别的内容。)
最后,您将最终得到类似的内容作为开始,然后您可以根据需要进行扩展,优化等:
const
cTypeUnknown = $00;
cTypeString = $01;
cTypeInteger = $02;
cTypeByte = $03;
cTypeArray = $80;
cTypeStringArray = cTypeStringArray or cTypeArray;
cTypeIntegerArray = cTypeIntegerArray or cTypeArray;
cTypeByteArray = cTypeByteArray or cTypeArray;
type
Streamable = class
public
procedure Read(Stream: TStream); virtual; abstract;
procedure Write(Stream: TStream); virtual; abstract;
end;
Field = class(Streamable)
public
function GetType: Byte; virtual; abstract;
end;
FieldClass = class of Field;
StringField = class(Field)
public
Data: String;
function GetType: Byte; override;
procedure Read(Stream: TStream); override;
procedure Write(Stream: TStream); override;
end;
StringArrayField = class(Field)
public
Data: array of String;
function GetType: Byte; override;
procedure Read(Stream: TStream); override;
procedure Write(Stream: TStream); override;
end;
IntegerField = class(Field)
public
Data: Integer;
function GetType: Byte; override;
procedure Read(Stream: TStream); override;
procedure Write(Stream: TStream); override;
end;
IntegerArrayField = class(Field)
public
Data: array of Integer;
function GetType: Byte; override;
procedure Read(Stream: TStream); override;
procedure Write(Stream: TStream); override;
end;
ByteField = class(Field)
public
Data: Byte;
function GetType: Byte; override;
procedure Read(Stream: TStream); override;
procedure Write(Stream: TStream); override;
end;
ByteArrayField = class(Field)
public
Data: array of Byte;
function GetType: Byte; override;
procedure Read(Stream: TStream); override;
procedure Write(Stream: TStream); override;
end;
AnyField = class(ByteArrayField)
public
Type: Byte;
function GetType: Byte; override;
end;
Record = class(Streamable)
public
Fields: array of Field;
procedure Read(Stream: TStream); override;
procedure Write(Stream: TStream); override;
end;
RecordArray = class(Streamable)
public
Records: array of Record;
procedure Read(Stream: TStream); override;
procedure Write(Stream: TStream); override;
end;
procedure WriteByte(Stream: TStream; Value: Byte);
begin
Stream.WriteBuffer(@Value, SizeOf(Byte));
end;
function ReadByte(Stream: TStream): Byte;
begin
Stream.ReadBuffer(@Result, SizeOf(Byte));
end;
procedure WriteInteger(Stream: TStream; Value: Integer);
begin
Stream.WriteBuffer(@Value, SizeOf(Integer));
end;
function ReadInteger(Stream: TStream): Integer;
begin
Stream.ReadBuffer(@Result, SizeOf(Integer));
end;
procedure WriteString(Stream: TStream; Value: String);
var
S: UTF8String;
begin
S := UTF8Encode(Value);
WriteInteger(Stream, Length(S));
if Length(S) > 0 then
Stream.WriteBuffer(S[1], Length(S));
end;
function ReadString(Stream: TStream): String;
var
S: UTF8String;
begin
SetLength(S, ReadInteger(Stream));
if Length(S) > 0 then
Stream.ReadBuffer(S[1], Length(S));
Result := UTF8Decode(S);
end;
function StringField.GetType: Byte;
begin
Result := cTypeString;
end;
procedure StringField.Read(Stream: TStream);
begin
Data := ReadString(Stream);
end;
procedure StringField.Write(Stream: TStream);
begin
WriteString(Data);
end;
function StringArrayField.GetType: Byte;
begin
Result := cTypeStringArray;
end;
procedure StringArrayField.Read(Stream: TStream);
var
I: Integer;
begin
SetLength(Data, ReadInteger(Stream));
for I := 0 to High(Data) do
Data[I] := ReadString(Stream);
end;
procedure StringArrayField.Write(Stream: TStream);
var
I: Integer;
begin
WriteInteger(Stream, Length(Data));
for I := 0 to High(Data) do
WriteString(Stream, Data[I]);
end;
procedure IntegerField.GetType: Byte;
begin
Result := cTypeInteger;
end;
procedure IntegerField.Read(Stream: TStream);
begin
Assert(ReadInteger(Stream) == SizeOf(Integer));
Data := ReadInteger(Stream);
end;
procedure IntegerField.Write(Stream: TStream);
begin
WriteInteger(Stream, SizeOf(Integer));
WriteInteger(Stream, Data);
end;
function IntegerArrayField.GetType;
begin
Result := cTypeIntegerArray;
end;
procedure IntegerArrayField.Read(Stream: TStream);
var
Num: Integer;
begin
I := ReadInteger(Stream);
Assert((I mod SizeOf(Integer)) == 0);
SetLength(Data, I);
if Length(Data) > 0 then
Stream.ReadBuffer(Data[0], I * SizeOf(Integer));
end;
procedure IntegerArrayField.Write(Stream: TStream);
begin
WriteInteger(Stream, Length(Data));
if Length(Data) > 0 then
Stream.WriteBuffer(Data[0], Length(Data) * SizeOf(Integer));
end;
procedure ByteField.GetType: Byte;
begin
Result := cTypeByte;
end;
procedure ByteField.Read(Stream: TStream);
begin
Assert(ReadInteger(Stream) == SizeOf(Byte));
Data := ReadByte(Stream);
end;
procedure ByteField.Write(Stream: TStream);
begin
WriteInteger(Stream, SizeOf(Byte));
WriteByte(Stream, Byte);
end;
function ByteArrayField.GetType: Byte;
begin
Result := cTypeByteArray;
end;
procedure ByteArrayField.Read(Stream: TStream);
begin
SetLength(Data, ReadInteger(Stream));
if Length(Data) > 0 then
Stream.ReadBuffer(Data[0], Length(Data));
end;
procedure ByteArrayField.Write(Stream: TStream); override;
begin
WriteInteger(Stream, Length(Data));
if Length(Data) > 0 then
Stream.WriteBuffer(Data[0], Length(Data));
end;
function AnyField.GetType: Byte;
begin
Result := Type;
end;
procedure Record.Read(Stream: TStream);
const
PlainTypes = array[1..3] of FieldClass = (StringField, IntegerField, ByteField);
ArrayTypes = array[1..3] of FieldClass = (StringArrayField, IntegerArrayField, ByteArrayField);
var
I: Integer;
RecType, PlainType: Byte;
begin
SetLength(Fields, ReadInteger(Stream));
for I := 0 to High(Fields) do
begin
RecType := ReadByte(Stream);
PlainType := RecType and (not cTypeArray);
if (PlainType >= cTypeString) and (PlainType <= cTypeByte) then
begin
if (RecType and cTypeArray) <> cTypeArray then
Fields[I] := PlainTypes[PlainType].Create
else
Fields[I] := ArrayTypes[PlainType].Create;
end else
Fields[I] := AnyField.Create;
Fields[I].Read(Stream);
end;
end;
procedure Record.Write(Stream: TStream)
var
I: Integer;
begin
WriteInteger(Stream, Length(Fields));
for I := 0 to High(Fields) do
begin
WriteByte(Stream, Fields[I].GetType);
Fields[I].Write(Stream);
end;
end;
procedure RecordArray.Read(Stream: TStream);
var
I: Integer;
begin
SetLength(Records, ReadInteger(Stream));
for I := High(Records) do
begin
Records[I] := Record.Create;
Records[I].Read(Stream);
end;
end;
procedure RecordArray.Write(Stream: TStream);
begin
WriteInteger(Stream, Length(Records));
for I := High(Records) do
Records[I].Write(Stream);
end;
答案 5 :(得分:0)
我可以想到每个“记录”中有一个ini或XML文件,INI文件存储在像SolFS这样的虚拟文件系统中。但是,我不知道你的意思是“轻松支持1百万行”,即必须支持哪些操作。如果您计划主要随机访问一些较少数量的记录,那么解析文本文件并不是什么大问题。在其他情况下,您可能希望查看一些二进制格式,例如二进制XML。我可以说我们的其他产品MsgConnect的TMCDataTree类支持以二进制格式保存的分层ini文件。