Класс TSerializableCollection
Является центральным элементом концепции объектного хранилища данных. Коллекция содержит набор объектов типа, производного от TSerializableObject, который определяется параметром ItemClassType при вызове конструктора класса TSerializableCollection. Таким образом, все элементы коллекции имеют один и тот же тип. Полиморфизм здесь не используется, т.к. возможность присутствия в коллекции объектов различного типа приводит к неоправданному усложнению механизмов работы с данными. Внутренний массив элементов, к которому можно обратиться через свойство ItemList, упорядочен по возрастанию значений первичного ключа, т.е. ID. Количество элементов в коллекции возвращается свойством Count. Для ускорения поиска элементы коллекции могут хешироваться по первичному ключу. Чтобы включить хеширование, надо установить в True значение свойства MaintainHash коллекции. По умолчанию для экономии памяти хеширование отключено. Найти элемент коллекции по значению первичного ключа или просто убедиться в том, что элемент с таким ключом присутствует в коллекции, можно с помощью метода SearchObject. Если нужен не сам объект, а его индекс во внутреннем массиве элементов, следует воспользоваться методом IndexOf. Если в коллекции мало элементов и нужно определить индекс одного из них во внутреннем массиве, можно вызвать метод ScanPointer для линейного поиска указателя. Если элементов много, функция IndexOf быстрее найдет нужный элемент методом бинарного поиска.
Коллекция может быть загружена из бинарного потока типа TBinaryReader методом Load и сохранена в бинарном потоке типа TBinaryWriter методом Save. Метод Equals поэлементно сравнивает данную коллекцию с другой коллекцией и возвращает True, если все соответствующие элементы обеих коллекций равны. Функция Clone возвращает копию коллекции, содержащую копии всех элементов и каждого из индексов. Если предполагается добавить в коллекцию большое число элементов, можно вызвать метод EnsureCapacity для резервирования места во внутренних массивах коллекции.
Для полной очистки коллекции используется метод Clear. При вызове этого метода свойство Changed устанавливается в False, т.к. эта операция не предполагает фактического удаления данных на диске. Например, метод Clear вызывается перед загрузкой с диска обновленных данных коллекции. Чтобы по-настоящему удалить из коллекции все элементы, нужно вызвать метод Delete или DeleteDirect для каждого элемента коллекции. Однако, при монопольном доступе к данным и отсутствии необходимости вызова события OnItemDeleted для каждого удаляемого элемента коллекции, можно очистить ее методом Clear, а затем вручную установить свойство Changed в значение True. Это самый быстрый способ удаления всех данных. Новые элементы коллекции создаются вызовом функции NewItem класса TSerializableCollection. При этом элемент сразу не добавляется в коллекцию. Сначала он должен быть заполнен данными. Затем, для подтверждения изменений вызывается метод EndEdit, который помещает информацию о добавлении новой записи в кэш изменений. Если новая запись не нужна, например, если пользователь нажал кнопку "Отмена" в окне добавления записи, нужно вызвать метод CancelEdit для освобождения памяти, занятой новым элементом. При необходимости корректировки данных нельзя вносить изменения непосредственно в элементы списка ItemList коллекции. Вместо этого, должен быть вызван метод BeginEdit, в который передан идентификатор изменяемого элемента коллекции. Метод BeginEdit возвращает ссылку на копию элемента, в которую можно вносить изменения. Затем эта копия передается в методы EndEdit для подтверждения изменений или CancelEdit для отмены изменений. Удалить элемент коллекции можно вызовом метода Delete с передачей в него идентификатора удаляемого элемента. Все описанные выше манипуляции с объектами не затрагивают основной список элементов ItemList, т.е. фактический набор данных коллекции. Изменения кэшируются во внутренних массивах: InsertedItemList и DeletedItemList с числом элементов, соответственно, InsertedCount и DeletedCount.
В первом из этих массивов находятся элементы, добавленные в коллекцию, а также исправленные, т.е. новые, версии измененных объектов. Во втором массиве находятся удаленные объекты и исходные, т.е. старые, версии измененных объектов. Эти массивы отсортированы в порядке убывания идентификаторов элементов. Проверить наличие кэшированных изменений вообще или изменений для элемента с конкретным идентификатором можно вызовом функции HasChanges. Чтобы применить кэшированные изменения к основному набору элементов используется метод ApplyChanges. Обычно перед вызовом этого метода проверяется наличие обновленных данных на файловом сервере. Если версия файла данных на сервере изменилась, коллекция перечитывается с диска и изменения применяются к новым данным. Управление версиями файлов будет рассмотрено далее в этом разделе. В классе TSerializableCollection предусмотрены специальные события: OnItemInserted, OnItemChanged, OnItemDeleted, которые инициируются, соответственно, при добавлении, изменении и удалении элемента основного набора данных коллекции. Метод ApplyChanges может вернуть одно из следующих значений: appChangesOk (изменения применены успешно), appChangesOriginalObjectChanged (произошла ошибка, связанная с тем, что изменяемый элемент коллекции был одновременно изменен другим пользователем), appChangesUniqueIndexViolation (ошибка, возникающая при попытке вставить значение, нарушающее уникальность одного из индексов коллекции, который не допускает дублирования значений). Чтобы очистить кэш изменений без фактической модификации данных используется метод RejectChanges. Поясним работу с первичными ключами элементов коллекции. При создании нового элемента методом NewItem свойству ID этого элемента присваивается временное отрицательное значение, которое последовательно уменьшается для каждого следующего создаваемого элемента. При вызове ApplyChanges для добавляемого элемента временное значение ID заменяется настоящим идентификатором, который создается функцией GenerateID коллекции.
При изменении идентификатора объекта в коллекции инициируется событие OnItemIDChanged, чтобы можно было обновить внешние ключи в элементах других коллекций, ссылающихся на добавленный элемент. В экземпляре класса TSerializableCollection предполагается, что элементы сохраняют значение свойства ID в виде числа типа Integer. Если заранее известно, что число элементов коллекции не превысит 65535, можно хранить уникальные идентификаторы как 2-байтные значения типа Word. Тогда вместо класса TSerializableCollection надо использовать производный от него класс TWordPrimaryKeyCollection, в котором перекрывается метод GenerateID, чтобы значения первичного ключа не превышали 65535. Если в коллекции может быть не более 255 элементов, имеет смысл хранить ID как один байт. Тогда в качестве коллекции нужно использовать класс TBytePrimaryKeyCollection. Возможна также ситуация, когда вообще нет смысла сохранять уникальный идентификатор вместе с данными объекта. Значение ID может динамически назначаться в момент загрузки коллекции из бинарного потока. Для реализации такой возможности предусмотрен класс TFakePrimaryKeyCollection. При отказе от хранения идентификатора возникает проблема удаления элементов в режиме многопользовательского доступа, т.к. после удаления элемента и повторной загрузки коллекции динамические идентификаторы следующих за ним элементов изменятся. Таким образом, при работе с TFakePrimaryKeyCollection в режиме многопользовательского доступа удаление отдельных элементов коллекции должно быть запрещено. В классе TSerializableCollection есть еще пара методов для работы с данными в обход кэша изменений. Метод EndEditDirect подтверждает изменение или добавление нового элемента аналогично методу EndEdit, а затем сразу применяет это изменение к основному набору данных, что эквивалентно вызову метода ApplyChanges с идентификатором только что измененного или добавленного объекта. Метод DeleteDirect удаляет элемент с указанным идентификатором из основного набора данных. Методы EndEditDirect и DeleteDirect подходят для работы с данными в режиме монопольного доступа, когда другие пользователи не имеют возможности изменить файл данных одновременно с текущими изменениями. Каждая коллекция, не являющаяся частью какого-либо объекта, хранится на диске в виде отдельного файла.
Для загрузки коллекции из файла предназначен метод LoadFile класса TSerializableCollection, для ее сохранения в файле – метод SaveFileDirect. Второй метод не предназначен для работы с данными в режиме многопользовательского доступа, т.к. он перезаписывает любые изменения, сделанные другими пользователями. При одновременной работе нескольких пользователей для сохранения изменений необходимо открыть файл данных методом OpenFile, затем применить кэшированные изменения к данным методом ApplyChanges. Если изменения применены успешно, можно сохранить коллекцию на диске вызовом метода SaveIfChanged. Если во время применения изменений к данным произошла ошибка, необходимо восстановить исходное состояние набора данных в памяти. Для этого используется метод UndoIfChanged коллекции при открытом файле данных. В конце операции файл данных должен быть закрыт методом CloseFile. Данные коллекции могут сохраняться на диске в упакованном виде. Для этого в методы OpenFile и SaveFileDirect передается параметр CompressionMode, который, если он отличен от dcmNoCompression, задает режим сжатия записываемого файла данных (одна из констант, перечисленных в описании модуля AcedCompression). Не следует, однако, применять сжатие к файлам, которые содержат часто изменяемые данные, так как это может значительно понизить общую производительность системы, особенно в режиме многопользовательского доступа к данным. В упакованном виде лучше хранить справочники, коды и прочие данные, которые меняются редко. В методы LoadFile, OpenFile и SaveFileDirect передается также параметр EncryptionKey. Он позволяет указать ключ для шифрования файла данных методом RC6. При использовании шифрования, данные, кроме всего прочего, защищаются цифровой сигнатурой SHA-256. Так же как и сжатие, шифрование не стоит использовать для часто изменяемых данных без крайней на то необходимости. Несколько пользователей могут одновременно читать данные из одного файла методом LoadFile. Однако, если кто-либо открыл файл методом OpenFile, другие пользователи не могут открыть этот файл для чтения или для записи, пока он не будет закрыт методом CloseFile.
При отказе в открытии файла на экране появляется сообщение для пользователя с предложением подождать или повторить попытку открытия файла. Кроме того, пользователь может отменить текущую операцию, в результате чего методы LoadFile, OpenFile и SaveFileDirect вернут значение False. При успешном открытии файла эти методы возвращают True. В первых четырех байтах файла данных хранится его версия – число, которое изменяется при каждом сохранении данных на диске. Если основной набор данных коллекции не менялся с момента ее загрузки в память или с момента предыдущего сохранения на диске (свойство Changed коллекции равно False), и при вызове метода LoadFile оказывается, что версия файла данных не изменилась за это время, то фактического считывания данных не происходит, так как в этом нет необходимости. То же самое происходит при открытии файла методом OpenFile. Это позволяет уменьшить нагрузку на файловый сервер, сократить объем информации, передаваемой по сети, и в целом повысить производительность приложения.