- производительность, настроенная на чтение-запись отдельных полей записей;
- доступ только по индексу записи, либо по её ключу (никаких запросов и выборок);
- отказоустойчивость может быть принесена в жертву (это лишь кэш);
- размеры файла могут достигать нескольких гигобайт (записи маленькие, но их мно-о-о-ого);
- доступ к записям производится в случайном порядке (т.е. любое кэширование мало что даст);
Впрочем, это все оправдания. Мне же хотелось показать идею и её реализацию, а точнее прототип кода, адаптированный к блогу.
Организацию хранилища записей я позаимствовал у классов ADO. Таблица - набор записей, у таблицы есть набор колонок, доступ к значению поля записи определен на пересечении таблицы, колонки и записи (точнее ее индекса в таблице).
Отличия от класса DataTable в способе хранения данных. Данные хранятся в физическом потоке (Stream) и доступ к ним осуществляется через классы BinaryReader/BinaryWriter.
Начну, пожалуй, с реализации базового класса нетипизированной колонки. Будут и типизированные, но нетипизированные нужны для складывания их в коллекцию колонок:public class Column
{
private readonly int _offset;
private readonly int _size;
private readonly ColumnCollection _columns;
protected Column(ColumnCollection columns, int offset, int size)
{
if (columns == null)
{
throw new ArgumentNullException("columns");
}
_columns = columns;
_offset = offset;
_size = size;
}
public int Offset { get { return _offset; } }
public int Size { get { return _size; } }
public ColumnCollection Columns { get { return _columns; } }
}Здесь все просто. Каждая колонка знает размер значения в байтах, свою коллекцию колонок и смещение относительно начала записи. Все это запоминается в констуркторе колонки. Далее класс типизированной колонки. Типизированная колонка абстрактная, т.к. читать и писать данные придется с помощью BinaryReader/BinaryWriter-а, а они не имеют generic методов для чтения/записи значений.public abstract class Column<T> : Column
{
protected Column(ColumnCollection columns, int offset, int size)
: base(columns, offset, size)
{
}
public abstract T ReadValue(BinaryReader reader);
public abstract void WriteValue(BinaryWriter writer, T value);
}Типичный класс колонки:class Int32Column : Column<int>
{
public Int32Column(ColumnCollection columns, int offset)
: base(columns, offset, 4)
{
}
public override int ReadValue(BinaryReader reader)
{
return reader.ReadInt32();
}
public override void WriteValue(BinaryWriter writer, int value)
{
writer.Write(value);
}
}Таких классов несколько, не буду приводить код каждого из них, все они подобны. Немного отличается только класс строковой колонки, но о ней возможно позже. Обратите внимание, что класс типизированной колонки скрытый! Это объясняется тем, что я не собираюсь предоставлять возможность создания колонок сложных типов. Ограничусь лишь некоторыми примитивными. Класс коллекции колонок содержит в себе список колонок и число байт, требуемое для записи данных во всех колонках.public class ColumnCollection
{
private readonly List<Column> _columns = new List<Column>();
private int _size;
public Column<int> AddInt32Column()
{
return AddInt32Column(this.Size);
}
public Column<int> AddInt32Column(int offset)
{
return AddColumn(new Int32Column(this, offset));
}
private Column<T> AddColumn<T>(Column<T> column)
{
_size = Math.Max(_size, column.Offset + column.Size);
_columns.Add(column);
return column;
}
public int Size { get { return _size; } }
}Коллекция колонок является так же производящим классом для типизированных колонок. Для каждого типа колонки (а их у меня чуть больше, чем только Int32Column) определено два метода создания экземпляра колонки: первый метод добавляет колонку в конец записи (указывает Offset, равный текущему размеру коллекции колонок в байтах), а второй - позволяет указать Offset вручную. Это потребуется для так называемых union полей.Вот что из себя представляет базовый тип хранилища:
public abstract class StreamStorage
{
public abstract T ReadValue<T>(Column<T> column, int rowIndex);
public abstract void WriteValue<T>(Column<T> column, int rowIndex, T value);
}Всего лишь два абстрактных метода, позволяющий писать и читать значения, соответствующие указанным колонкам и индексу записи. Этот базовый тип нужен для объявления базового типа записи. Хотел я параметризовать тип записи типом хранилища и тип хранилища типом записи, и возможно смог бы это сделать, но меня посетила идея, что у файла должен быть заголовок. И что заголовок - это второй тип записи. Т.е. хранилище надо параметризовывать двумя типами записи, но тогда каждую запись надо будет параметризовывать типом хранилища, в котором 2 типа записи... Но это все лишнее. Записи от хранилища не нужно ничего, кроме возможности обратиться к вышеуказанным методам. Потому базовый тип хранилища не имеет generic параметров. А базовый тип записи имеет generic параметр - конкретный тип записи: public class RowBase<TRow>
where TRow : RowBase<TRow>
{
private static readonly ColumnCollection s_columns = new ColumnCollection();
public static ColumnCollection ColumnCollection { get { return s_columns; } }
private readonly int _index;
private readonly StreamStorage _storage;
protected RowBase(StreamStorage storage, int index)
{
_storage = storage;
_index = index;
}
public virtual void Initialize()
{
}
public int GetIndex()
{
return _index;
}
public StreamStorage GetStorage()
{
return _storage;
}
protected T ReadValue<T>(Column<T> column)
{
return _storage.ReadValue(column, GetIndex());
}
protected void WriteValue<T>(Column<T> column, T value)
{
_storage.WriteValue(column, GetIndex(), value);
}
}Базовый класс записи со спецификацией типа (т.е. все записи одного типа) привязаны к одному экземпляру коллекции колонок. Сделано это лишь для проверок. Для работоспособности хранилища интересен лишь размер записи, который определен в коллекции колонок и смещения самих колонок относительно записи. Но хранилище так же будет знать экземпляр коллекции колонок для проверки экземпляров колонок, которые приходят на публичные методы чтения и записи значений. Базовый класс записи содержит в себе ссылку на экземпляр хранилища, свой индекс, и определяет защищенные методы для чтения и записи значений.Надеюсь, что код конкретной записи прояснит все что не ясно:
public class TestRow : RowBase<TestRow>
{
private static readonly Column<Guid> s_GuidColumn = ColumnCollection.AddGuidColumn();
private static readonly Column<int> s_Int32Column = ColumnCollection.AddInt32Column();
public TestRow(StreamStorage storage, int index)
: base(storage, index)
{
}
public Guid GuidValue
{
get { return ReadValue(s_GuidColumn); }
set { WriteValue(s_GuidColumn , value); }
}
public int Int32Value
{
get { return ReadValue(s_Int32Column); }
set { WriteValue(s_Int32Column, value); }
}
public override void Initialize()
{
base.Initialize();
GuidValue = Guid.NewGuid();
Int32Value = 0;
}
}Итак, тип конкретной записи определяет колонки (статические поля), которые создает обращаясь к статической коллекции колонок, определенной у базового типа записи. Тип конкретной записи определяет свойства экземпляра записи. Реализация каждого свойства обращается к базовым методам чтения/записи значения и передает соответствующий экземпляр колонки. При обращении к свойству записи происходит передача колонки и номера записи хранилищу и вызывается метод чтения либо записи значения из хранилища.О том, как устроено хранилище - в другой раз. Впрочем, наверняка идея уже ясна.