Storage classes in java

Много-файловое хранилище Java объектов в формате xml

В программировании часто перед нами встают задачи, которые мы можем решить несколькими путями: найти и использовать уже готовые решения, или же решать задачу самостоятельно. Хоть и написано множество спецификаций и их реализаций, они не всегда дают нам то, что требуется в конкретном случае. Вот и мне в очередной раз пришлось столкнуться с подобной ситуацией.
Задача состояла в хранении объектов в файле в формате xml. Ничего казалось бы сложного, если бы не несколько «но». Объектов много, имеют они древовидную структуру и над ними постоянно выполняются операции добавления, изменения и удаления в разных потоках. Как вы понимаете постоянные запись и чтение большого xml файла довольно трудоемкая задача. Тем более если с одними и теми же данными работают несколько потоков. Так собственно и родилась идея написать много-файловое хранилище объектов в формате xml.
В этой статье я не буду рассматривать саму реализацию. Приведу лишь основные идеи и как использовать эту реализацию. Если вы хотите углубиться, то можете скачать посмотреть исходные коды.

Исходники доступны по ссылке: xdstore-1.3
Исходные тексты немного отличаются от приведенных в этой статье. В них были глубже проработаны исключительные ситуации, а именно, — для каждой операции, включая чтение, выбрасывается свое исключение. Также в последней версии реализована фрагментация.

Основная идея разработки

  • ParentObjectFile – объекты класса будут сохраняться в файле объекта владельца как дочерние элементы, эта политика применяется по умолчанию;
  • SingleObjectFile – каждому объекту класса предоставляется отдельный файл, а в файле объекта владельца будет сохранена лишь ссылка на этот объект (в дальнейшем буду просто называть ее объектной ссылкой); все файлы каждого объекта будут сохраняться в отдельной папке внутри хранилища;
  • ClassObjectsFile – все объекты этого класса будут храниться в отдельном файле, а в файлах объектов владельцев будут сохранены лишь объектные ссылки.

Одна интересная задача

Чтобы лучше понять ситуацию, в которую мы попадаем при попытке реализовать такое хранилище, необходимо правильно поставить задачу. В терминах БД звучит она следующим образом: в таблице базы данных имеются две строки, одновременно начинаются две транзакции, каждая из которых модифицирует обе строки, затем завершается коммитом первая транзакция и начинается третья, которая также модифицирует эти две строки.
Нас интересует поведение в подобной ситуации, т.е. что произойдет с данными в каждой из транзакций. В текущей реализации библиотеки поведение будет следующим:
1) Поскольку данные были модифицированы первой транзакцией, то вторая транзакция получит отказ на изменение данных в виде исключения. Объясняется это тем, что первая и вторая транзакции начались в одно время и скорее всего работали с одинаковыми копиями, и чтобы не потерять изменения первой транзакции второй необходимо отказать.
2) А вот данные третьей транзакции будут приняты, поскольку она началась после коммита первой транзакции и работает с обновленными данными.
Поскольку это довольно простая реализация, то при решении поставленной задачи не использовались блокировки записей чтобы избежать deadlock-ов и необходимости отката транзакций по таймауту. В этом случае выбрасывается исключение, по которому транзакция должна быть откачена.

Читайте также:  Arraylist java добавление элемента

Начало использования

Сама цель данной разработки получить простую и гибкую библиотеку, позволяющую сохранять объекты в формате xml. Поэтому получившийся интерфейс довольно прост, а требования предъявляемые к сохраняемым объектам сведены к минимуму. Основное требование для каждого сохраняемого объекта заключается в необходимости реализовать простой интерфейс IXmlDataStoreIdentifiable. Выглядит он следующим образом:

public interface IXmlDataStoreIdentifiable

Как вы можете видеть, необходимо лишь реализовать два метода работы с идентификатором объекта. Это необходимое условие обусловлено тем, что при некоторых политиках сохраняются лишь ссылки на объекты, по которым в дальнейшем может потребоваться восстановить (загрузить) все свойства. Ссылка в xml файле выглядит следующим образом:

При загрузке этой ссылки будет создан объект указанного класса и у него проставлено свойство идентификатора. Остальные поля будут проинициализированы по умолчанию, т. е. они не будут загружены.
Рассмотрим теперь простой пример настройки хранилища для хранения объектов следующих классов: XdUniverse и XdGalaxy. Для начала определим их классы.

package org.flib.xdstore.entities; import java.util.Collection; import org.flib.xdstore.IXmlDataStoreIdentifiable; public class XdUniverse implements IXmlDataStoreIdentifiable < private String id; private Collectiongalaxies; @Override public String getId() < return id; >@Override public void setId(final String id) < this.id = id; >public Collection getGalaxies() < return galaxies; >public void setGalaxies(Collection galaxies) < this.galaxies = galaxies; >public void addGalaxy(XdGalaxy galaxy) < galaxies.add(galaxy); >public XdGalaxy removeGalaxy() < final Iteratorit = galaxies.iterator(); XdGalaxy galaxy = null; if(it.hasNext()) < galaxy = it.next(); it.remove(); >return galaxy; > > 
package org.flib.xdstore.entities; import org.flib.xdstore.IXmlDataStoreIdentifiable; public class XdGalaxy implements IXmlDataStoreIdentifiable < private String id; @Override public String getId() < return id; >@Override public void setId(String id) < this.id = id; >> 
final XmlDataStore store = new XmlDataStore("./teststore"); store.setStorePolicy(XdUniverse.class, XmlDataStorePolicy.ClassObjectsFile); store.setStorePolicy(XdGalaxy.class, XmlDataStorePolicy.ClassObjectsFile); 

Сейчас мы выбрали настройки, что все объекты каждого из классов будут храниться в своем файле, т.е. для каждого класса один файл. Можно использовать другие настройки и, например, не указывать политику для класса XdGalaxy, — тогда его объекты будут сохраняться вместе с объектами класса XdUniverse.
В результате для наших настроек после записи объектов мы получим два файла: XdUniverse.xml и XdGalaxy.xml.

Как видно из примера в этом файле хранятся ссылки на объекты из второго файла XdGalaxy.xml, приведенного ниже.

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

Сохранение и загрузка объектов

Рассмотрим интерфейс класса XmlDataStore, касающийся операций сохранения объектов. Он довольно прост и позволяет нам сохранять объекты без указания политик, поскольку они уже проставлены при инициализации хранилища.

public class XmlDataStore < public XmlDataStoreTransaction beginTransaction(); public void commitTransaction(final XmlDataStoreTransaction transaction); public void rollbackTransaction(final XmlDataStoreTransaction transaction); public boolean saveRoot(final T root) throws XmlDataStoreException public boolean saveObject(final T object) throws XmlDataStoreException public boolean saveObjects(final Collection objects) throws XmlDataStoreException > 

Хранилище разрабатывалось для многопоточного использования и в ходе работы может быть задействовано несколько ресурсных объектов, поэтому оно использует механизм транзакций и предоставляет соответствующие методы. Принятие и откат транзакции могут быть вызваны также через методы объекта самой транзакции.
Сохранение корневых объектов и дочерних объектов немного отличаются, поэтому методы работы над корневыми объектами выделены в отдельную группу. Отличие заключается в том, что при политике SingleObjectFile для каждого корневого объекта будет выделен отдельный файл и в дополнение для них всех создан дополнительный файл, в котором будут храниться ссылки. Это позволяет разом загрузить все корневые объекты.
Теперь рассмотрим операцию сохранения.

final XmlDataStore store = initStore("./teststore"); final XdUniverse universe = generateUniverse(); final XmlDataStoreTransaction tx = store.beginTransaction(); try < store.saveRoot(universe); store.saveObjects(universe.getGalaxies()); tx.commit(); >catch (XmlDataStoreException e)

Из примера видно, что сохранить объекты довольно просто. Отметим лишь тот момент, что поскольку объекты класса XdGalaxy сохраняются в отдельном файле, нам необходимо явно выполнить операцию их сохранения. Их можно также сохранять по отдельности используя другой метод, описанный выше. Сама запись объектов в файл происходит при выполнении операции принятия транзакции и до тех пор пока она не вызвана все операции производятся с кэшем.
Рассмотрим теперь часть интерфейса, относящуюся к загрузке объектов из хранилища.

public class XmlDataStore < public Map loadRoots(final Class cl) throws XmlDataStoreException public T loadRoot(final Class cl, final String id) throws XmlDataStoreException public boolean loadObject(final T reference) throws XmlDataStoreException public T loadObject(Class cl, final String id) throws XmlDataStoreException public boolean loadObjects(final Collection references) throws XmlDataStoreException > 

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

