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

в 11:53, , рубрики: java, java objects xml store, java xml store, multifiles store, xdstore, XML, метки: , , ,

Введение

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

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

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

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

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

Под понятием объектной ссылки понимается объект указанного класса, у которого проставлено одно поле – идентификатор. В xml файле вместо полных данных этого объекта сохраняется лишь имя класса и идентификатор, чтобы в дальнейшем по этой ссылке можно было получить все данные. Загрузка таких объектов подобна поздней инициализации в hibernate.
Сохраняемые объекты должны быть реализованы как JavaBeans с методами get(is) и set для сохраняемых полей.

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

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

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

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

public interface IXmlDataStoreIdentifiable {

	String	getId();
	
	void	setId(String id);

}

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

<reference class="org.flib.xdstore.entities.XdGalaxy" id="cc74e3f2"/>

При загрузке этой ссылки будет создан объект указанного класса и у него проставлено свойство идентификатора. Остальные поля будут проинициализированы по умолчанию, т. е. они не будут загружены.
Рассмотрим теперь простой пример настройки хранилища для хранения объектов следующих классов: 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 Collection<XdGalaxy>	galaxies;
	
	@Override
	public String getId() {
		return id;
	}

	@Override
	public void setId(final String id) {
		this.id = id;
	}

	public Collection<XdGalaxy> getGalaxies() {
		return galaxies;
	}

	public void setGalaxies(Collection<XdGalaxy> galaxies) {
		this.galaxies = galaxies;
	}

	public void addGalaxy(XdGalaxy galaxy) {
		galaxies.add(galaxy);
	}

        public XdGalaxy removeGalaxy() {
		final Iterator<XdGalaxy> it = galaxies.iterator();
		XdGalaxy galaxy = null;
		if(it.hasNext()) {
			galaxy = it.next();
			it.remove();
		}
		return galaxy;
	}
}

И простенький класс XdGalaxy.

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.

<?xml version="1.0" encoding="UTF-8"?>
<objects>
	<object isNull="false" class="org.flib.xdstore.entities.XdUniverse" id="002df141">
		<collection name="Galaxies" class="java.util.ArrayList">
			<reference class="org.flib.xdstore.entities.XdGalaxy" id="cc74e3f2"/>
			<reference class="org.flib.xdstore.entities.XdGalaxy" id="ca519d20"/>
		</collection>
		<object name="Id" isNull="false" class="java.lang.String" value="002df141"/>
	</object> 
</objects>

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

<?xml version="1.0" encoding="UTF-8"?>
<objects>
	<object isNull="false" class="org.flib.xdstore.entities.XdGalaxy" id="cc74e3f2">
		<object name="Id" isNull="false" class="java.lang.String" value="cc74e3f2"/>
	</object>
	<object isNull="false" class="org.flib.xdstore.entities.XdGalaxy" id="ca519d20">
		<object name="Id" isNull="false" class="java.lang.String" value="ca519d20"/>
	</object> 
</objects>

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

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

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

public class XmlDataStore {
	public XmlDataStoreTransaction beginTransaction();
	
	public void commitTransaction(final XmlDataStoreTransaction transaction);
	
	public void rollbackTransaction(final XmlDataStoreTransaction transaction);

	public <T extends IXmlDataStoreIdentifiable> boolean saveRoot(final T root) throws XmlDataStoreException
	
	public <T extends IXmlDataStoreIdentifiable> boolean saveObject(final T object) throws XmlDataStoreException
	
	public <T extends IXmlDataStoreIdentifiable> boolean saveObjects(final Collection<T> 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) {
	tx.rollback();
}

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

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

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

final XmlDataStore store = initStore("./teststore");
final XmlDataStoreTransaction tx = store.beginTransaction();
try {
	final Map<String, XdUniverse> roots = store.loadRoots(XdUniverse.class);
	for (final XdUniverse root : roots.values()) {
		final Collection<XdGalaxy> galaxies = root.getGalaxies();
		store.loadObjects(galaxies);
	}
	
	tx.commit();
} catch(XmlDataStoreException e) {
	tx.rollback();
}

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

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

Методы обновления (изменения) и удаления объектов представлены ниже.

public class XmlDataStore {
	
	public <T extends IXmlDataStoreIdentifiable> boolean updateRoot(final T root) throws XmlDataStoreException

	public <T extends IXmlDataStoreIdentifiable> boolean deleteRoot(final T root) throws XmlDataStoreException
	
	public <T extends IXmlDataStoreIdentifiable> boolean deleteRoot(final Class<T> cl, final String id)
		throws XmlDataStoreException

	public <T extends IXmlDataStoreIdentifiable> boolean updateObject(final T object) throws XmlDataStoreException

	public <T extends IXmlDataStoreIdentifiable> boolean deleteObject(final T reference) throws XmlDataStoreException

	public <T extends IXmlDataStoreIdentifiable> boolean deleteObjects(final Collection<T> references)
		throws XmlDataStoreException
}

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

final XmlDataStore store = initStore("./teststore");
final XmlDataStoreTransaction tx = store.beginTransaction();
try {
	final Map<String, XdUniverse> roots = store.loadRoots(XdUniverse.class);
	for (final XdUniverse root : roots.values()) {
		final Collection<XdGalaxy> galaxies = 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) {
	tx.rollback();
}

В случае добавления объекта код выглядит следующим образом.

final XmlDataStore store = initStore("./teststore");
final XmlDataStoreTransaction tx = store.beginTransaction();
try {
	final Map<String, XdUniverse> roots = store.loadRoots(XdUniverse.class);
	for (final XdUniverse root : roots.values()) {
		final Collection<XdGalaxy> galaxies = 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) {
	tx.rollback();
}

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

final XmlDataStore store = initStore(storedir);
final XmlDataStoreTransaction tx = store.beginTransaction();
try {
final Map<String, XdUniverse> roots = store.loadRoots(XdUniverse.class);
	for (final XdUniverse root : roots.values()) {
		final Collection<XdGalaxy> galaxies = root.getGalaxies();

		store.deleteObjects(galaxies);
		store.deleteRoot(root);
	}
	tx.commit();
} catch(XmlDataStoreException e) {
	tx.rollback();
}

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

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

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

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

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

Автор: Бесчастный Евгений

Автор: EwgenyB

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js