Um guia para o SirixDB
1. Visão geral
Neste tutorial, daremos uma visão geral do que éSirixDB e seus objetivos de design mais importantes.
A seguir, daremos um passeio por uma API transacional baseada em cursor de baixo nível.
2. Recursos do SirixDB
SirixDB é um armazenamento de documentos deNoSQL temporal estruturado em log, que armazena dados evolutivos. Ele nunca substitui nenhum dado no disco. Assim, podemos restaurar e consultar o histórico de revisão completo de um recurso no banco de dados de forma eficiente. O SirixDB garante que um mínimo de sobrecarga de armazenamento seja criado para cada nova revisão.
Atualmente, o SirixDB oferece dois modelos de dados nativos integrados, a saber, um repositório XML binário e um repositório JSON.
2.1. Objetivos do projeto
Alguns dos princípios fundamentais e objetivos de design mais importantes são:
-
Concurrency - SirixDB contém muito poucos bloqueios e visa ser o mais adequado para sistemas multithread quanto possível
-
Asynchronous REST API – operações podem acontecer independentemente; cada transação é vinculada a uma revisão específica e apenas uma transação de leitura e gravação em um recurso é permitida simultaneamente para N transações somente leitura
-
Versioning/Revision history – SirixDB armazena um histórico de revisão de cada recurso no banco de dados, enquanto mantém a sobrecarga de armazenamento a um mínimo. O desempenho de leitura e gravação é ajustável. Depende do tipo de versão, que podemos especificar para criar um recurso
-
Data integrity – SirixDB, como ZFS, armazena checksums completos das páginas nas páginas pai. Isso significa que quase toda a corrupção de dados pode ser detectada após a leitura no futuro, pois os desenvolvedores do SirixDB pretendem particionar e replicar bancos de dados no futuro.
-
Copy-on-write semantics – semelhante aos sistemas de arquivos Btrfs e ZFS, o SirixDB usa a semântica CoW, o que significa que o SirixDB nunca sobrescreve os dados. Em vez disso, os fragmentos de página do banco de dados são copiados e gravados em um novo local.
-
Per revision and per record versioning – SirixDB não faz versões apenas por página, mas também por registro. Assim, sempre que alteramos uma fração potencialmente pequena de registros em uma página de dados, não é necessário copiar a página inteira e gravá-la em um novo local em um disco ou unidade flash. Em vez disso, podemos especificar uma das várias estratégias de versão conhecidas de sistemas de backup ou um algoritmo de captura instantânea deslizante durante a criação de um recurso de banco de dados. O tipo de versão que especificamos é usado pelo SirixDB para versão das páginas de dados
-
Guaranteed atomicity (without a WAL) – o sistema nunca entrará em um estado inconsistente (a menos que haja falha de hardware), o que significa que um desligamento inesperado nunca danificará o sistema. Isso é realizado sem a sobrecarga de um log de gravação antecipada (WAL)
-
Os lotes do SirixDB doLog-structured and SSD friendly – gravam e sincronizam tudo sequencialmente em uma unidade flash durante os commits. Ele nunca substitui dados confirmados
Primeiro, queremos apresentar a API de baixo nível exemplificada com dados JSON antes de mudar nosso foco para níveis mais altos em artigos futuros. Por exemplo, uma API XQuery para consultar bancos de dados XML e JSON ou uma API RESTful temporal assíncrona. Basicamente, podemos usar a mesma API de baixo nível com diferenças sutis para armazenar, percorrer e comparar recursos XML também.
Para usar SirixDB,we at least have to use Java 11.
3. Dependência Maven para incorporar SirixDB
Para seguir os exemplos, primeiro temos que incluirthe sirix-core dependency, por exemplo, via Maven:
io.sirix
sirix-core
0.9.3
Ou via Gradle:
dependencies {
compile 'io.sirix:sirix-core:0.9.3'
}
4. Codificação em árvore no SirixDB
Um nó no SirixDB faz referência a outros nós por uma codificaçãofirstChild/leftSibling/rightSibling/parentNodeKey/nodeKey:
Os números na figura são IDs de nó estáveis e únicos gerados automaticamente, gerados com um simples gerador de números seqüenciais.
Cada nó pode ter um primeiro filho, um irmão esquerdo, um irmão direito e um nó pai. Além disso, o SirixDB pode armazenar o número de filhos, o número de descendentes e hashes de cada nó.
Nas seções a seguir, apresentaremos a principal API JSON de baixo nível do SirixDB.
5. Crie um banco de dados com um único recurso
Primeiro, queremos mostrar como criar um banco de dados com um único recurso. O recurso será importado de um arquivo JSON e armazenado persistentemente no formato binário interno do 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();
}
}
Primeiro, criamos um banco de dados. Em seguida, abrimos o banco de dados e criamos o primeiro recurso. Existem várias opções para criar um recurso (see the official documentation).
Em seguida, abrimosa single read-write transaction on the resource para importar o arquivo JSON. A transação fornece um cursor para navegação pelos métodosmoveToX. Além disso, a transação fornece métodos para inserir, excluir ou modificar nós. Observe que a API XML fornece atémethods for moving nodes in a resource and copying nodes de outros recursos XML.
Para fechar adequadamente a transação aberta de leitura e gravação, o gerenciador de recursos e o banco de dados usamos a instruçãotry-with-resources do Java.
Nós exemplificamos a criação de um banco de dados e recurso em dados JSON, mas a criação de um banco de dados e recurso XML é quase idêntica.
Na próxima seção, vamos abrir um recurso em um banco de dados e mostrar os eixos e métodos de navegação.
6. Abra um recurso em um banco de dados e navegue
6.1. Navegação de pré-encomenda em um recurso JSON
Para navegar pela estrutura em árvore, podemos reutilizar a transação de leitura e gravação após a confirmação. No código a seguir, no entanto, abriremos o recurso novamente e iniciaremos uma transação somente leitura na revisão mais recente:
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:
}
});
}
Usamos o eixo descendente para iterar sobre todos os nós na pré-encomenda (profundidade primeiro). Hashes de nós são criados de baixo para cima para todos os nós por padrão, dependendo da configuração do recurso.
Os nós da matriz e do objeto não têm nome nem valor. Podemos usar o mesmo eixo para iterar através dos recursos XML, apenas os tipos de nós diferem.
SirixDB oferecea bunch of axes as for instance all XPath-axes para navegar por recursos XML e JSON. Além disso, ele forneceLevelOrderAxis, aPostOrderAxis, aNestedAxis para o eixo da cadeia e várias variantesConcurrentAxis para buscar nós simultaneamente e em paralelo.
Na próxima seção, mostraremos como usar oVisitorDescendantAxis, que itera na pré-encomenda, guiado pelos tipos de retorno de um visitante do nó.
6.2. Eixo descendente do visitante
Como é muito comum definir o comportamento com base nos diferentes tipos de nós, o SirixDB usathe visitor pattern.
Podemos especificar um visitante como um argumento de construtor para um eixo especial chamadoVisitorDescendantAxis.. Para cada tipo de nó, há um método de visita equivalente. Por exemplo, para nós-chave de objeto, é o métodoVisitResult visit(ImmutableObjectKeyNode node).
Cada método retorna um valor do tipoVisitResult. A única implementação da interfaceVisitResult é o seguinte enum:
public enum VisitResultType implements VisitResult {
SKIPSIBLINGS,
SKIPSUBTREE,
CONTINUE,
TERMINATE
}
OVisitorDescendantAxis itera através da estrutura em árvore na pré-encomenda. Ele usaVisitResultTypes para guiar a travessia:
-
SKIPSIBLINGS significa que a travessia deve continuar sem visitar os irmãos certos do nó atual para o qual o cursor aponta
-
SKIPSUBTREE significa continuar sem visitar os descendentes deste nó
-
UsamosCONTINUE se a travessia deve continuar na pré-ordem
-
Também podemos usarTERMINATE para encerrar a travessia imediatamente
A implementação padrão de cada método na interfaceVisitor retornaVisitResultType.CONTINUE para cada tipo de nó. Portanto, precisamos implementar apenas os métodos para os nós nos quais estamos interessados. Se implementamos uma classe que implementa a interfaceVisitor chamadaMyVisitor, podemos usarVisitorDescendantAxis da seguinte maneira:
var axis = VisitorDescendantAxis.newBuilder(rtx)
.includeSelf()
.visitor(new MyVisitor())
.build();
while (axis.hasNext()) axis.next();
Os métodos emMyVisitor são chamados para cada nó na travessia. O parâmetrortx é uma transação somente leitura. A travessia começa com o nó para o qual o cursor aponta atualmente.
6.3. Eixo de viagem no tempo
Um dos recursos mais distintos do SirixDB é a versão completa. Assim, o SirixDB não apenas oferece todos os tipos de eixos para percorrer a estrutura da árvore em uma única revisão. Também podemos usar um dos seguintes eixos para navegar no tempo:
-
FirstAxis
-
LastAxis
-
PreviousAxis
-
NextAxis
-
AllTimeAxis
-
FutureAxis
-
PastAxis
Os construtores usam um gerenciador de recursos e um cursor transacional como parâmetros. O cursor navega para o mesmo nó em cada revisão.
Se outra revisão no eixo - assim como o nó na respectiva revisão - existir, o eixo retornará uma nova transação. Os valores retornados são transações somente leitura abertas nas respectivas revisões, enquanto o cursor aponta para o mesmo nó nas diferentes revisões.
Mostraremos um exemplo simples paraPastAxis:
var axis = new PastAxis(resourceManager, rtx);
if (axis.hasNext()) {
var trx = axis.next();
// Do something with the transactional cursor.
}
6.4. Filtragem
SirixDB fornece vários filtros, que podemos usar em conjunto com umFilterAxis. O código a seguir, por exemplo, percorre todos os filhos de um nó de objeto e filtra os nós de chave de objeto com a chave “a” como em\{“a”:1, “b”: “foo”}.
new FilterAxis(new ChildAxis(rtx), new JsonNameFilter(rtx, "a"))
OFilterAxis opcionalmente leva mais de um filtro como argumento. O filtro é umJsonNameFilter, para filtrar por nomes em chaves de objeto ou um dos filtros de tipo de nó:ObjectFilter,ObjectRecordFilter,ArrayFilter,StringValueFilter, NumberValueFilter,BooleanValueFilter eNullValueFilter.
O eixo pode ser usado da seguinte maneira para os recursos JSON filtrarem por nomes de chave de objeto com o nome "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();
}
Como alternativa, poderíamos simplesmente transmitir sobre o eixo (sem usarFilterAxis) e, em seguida, filtrar por um predicado.
rtx é do tipoNodeReadOnlyTrx no seguinte exemplo:
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. Modificar um recurso em um banco de dados
Obviamente, queremos poder modificar um recurso. O SirixDB armazena um novo instantâneo compacto durante cada confirmação.
Depois de abrir um recurso, temos que iniciar a única transação de leitura e gravação, como vimos antes.
7.1. Operações simples de atualização
Depois de navegar para o nó que queremos modificar, podemos atualizar, por exemplo, o nome ou o valor, dependendo do tipo de nó:
if (wtx.isObjectKey()) wtx.setObjectKeyName("foo");
if (wtx.isStringValue()) wtx.setStringValue("foo");
Podemos inserir novos registros de objeto viainsertObjectRecordAsFirstChildeinsertObjectRecordAsRightSibling. Similar methods exist for all node types. Os registros de objeto são compostos de dois nós: um nó de chave de objeto e um nó de valor de objeto.
O SirixDB verifica a consistência e, como tal, lança umSirixUsageException não verificado se uma chamada de método não for permitida em um tipo de nó específico.
Os registros de objetos, que são pares de chave / valor, por exemplo, só podem ser inseridos como um primeiro filho se o cursor estiver localizado em um nó de objeto. Nós inserimos um nó-chave de objeto, bem como um dos outros tipos de nós como o valor com os métodosinsertObjectRecordAsX.
Também podemos encadear os métodos de atualização - para este exemplo,wtx está localizado em um nó de objeto:
wtx.insertObjectRecordAsFirstChild("foo", new StringValue("bar"))
.moveToParent().trx()
.insertObjectRecordAsRightSibling("baz", new NullValue());
Primeiro, inserimos um nó-chave de objeto com o nome "foo" como o primeiro filho de um nó de objeto. Em seguida, umStringValueNode é criado como o primeiro filho do nó de registro do objeto recém-criado.
O cursor é movido para o nó do valor após a chamada do método. Portanto, primeiro precisamos mover o cursor para o nó-chave do objeto, o pai novamente. Então, podemos inserir o próximo nó-chave do objeto e seu filho, aNullValueNode como irmão direito.
7.2. Inserções em massa
Também existem métodos de inserção em massa mais sofisticados, como já vimos quando importamos os dados JSON. SirixDB fornece um método para inserir dados JSON como primeiro filho (insertSubtreeAsFirstChild) e como irmão direito (insertSubtreeAsRightSibling).
Para inserir uma nova subárvore com base em uma String, podemos usar:
var json = "{\"foo\": \"bar\",\"baz\": [0, \"bla\", true, null]}";
wtx.insertSubtreeAsFirstChild(JsonShredder.createStringReader(json));
A API JSON atualmente não oferece a possibilidade de copiar subárvores. No entanto, a API XML sim. Podemos copiar uma subárvore de outro recurso XML no SirixDB:
wtx.copySubtreeAsRightSibling(rtx);
Aqui, o nó para o qual a transação somente leitura (rtx) aponta atualmente é copiado com sua subárvore como um novo irmão direito do nó para o qual a transação de leitura e gravação (wtx) aponta.
SirixDB sempreapplies changes in-memory and then flushes them to a disk ou a unidade flash durante a confirmação de uma transação. A única exceção é se o cache da memória precisar remover algumas entradas em um arquivo temporário devido a restrições de memória.
Podemoscommit() ourollback() a transação. Observe que podemos reutilizar a transação após uma das duas chamadas de método.
O SirixDB também aplica algumas otimizações sob o capô ao invocar inserções em massa.
Na próxima seção, veremos outras possibilidades sobre como iniciar uma transação de leitura e gravação.
7.3. Iniciar uma transação de leitura e gravação
Como vimos, podemos começar uma transação de leitura e gravação e criar um novo instantâneo chamando o métodocommit. No entanto, também podemos iniciar um cursor transacional de confirmação automática:
resourceManager.beginNodeTrx(TimeUnit.SECONDS, 30);
resourceManager.beginNodeTrx(1000);
resourceManager.beginNodeTrx(1000, TimeUnit.SECONDS, 30);
Confirmamos automaticamente a cada 30 segundos, após cada milésima modificação ou a cada 30 segundos e a cada milésima modificação.
Também podemos iniciar uma transação de leitura e gravação e depois reverter para uma revisão anterior, que podemos confirmar como uma nova revisão:
resourceManager.beginNodeTrx().revertTo(2).commit();
Todas as revisões intermediárias ainda estão disponíveis. Depois de confirmarmos mais de uma revisão, podemos abrir uma revisão específica, especificando o número exato da revisão ou com um carimbo de data / hora:
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. Compare Revisões
Para calcular as diferenças entre as duas revisões de um recurso, uma vez armazenadas no SirixDB, podemos invocar um algoritmo diff:
DiffFactory.invokeJsonDiff(
new DiffFactory.Builder(
resourceManager,
2,
1,
DiffOptimized.HASHED,
ImmutableSet.of(observer)));
O primeiro argumento para o construtor é o gerenciador de recursos, que já usamos várias vezes. Os próximos dois parâmetros são as revisões a serem comparadas. O quarto parâmetro é um enum, que usamos para determinar se o SirixDB deve levar em consideração os hashes para acelerar ou não a computação diferencial.
Se um nó for alterado devido a operações de atualização no SirixDB, todos os nós ancestrais também adaptarão seus valores de hash. Se os hashes e as chaves de nó nas duas revisões forem idênticos, o SirixDB pula a subárvore durante o percurso das duas revisões, porque não há mudanças na subárvore quando especificamosDiffOptimized.HASHED.
An immutable set of observers is the last argument. Um observador deve implementar a seguinte interface:
public interface DiffObserver {
void diffListener(DiffType diffType, long newNodeKey, long oldNodeKey, DiffDepth depth);
void diffDone();
}
O métododiffListener como o primeiro parâmetro especifica o tipo de diff encontrado entre dois nós em cada revisão. Os próximos dois argumentos são os identificadores estáveis de nós únicos dos nós comparados nas duas revisões. O último argumentodepth especifica a profundidade dos dois nós, que o SirixDB acabou de comparar.
9. Serializar para JSON
Em algum momento, queremos serializar um recurso JSON na codificação binária do SirixDBs de volta ao JSON:
var writer = new StringWriter();
var serializer = new JsonSerializer.Builder(resourceManager, writer).build();
serializer.call();
Para serializar as revisões 1 e 2:
var serializer = new
JsonSerializer.Builder(resourceManager, writer, 1, 2).build();
serializer.call();
E todas as revisões armazenadas:
var serializer = new
JsonSerializer.Builder(resourceManager, writer, -1).build();
serializer.call();
10. Conclusão
Vimos como usar a API do cursor transacional de baixo nível para gerenciar bancos de dados e recursos JSON no SirixDB. APIs de nível superior ocultam parte da complexidade.
O código-fonte completo está disponível emGitHub.