Автор оригинала: Johannes Lichtenberger.
1. Обзор
В этом уроке мы дадим обзор того, что такое SirixDB , и его наиболее важных целей проектирования.
Далее мы рассмотрим низкоуровневый транзакционный API на основе курсора.
2. Особенности SirixDB
SirixDB-это структурированное по журналам временное хранилище документов NoSQL, в котором хранятся эволюционные данные. Он никогда не перезаписывает данные на диске. Таким образом, мы можем эффективно восстанавливать и запрашивать полную историю изменений ресурса в базе данных. SirixDB гарантирует, что для каждой новой версии создается минимум накладных расходов на хранение.
В настоящее время SirixDB предлагает две встроенные собственные модели данных, а именно двоичное хранилище XML, а также хранилище JSON.
2.1. Цели Проектирования
Некоторые из наиболее важных основных принципов и целей проектирования:
- Параллелизм – SirixDB содержит очень мало блокировок и стремится быть максимально подходящим для многопоточных систем
- Асинхронный REST API – операции могут выполняться независимо; каждая транзакция привязана к определенной ревизии, и только одна транзакция чтения-записи на ресурсе разрешена одновременно для N транзакций только для чтения
- Управление версиями/История ревизий – SirixDB хранит историю ревизий каждого ресурса в базе данных, сводя к минимуму накладные расходы на хранение. Производительность чтения и записи настраивается. Это зависит от типа управления версиями, который мы можем указать для создания ресурса
- Целостность данных – SirixDB, как и ZFS, хранит полные контрольные суммы страниц на родительских страницах. Это означает, что почти все повреждения данных могут быть обнаружены при чтении в будущем, поскольку разработчики SirixDB стремятся разделить и реплицировать базы данных в будущем
- Семантика копирования при записи- аналогично файловым системам Btrfs и ZFS, SirixDB использует семантику CoW, что означает, что SirixDB никогда не перезаписывает данные. Вместо этого фрагменты страниц базы данных копируются и записываются в новое место
- Управление версиями для каждой версии и для каждой записи – SirixDB выполняет не только версию для каждой страницы, но и для каждой записи. Таким образом, всякий раз, когда мы изменяем потенциально небольшую часть
записей на странице данных, ей не нужно копировать всю страницу и записывать ее в новое место на диске или флэш-накопителе. Вместо этого мы можем указать одну из нескольких стратегий управления версиями, известных из систем резервного копирования, или алгоритм скользящего моментального снимка во время создания ресурса базы данных. Указанный нами тип управления версиями используется SirixDB для версирования страниц данных - Гарантированная атомарность (без WAL) – система никогда не перейдет в несогласованное состояние (если только не произойдет аппаратный сбой), что означает, что неожиданное отключение питания никогда не повредит системе. Это достигается без накладных расходов на запись в журнал ( WAL )
- Лог-структурированный и удобный для SSD – SirixDB пакетами записывает и синхронизирует все последовательно на флэш-накопитель во время фиксации. Он никогда не перезаписывает зафиксированные данные
Сначала мы хотим представить низкоуровневый API на примере данных JSON, прежде чем переключить наше внимание на более высокие уровни в будущих статьях. Например, XQuery-API для запросов к базам данных XML и JSON или асинхронный, временный RESTful API. В основном мы можем использовать один и тот же низкоуровневый API с небольшими различиями для хранения, просмотра и сравнения XML-ресурсов.
Чтобы использовать SirixDB, мы, по крайней мере, должны использовать Java 11 .
3. Зависимость Maven для встраивания SirixDB
Чтобы следовать примерам, мы сначала должны включить | sirix-core dependency , например, через Maven:
io.sirix sirix-core 0.9.3
Или через Gradle:
dependencies { compile 'io.sirix:sirix-core:0.9.3' }
4. Древовидное кодирование в SirixDB
Узел в SirixDB ссылается на другие узлы с помощью первого дочернего/левого родного брата/правого родного брата/ключа родительского узла/nodeKey кодирования:
Числа на рисунке являются автоматически генерируемыми уникальными, стабильными идентификаторами узлов, генерируемыми с помощью простого генератора последовательных чисел.
У каждого узла может быть первый ребенок, левый брат, правый брат и родительский узел. Кроме того, SirixDB может хранить количество дочерних элементов, количество потомков и хэши каждого узла.
В следующих разделах мы представим базовый низкоуровневый JSON API SirixDB.
5. Создайте Базу Данных С Одним Ресурсом
Во-первых, мы хотим показать, как создать базу данных с одним ресурсом. Ресурс будет импортирован из файла JSON и сохранен во внутреннем двоичном формате SirixDB:
var pathToJsonFile = Paths.get("jsonFile"); var databaseFile = Paths.get("database"); Databases.createJsonDatabase(new DatabaseConfiguration(databaseFile)); try (var database = Databases.openJsonDatabase(databaseFile)) { database.createResource(ResourceConfiguration.newBuilder("resource").build()); try (var manager = database.openResourceManager("resource"); var wtx = manager.beginNodeTrx()) { wtx.insertSubtreeAsFirstChild(JsonShredder.createFileReader(pathToJsonFile)); wtx.commit(); } }
Сначала мы создаем базу данных. Когда мы открываем базу данных и создаем первый ресурс. Существуют различные варианты создания ресурса ( см. Официальную документацию ).
Затем мы открываем одну транзакцию чтения-записи на ресурсе для импорта файла JSON. Транзакция предоставляет курсор для навигации по методам moveto . Кроме того, транзакция предоставляет методы для вставки, удаления или изменения узлов. Обратите внимание, что XML API даже предоставляет методы для перемещения узлов в ресурсе и копирования узлов из других XML-ресурсов.
Чтобы правильно закрыть открытую транзакцию чтения-записи, диспетчер ресурсов и базу данных, мы используем инструкцию Java try-with-resources.
Мы проиллюстрировали создание базы данных и ресурса на данных JSON, но создание базы данных XML и ресурса почти идентичны.
В следующем разделе мы откроем ресурс в базе данных и покажем навигационные оси и методы.
6. Откройте ресурс в базе данных и перейдите
6.1. Навигация по предварительному заказу в ресурсе JSON
Чтобы перемещаться по древовидной структуре, мы можем повторно использовать транзакцию чтения-записи после фиксации. Однако в следующем коде мы снова откроем ресурс и начнем транзакцию только для чтения с самой последней версии:
try (var database = Databases.openJsonDatabase(databaseFile); var manager = database.openResourceManager("resource"); var rtx = manager.beginNodeReadOnlyTrx()) { new DescendantAxis(rtx, IncludeSelf.YES).forEach((unused) -> { switch (rtx.getKind()) { case OBJECT: case ARRAY: LOG.info(rtx.getDescendantCount()); LOG.info(rtx.getChildCount()); LOG.info(rtx.getHash()); break; case OBJECT_KEY: LOG.info(rtx.getName()); break; case STRING_VALUE: case BOOLEAN_VALUE: case NUMBER_VALUE: case NULL_VALUE: LOG.info(rtx.getValue()); break; default: } }); }
Мы используем ось потомков для перебора всех узлов в предварительном порядке (сначала по глубине). Хэши узлов создаются снизу вверх для всех узлов по умолчанию в зависимости от конфигурации ресурса.
Узлы массива и узлы объектов не имеют ни имени, ни значения. Мы можем использовать одну и ту же ось для итерации по ресурсам XML, только типы узлов отличаются.
SirixDB предлагает кучу осей, как, например, все оси XPath для навигации по ресурсам XML и JSON. Кроме того, он предоставляет ось Порядка уровней , ось PostOrderAxis, | Вложенную ось для цепной оси и несколько Параллельных осей вариантов для одновременной и параллельной выборки узлов.
В следующем разделе мы покажем, как использовать ось Посетитель-потомок , которая повторяется в предварительном порядке, руководствуясь типами возврата посетителя узла.
6.2. Ось Потомков посетителей
Поскольку очень часто поведение определяется на основе различных типов узлов, SirixDB использует шаблон посетителя .
Мы можем указать посетителя в качестве аргумента конструктора для специальной оси, называемой Ось потомка посетителя . Для каждого типа узла существует эквивалентный метод посещения. Например, для узлов ключа объекта это метод VisitResult visit(Неизменяемый узел узла ключа объекта).
Каждый метод возвращает значение типа Результат посещения . Единственной реализацией интерфейса Visit Result является следующее перечисление:
public enum VisitResultType implements VisitResult { SKIPSIBLINGS, SKIPSUBTREE, CONTINUE, TERMINATE }
Ось Потомок посетителя повторяет древовидную структуру в предварительном порядке. Он использует Тип результата посещения s для руководства обходом:
- ПРОПУСКИ означает, что обход должен продолжаться без посещения правых братьев и сестер текущего узла, на который указывает курсор
- ПРОПУСТИТЬ ПОДДЕРЕВО означает продолжить, не посещая потомков этого узла
- Мы используем CONTINUE , если обход должен продолжаться в предварительном порядке
- Мы также можем использовать TERMINATE для немедленного завершения обхода
Реализация по умолчанию каждого метода в интерфейсе Visitor возвращает VisitResultType.ПРОДОЛЖИТЬ для каждого типа узла. Таким образом, нам нужно только реализовать методы для узлов, которые нас интересуют. Если мы реализовали класс, который реализует интерфейс Visitor под названием My Visitor , мы можем использовать VisitorDescendantAxis следующим образом:
var axis = VisitorDescendantAxis.newBuilder(rtx) .includeSelf() .visitor(new MyVisitor()) .build(); while (axis.hasNext()) axis.next();
Методы в My Visitor вызываются для каждого узла в обходе. Параметр rtx является транзакцией только для чтения. Обход начинается с узла, на который в данный момент указывает курсор.
6.3. Ось Перемещения во времени
Одной из наиболее отличительных особенностей SirixDB является тщательное управление версиями. Таким образом, SirixDB не только предлагает все виды осей для итерации по древовидной структуре в рамках одной ревизии. Мы также можем использовать одну из следующих осей для навигации во времени:
- Первая Ось
- Последняя ось
- Предыдущая ось
- Следующая Ось
- Ось всех времен
- Ось будущего
- Прошлая Ось
Конструкторы принимают в качестве параметров диспетчер ресурсов, а также транзакционный курсор. Курсор перемещается к одному и тому же узлу в каждой ревизии.
Если существует другая ревизия в оси, а также узел в соответствующей ревизии, то ось возвращает новую транзакцию. Возвращаемые значения являются транзакциями только для чтения, открытыми в соответствующих редакциях, в то время как курсор указывает на один и тот же узел в разных редакциях.
Мы покажем простой пример для оси Past :
var axis = new PastAxis(resourceManager, rtx); if (axis.hasNext()) { var trx = axis.next(); // Do something with the transactional cursor. }
6.4. Фильтрация
SirixDB предоставляет несколько фильтров, которые мы можем использовать в сочетании с осью фильтра|/. Следующий код, например, обходит все дочерние узлы узла объекта и фильтрует узлы ключа объекта с ключом “a”, как в {“a”:1, “b”: “foo”} .
new FilterAxis(new ChildAxis(rtx), new JsonNameFilter(rtx, "a"))
Ось Filter необязательно принимает в качестве аргумента несколько фильтров. Фильтр либо является фильтром имен Json для фильтрации имен в ключах объектов , либо одним из фильтров типа узла: Object Filter , |/ObjectRecordFilter , ArrayFilter , StringValueFilter , NumberValueFilter , BooleanValueFilter и NullValueFilter|/.
Ось можно использовать следующим образом для ресурсов JSON для фильтрации по именам ключей объектов с именем “foobar”:
var axis = new VisitorDescendantAxis.Builder(rtx).includeSelf().visitor(myVisitor).build(); var filter = new JsonNameFilter(rtx, "foobar"); for (var filterAxis = new FilterAxis(axis, filter); filterAxis.hasNext();) { filterAxis.next(); }
В качестве альтернативы мы могли бы просто передавать поток по оси (без использования оси Filter вообще), а затем фильтровать по предикату.
rtx имеет тип NodeReadOnlyTrx в следующем примере:
var axis = new PostOrderAxis(rtx); var axisStream = StreamSupport.stream(axis.spliterator(), false); axisStream.filter((unusedNodeKey) -> new JsonNameFilter(rtx, "a")) .forEach((unused) -> /* Do something with the transactional cursor */);
7. Изменение ресурса в базе данных
Очевидно, что мы хотим иметь возможность изменять ресурс. SirixDB сохраняет новый компактный снимок во время каждой фиксации.
После открытия ресурса мы должны запустить единственную транзакцию чтения-записи, как мы видели раньше.
7.1. Простые Операции Обновления
Как только мы перейдем к узлу, который мы хотим изменить, мы сможем обновить, например, имя или значение, в зависимости от типа узла:
if (wtx.isObjectKey()) wtx.setObjectKeyName("foo"); if (wtx.isStringValue()) wtx.setStringValue("foo");
Мы можем вставлять новые записи объектов через вставить запись объекта В качестве Первого дочернего элемента и вставить запись объекта В качестве правого элемента . Аналогичные методы существуют для всех типов узлов. Записи объектов состоят из двух узлов: узла ключа объекта и узла значения объекта.
SirixDB проверяет согласованность и, таким образом, выдает непроверенное SirixUsageException , если вызов метода не разрешен для определенного типа узла.
Записи объектов, например пары ключ/значение, могут быть вставлены в качестве первого дочернего элемента только в том случае, если курсор расположен на узле объекта. Мы вставляем как узел ключа объекта, так и один из других типов узлов в качестве значения с помощью методов insert Object Record AsX .
Мы также можем связать методы обновления в цепочку – в этом примере wtx находится на узле объекта:
wtx.insertObjectRecordAsFirstChild("foo", new StringValue("bar")) .moveToParent().trx() .insertObjectRecordAsRightSibling("baz", new NullValue());
Сначала мы вставляем узел ключа объекта с именем “foo” в качестве первого дочернего элемента узла объекта. Затем узел String Value создается в качестве первого дочернего элемента вновь созданного узла записи объекта.
Курсор перемещается в узел значения после вызова метода. Таким образом, сначала мы должны снова переместить курсор на узел ключа объекта, родительский. Затем мы можем вставить следующий узел ключа объекта и его дочерний узел, узел С нулевым значением в качестве правого родного брата.
7.2. Массовые Вставки
Существуют и более сложные методы массовой вставки, как мы уже видели, когда импортировали данные JSON. SirixDB предоставляет метод для вставки данных JSON в качестве первого дочернего элемента ( insertSubtreeAsFirstChild ) и в качестве правого родного брата ( insertSubtreeAsRightSibling ).
Чтобы вставить новое поддерево на основе строки, мы можем использовать:
var json = "{\"foo\": \"bar\",\"baz\": [0, \"bla\", true, null]}"; wtx.insertSubtreeAsFirstChild(JsonShredder.createStringReader(json));
API JSON в настоящее время не предоставляет возможности копировать поддеревья. Тем не менее, XML API делает это. Мы можем скопировать поддерево из другого XML-ресурса в SirixDB:
wtx.copySubtreeAsRightSibling(rtx);
Здесь узел, на который в данный момент указывает транзакция только для чтения ( rtx ), копируется вместе со своим поддеревом в качестве нового правого родственника узла, на который указывает транзакция чтения-записи ( wtx ).
SirixDB всегда применяет изменения в памяти, а затем сбрасывает их на диск или флэш-накопитель во время фиксации транзакции. Единственное исключение-если кэш в памяти должен удалить некоторые записи во временный файл из-за ограничений памяти.
Мы можем либо commit () , либо rollback() транзакцию. Обратите внимание, что мы можем повторно использовать транзакцию после одного из двух вызовов метода.
SirixDB также применяет некоторые оптимизации под капотом при вызове массовых вставок.
В следующем разделе мы рассмотрим другие возможности запуска транзакции чтения-записи.
7.3. Запустите транзакцию чтения-записи
Как мы уже видели, мы можем начать транзакцию чтения-записи и создать новый моментальный снимок, вызвав метод commit . Однако мы также можем запустить курсор автоматической фиксации транзакций:
resourceManager.beginNodeTrx(TimeUnit.SECONDS, 30); resourceManager.beginNodeTrx(1000); resourceManager.beginNodeTrx(1000, TimeUnit.SECONDS, 30);
Либо мы автоматически вводим каждые 30 секунд, после каждой 1000-й модификации, либо каждые 30 секунд и каждую 1000-ю модификацию.
Мы также можем начать транзакцию чтения-записи, а затем вернуться к предыдущей редакции, которую мы можем зафиксировать как новую редакцию:
resourceManager.beginNodeTrx().revertTo(2).commit();
Все промежуточные версии по-прежнему доступны. После того как мы зафиксировали более одной ревизии, мы можем открыть конкретную ревизию, указав точный номер ревизии или отметку времени:
var rtxOpenedByRevisionNumber = resourceManager.beginNodeReadOnlyTrx(2); var dateTime = LocalDateTime.of(2019, Month.JUNE, 15, 13, 39); var instant = dateTime.atZone(ZoneId.of("Europe/Berlin")).toInstant(); var rtxOpenedByTimestamp = resourceManager.beginNodeReadOnlyTrx(instant);
8. Сравните Изменения
Чтобы вычислить различия между любыми двумя версиями ресурса, однажды сохраненными в SirixDB, мы можем вызвать алгоритм diff:
DiffFactory.invokeJsonDiff( new DiffFactory.Builder( resourceManager, 2, 1, DiffOptimized.HASHED, ImmutableSet.of(observer)));
Первым аргументом для строителя является менеджер ресурсов, который мы уже использовали несколько раз. Следующие два параметра – это изменения для сравнения. Четвертый параметр-это перечисление, которое мы используем, чтобы определить, следует ли SirixDB учитывать хэши для ускорения вычисления различий или нет.
Если узел изменяется из-за операций обновления в SirixDB, все узлы-предки также адаптируют свои хэш-значения. Если хэши и ключи узлов в двух ревизиях идентичны, SirixDB пропускает поддерево во время обхода двух ревизий, поскольку при указании DiffOptimized в поддереве нет изменений.ХЭШИРОВАННЫЙ .
Неизменный набор наблюдателей-это последний аргумент. Наблюдатель должен реализовать следующий интерфейс:
public interface DiffObserver { void diffListener(DiffType diffType, long newNodeKey, long oldNodeKey, DiffDepth depth); void diffDone(); }
Метод diffListener в качестве первого параметра указывает тип различий, встречающихся между двумя узлами в каждой ревизии. Следующие два аргумента являются стабильными уникальными идентификаторами узлов сравниваемых узлов в двух ревизиях. Последний аргумент depth указывает глубину двух узлов, которые только что сравнил SirixDB.
9. Сериализация в JSON
В какой-то момент времени мы хотим сериализовать ресурс JSON в двоичной кодировке SirixDBs обратно в JSON:
var writer = new StringWriter(); var serializer = new JsonSerializer.Builder(resourceManager, writer).build(); serializer.call();
Для сериализации версий 1 и 2:
var serializer = new JsonSerializer.Builder(resourceManager, writer, 1, 2).build(); serializer.call();
И все сохраненные ревизии:
var serializer = new JsonSerializer.Builder(resourceManager, writer, -1).build(); serializer.call();
10. Заключение
Мы видели, как использовать низкоуровневый API транзакционного курсора для управления базами данных и ресурсами JSON в SirixDB. API более высокого уровня скрывают некоторую сложность.
Полный исходный код доступен на GitHub .