- производительность, настроенная на чтение-запись отдельных полей записей;
- доступ только по индексу записи, либо по её ключу (никаких запросов и выборок);
- отказоустойчивость может быть принесена в жертву (это лишь кэш);
- размеры файла могут достигать нескольких гигобайт (записи маленькие, но их мно-о-о-ого);
- доступ к записям производится в случайном порядке (т.е. любое кэширование мало что даст);
Впрочем, это все оправдания. Мне же хотелось показать идею и её реализацию, а точнее прототип кода, адаптированный к блогу.
Организацию хранилища записей я позаимствовал у классов 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; } }Итак, тип конкретной записи определяет колонки (статические поля), которые создает обращаясь к статической коллекции колонок, определенной у базового типа записи. Тип конкретной записи определяет свойства экземпляра записи. Реализация каждого свойства обращается к базовым методам чтения/записи значения и передает соответствующий экземпляр колонки. При обращении к свойству записи происходит передача колонки и номера записи хранилищу и вызывается метод чтения либо записи значения из хранилища.
О том, как устроено хранилище - в другой раз. Впрочем, наверняка идея уже ясна.