final XmlDataStore store = initStore("./teststore"); final XmlDataStoreTransaction tx = store.beginTransaction(); try < final Maproots = store.loadRoots(XdUniverse.class); for (final XdUniverse root : roots.values()) < final Collectiongalaxies = root.getGalaxies(); store.loadObjects(galaxies); > tx.commit(); > catch(XmlDataStoreException e)

Из примера видно, что сначала загружаются все корни, а затем для каждого корня по объектным ссылкам загружаются все дочерние объекты.

Читайте также:  Styling text links in css

Обновление и удаление объектов

public class XmlDataStore < public boolean updateRoot(final T root) throws XmlDataStoreException public boolean deleteRoot(final T root) throws XmlDataStoreException public boolean deleteRoot(final Class cl, final String id) throws XmlDataStoreException public boolean updateObject(final T object) throws XmlDataStoreException public boolean deleteObject(final T reference) throws XmlDataStoreException public boolean deleteObjects(final Collection references) throws XmlDataStoreException > 

Следует отметить, что все зависимые объекты, которые хранятся в отдельных от владельца файлах, должны быть явно обновлены или удалены. Например, в нашем случае при удалении объекта класса XdGalaxy из объекта XdUniverse необходимо обновить объект XdUniverse и дополнительно явно удалить XdGalaxy.

final XmlDataStore store = initStore("./teststore"); final XmlDataStoreTransaction tx = store.beginTransaction(); try < final Maproots = store.loadRoots(XdUniverse.class); for (final XdUniverse root : roots.values()) < final Collectiongalaxies = root.getGalaxies(); store.loadObjects(galaxies); > if(roots.size() > 0) < final XdUniverse universe = roots.values().iterator().next(); final XdGalaxy galaxy = universe.removeGalaxy(); if(galaxy != null) < store.updateRoot(universe); store.deleteObject(galaxy); >> tx.commit(); > catch(XmlDataStoreException e)
final XmlDataStore store = initStore("./teststore"); final XmlDataStoreTransaction tx = store.beginTransaction(); try < final Maproots = store.loadRoots(XdUniverse.class); for (final XdUniverse root : roots.values()) < final Collectiongalaxies = root.getGalaxies(); store.loadObjects(galaxies); > if(roots.size() > 0) < final XdUniverse universe = roots.values().iterator().next(); final XdGalaxy galaxy = initGalaxy(); // initialization XdGalaxy universe.addGalaxy(galaxy); store.updateRoot(universe); store.saveObject(galaxy); >tx.commit(); > catch(XmlDataStoreException e)

Если же политика сохранения ParentObjectFile, то для дочерних объектов нет необходимости явно выполнять операции удаления и сохранения, поскольку после обновления объекта владельца необходимая операция будет выполнена автоматически.
Полная очистка нашего хранилища будет выглядеть следующим образом:

final XmlDataStore store = initStore(storedir); final XmlDataStoreTransaction tx = store.beginTransaction(); try < final Maproots = store.loadRoots(XdUniverse.class); for (final XdUniverse root : roots.values()) < final Collectiongalaxies = root.getGalaxies(); store.deleteObjects(galaxies); store.deleteRoot(root); > tx.commit(); > catch(XmlDataStoreException e)

Из примера видно, что нам даже не потребовалось загружать объекты класса XdGalaxy перед удалением. Мы просто передали коллекцию объектных ссылок. Это возможно поскольку объектная ссылка хранит идентификатор объекта.

Читайте также:  Birthday Reminders for August

Немного о реализации

Для повышения производительности работы хранилища используется неотключаемое кэширование. Т.е. при работе с любым ресурсным объектом (файлом) все хранимые в нем объекты загружаются и кэшируются при первой транзакции. Все остальные транзакции работают с уже кэшированными данными. Данные кэша сбрасываются, когда завершается последняя транзакция, которая работает с этим ресурсным объектом. Все изменения регистрируются в кэше и не сбрасываются на диск до тех пор, пока не происходит принятие транзакции.
Поскольку в ходе выполнения транзакции может быть затронуто неопределенное количество ресурсных объектов, то операция принятия изменений транзакции выполняется над всеми поочередно. Если при этом процессе происходит какая-либо ошибка, то целостность хранилища данных нарушается и выбрасывается исключение типа XmlDataStoreRuntimeException. В текущей реализации восстановление целостного состояния хранилища не реализовано. Это один из существенных недостатков текущей версии.

Планы по развитию

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

Источник

Оцените статью