Много-файловое хранилище Java объектов в формате 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, трудоемкость операций чтения и записи растет прямо пропорционально росту количества объектов. Для того чтобы повысить производительность хранилища планируется реализовать фрагментацию и построение файла индекса. Фрагментация подразумевает под собой разбиение одного файла на фрагменты, содержащие ограниченное количество объектов, а индекс в данном случае будет содержать ссылки с указанием файла фрагмента, в котором сохранен объект.
Также в планы входит реализация восстановления целостного состояния хранилища после сбоя при принятии изменений транзакции.
Возможно, что в новой реализации хранилища появятся триггеры, которые будут вызываться при изменении состояния хранимых объектов. Т.е. при добавлении, изменении или удалении объектов.

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

ссылка на оригинал статьи http://habrahabr.ru/post/185890/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *