Как сохранить динамические данные (неизвестное количество полей) в файл? - PullRequest
5 голосов
/ 11 февраля 2011

Мне нужно хранить некоторые данные в файле. ДЛЯ МОМЕНТА каждая запись (набор данных) состоит из:

  • строка (переменной длины),
  • массив целых чисел (переменной длины),
  • массив байтов (переменной длины),
  • некоторые целочисленные значения.

Нетрудно сохранить все эти вещи в двоичном файле. Однако я точно знаю, что (к сожалению) мой формат данных со временем изменится, и я хочу иметь возможность добавлять больше полей к каждой «записи». Итак, очевидно, мой формат файла не может быть исправлен. Я полагаю, что лучшим решением будет сохранение моих данных в таблице (БД), но я не хочу связываться с большими инструментами (SQL, ADO, BDE, Nexus ...) Мне нужна элементарная библиотека (если возможно, один файл PAS), которая может это сделать. Поскольку целью этого является скорее хранение данных, чем работа с данными, можно ли это сделать без таблицы БД?

Требования к этой библиотеке:

  • он должен легко поддерживать более 1 миллиона строк
  • действительно легкий
  • один файл PAS, если это возможно
  • ОБЯЗАТЕЛЬНО: легко установить на новый компьютер (вместе с проектом, в котором он компилируется)
  • ОБЯЗАТЕЛЬНО: чтобы его использовать, мне не нужно ничего распространять
  • ОБЯЗАТЕЛЬНО: чтобы использовать его, пользователю не нужно устанавливать / настраивать вещи
  • могут быть бесплатными / условно бесплатными
  • он не должен поддерживать запросы SQL или аналогичные дополнительные вещи

Я использую D7

Ответы [ 6 ]

10 голосов
/ 11 февраля 2011

Взгляните на наше устройство Synopse Big Table .

С его недавним обновлением, оно может идеально соответствовать вашим потребностям.

Вот как вы создаете полеlayout:

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');

О скорости, , вы не можете найти ничего быстрее IMHO .Создание на моем ноутбуке 1 000 000 записей с некоторым текстом и целочисленным значением, причем оба поля используют индекс, а целочисленное поле установлено как уникальное, занимает менее 880 мс.Он будет использовать очень мало дискового пространства, потому что все хранилище закодировано переменной длины (аналогично буферам протокола Google).

Требуется всего два модуля, работает с Delphi 6 до XE (и уже готов к Unicodeпоэтому, используя это устройство, вы можете безопасно перейти на более новую версию Delphi, когда захотите).Нет необходимости в установке, а всего лишь несколько килобайт добавлено в ваш исполняемый файл.Это всего лишь небольшой, но мощный движок NoSQL, написанный на чистом Delphi, но обладающий мощью использования базы данных (т. Е. Макета чистого поля) и скоростью движка в памяти, без ограничений по размеру.

И это полный OpenSource с разрешающей лицензией.

Обратите внимание, что мы также предоставляем оболочку SQLite3 , но это еще один проект.Медленнее, но мощнее, с поддержкой SQL и интегрированным клиент / серверным ORM.

5 голосов
/ 11 февраля 2011

Использовать Synopse BigTable, http://synopse.info/ ключ => значение базы данных, значение в этом случае - сериализация ваших данных (json, binary, xml, ...).

Это CRAZY быстро, легко и бесплатно.

4 голосов
/ 11 февраля 2011

Я не думаю, что вам нужна база данных для этого. Если вы используете базу данных, я не вижу, как она решает проблему изменения структуры данных.

Лично я бы сохранил в формате YAML, который очень легко расширяется. Это требует много работы по связыванию с некоторым LIBYAML, поэтому очень легкая альтернатива - хранить файлы INI. Они легко расширяемы при сохранении совместимости со старыми файлами.

Вы можете довольно легко свернуть свой собственный двоичный формат, который является расширяемым. Что вы делаете, вы пишете каждую запись в блок . Каждый блок имеет короткий заголовок, который включает его длину.

Когда вы читаете данные, которые вы читаете до конца блока, а затем, если вы ожидаете больше данных, вы просто прекращаете чтение и используете для данных значения по умолчанию. Если вы прочитали все данные, о которых вы знаете, но не в конце блока, файл должен быть из более поздней версии вашей программы, и вы просто переходите к концу блока. Возможно, вы предупреждаете, что файл содержит данные, о которых вы не знали.

Расширяемость достигается всегда записывая данные в том же порядке, что и предыдущие версии. Любые новые данные идут в конце каждого блока.

1 голос
/ 12 февраля 2011

Каждый раз, когда вам нужно сохранить данные переменной длины в двоичном формате, вы должны хранить длину данных перед фактическими данными.

Поскольку вам также необходимо добавить новые поля позже, вы должны сохранитьколичество полей в записи (или, по крайней мере, маркер конца записи в конце), чтобы вы могли сохранять правильное положение при перемещении файла во время операций чтения и поиска.

Что касается фактических данных записиЯ бы предложил формат данных типа длина-длина для каждого поля, чтобы вы могли добавлять новые поля, не зная, какие их типы данных будут опережать время, и чтобы код распознавал и считывал / пропускал отдельные поля по мере необходимости независимо от содержимого (то есть, если старое приложение пытается прочитать файл с более новыми полями, оно может пропустить то, что оно не распознает).

В конце вы получите что-то вроде этого как начало, которое вы можетезатем разверните, оптимизируйте и т. д. по мере необходимости:

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;
1 голос
/ 11 февраля 2011

В порядке вашего уровня усилий для реализации, я предлагаю, в следующем порядке:

  1. Файлы CSV или INI (TMemIniFile или TJvCsvDataSet). Это наименьшая работа для вас. Вы можете поддерживать миллионы строк в одном файле, но потребляемая память будет огромной. Я предусмотрел компонент «записи данных», чтобы заменить мой TJvCsvDataSet чем-то, что только добавляет записи, но не загружает их в память. Это позволит вам записывать в файлы CSV и даже читать их обратно, строка за строкой, но не загружать их все сразу. Этот подход может быть идеей для вас. Простой класс чтения / записи CSV, который НЕ является объектом набора данных.

  2. файлы по одному XML-тегу на строку. Это более гибко, чем файлы INI, и может быть иерархическим. INI-файлы не являются иерархическими. Ни SAX, ни DOM не требуются, если вы просто открываете файловый поток и добавляете строку текста в этой форме, оканчивающуюся на cr + lf:

  3. Элемент списка

  4. Какой-то бинарный nosql db, такой как bsddb, couchdb и т. Д.
0 голосов
/ 11 февраля 2011

Я могу придумать что-то вроде одного INI-файла или XML-файла на «запись», когда INI-файлы хранятся в виртуальной файловой системе, такой как SolFS . Однако я не знаю, что вы подразумеваете под «легко поддерживать 1 млн строк», то есть какие операции должны поддерживаться. Если вы планируете в основном произвольный доступ к какому-либо меньшему количеству записей, то нет ничего сложного в разборе текстовых файлов. В другом случае вы можете захотеть взглянуть на некоторый двоичный формат, такой как двоичный XML. Я могу сказать, что класс TMCDataTree нашего другого продукта, MsgConnect, поддерживает иерархические INI-файлы, сохраненные в двоичном формате.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...