BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos ArangoDB: Um banco, uma linguagem e múltiplos data models

ArangoDB: Um banco, uma linguagem e múltiplos data models

ArangoDB é um banco de dados NoSQL e multi-model. Os bancos de dados NoSQL tem quatro tipos: chave-valor, família de coluna, grafos e cada tipo com estrutura de persistência diferente para resolver específicos problemas. Como o gráfico abaixo mostra, existe um equilíbrio entre complexidade de modelo e escalabilidade.

As regras e os comportamentos da aplicação mudaram tanto como negócio quanto por complexidade, assim, algumas vezes é necessário utilizar mais de um tipo de persistência e tipo de banco de dados. Com esse objetivo em mente, mais bancos de dados resultam num número maior de infraestrutura, operações e complexidade, seja do ponto de vista de software seja do ponto de vista de hardware. Os bancos de dados multi-models podem preencher a lacuna desse problema, uma vez que esse tipo de banco de dados consegue agrupar mais de um banco de dados utilizando apenas uma única infraestrutura. O objetivo desse post é falar um pouco mais sobre ArangoDB, um banco de dados open source e multi-model que suporta chave-valor, documento e grafos e como integrar-lho com Java utilizando o Eclipse JNoSQL.

Segundo a própria documentação, o ArangoDB tem algumas facilidades:

  • Consolidação: Como um banco de dados multi-model, o ArangoDB minimiza componentes para se manter, reduzindo a complexidade das tecnologias que uma aplicação utiliza.
  • Performance simplificada: As aplicações crescem e amadurecem ao longo do tempo. Com o ArangoDB, se pode, rapidamente, aumentar a performance. O ArangoDB pode escalar tanto verticalmente como horizontalmente e tem característica elástica, ou seja, o número de nós pode aumentar e diminuir de maneira bastante simples.
  • Redução de complexidade: No mundo de persistência poliglota, o objetivo é sempre utilizar a mesma técnica e ferramenta que o requisito precisa. Isso é fácil de resolver de maneira bastante simples com multi-model.
  • Tolerância a falhas: O ArangoDB tem um eficiente sistema de tolerância a falhas ativado de maneira nativa.
  • Velocy Pack (Vpack): Um JSON binário do qual o ArangoDB utiliza para armazenar os bancos de dados de maneira nativa. Esse JSON é compacto, que suporta diversos tipos de dados como inteiros, Blobs e números flutuantes além de conseguir converter ou ser transformado num JSON normal de maneira bastante simples e evita um grande número de alocação de memória.

ArangoDB Query Language (AQL): uma linguagem, três bancos de dados.

Nos bancos de dados ArangoDB um desenvolvedor armazena os dados como chave-valor, grafos ou documentos e pode acessar as informações utilizando uma única query, graças ao AQL ou ArangoDB Query Language. Essa query é bem semelhante ao SQL com algumas pequenas diferenças, por exemplo, o uso do FILTER ao invés do WHERE. Apesar das diferenças, um desenvolvedor que está familiarizado com SQL, não terá dificuldade em aprender AQL.

SELECT * FROM users;//SQL
FOR user IN users RETURN user;//AQL
SELECT * FROM users WHERE active = 1; //SQL
FOR user IN users FILTER user.active == 1 RETURN user;//AQL

O ArangoDB tem uma instalação bastante simples, porém, nesse post não cobrirá a instalação do banco de dados em todos os sistemas operacionais. Para seguir o texto, será utilizado o docker e para iniciar uma instância dentro do docker, basta utilizar o seguinte comando:

docker run -e ARANGO_NO_AUTH=1 -d --name arangodb-instance -p 8529:8529 -d arangodb/arangodb

Jakarta EE vs ArangoDB

Dentro do mundo Jakarta EE uma das maneiras integrar com o ArangoDB é o utilizando o Eclipse JNoSQL que é um projeto open source que será a primeira especificação Jakarta que como missão traz facilitar a integração do mundo Java com os bancos NoSQL. Para fazer num projeto maven serão necessários adicionar três dependências:

  • arangodb-extension: essa dependência possui as APIs padrões para a chave-valor e documento além de extensões para os usos de recursos específicos do ArangoDB como, por exemplo, o acesso do recurso de AQL.
  • artemis-graph: A camada de mapeamento do Eclipse JNoSQL para os bancos do tipo grafos do qual utiliza o Apache TinkerPop como camada de comunicação.
  • arangodb-tinkerpop-provider: A implementação do Apache TinkerPop para o ArangoDB.
 <dependencies>
        <dependency>
            <groupId>org.jnosql.artemis</groupId>
            <artifactId>artemis-graph</artifactId>
            <version>0.0.8</version>
        </dependency>
       <dependency>
            <groupId>org.jnosql.artemis</groupId>
            <artifactId>aragangodb-extension</artifactId>
            <version>0.0.8</version>
       </dependency>
        <dependency>
            <groupId>org.arangodb</groupId>
            <artifactId>arangodb-tinkerpop-provider</artifactId>
            <version>2.0.2</version>
        </dependency>
    </dependencies>

Além das dependências do Eclipse JNoSQL e do banco de dados, o próximo passo é adicionar uma API CDI 2.0 e uma implementação, no nosso caso, será utilizada uma implementação de referência, Weld.

No próximo passo, será a modelagem e nesse exemplo serão criados três entidades, vale salientar que as anotações são muito semelhantes ao JPA que é confortável para os desenvolvedores que trabalham com tecnologia SQL.

@Entity
public class Hero implements Serializable {
@Id
private String id;
@Column
private String name;
@Column
private String realName;
@Column
private Integer age;
@Column
private Set<String> powers;
//getter and setter
}
@Entity
public class Villain implements Serializable {
@Id
private String id;
@Column
private String name;
//getter and setter
}
@Entity
public class Person {
    @Id
    private String id;
    @Column
    private String name;
    @Column
    private int age;
    @Column
    private String occupation;
    @Column
    private Double salary;
//getter and setter
}

Com as configurações definidas e o banco instalado, o próximo passo é explorar os recursos das APIs. Um ponto importante é que para se utilizar os recursos dentro do mapeamento é importante ensinar para o CDI onde estão as classes que gerenciam as comunicações para que o CDI consiga utilizar, no caso, uma instância de DocumentCollection, BucketManager e Graph para document, chave-valor e grafo respectivamente. Existem diversas maneiras de criar tais instâncias, por exemplo, com arquivo XML, yaml, JSON e programaticamente. No nosso caso, será realizado com um arquivo JSON como mostra abaixo:

[
  {
    "description": "The arangodb document configuration",
    "name": "document",
    "provider": "org.jnosql.diana.arangodb.document.ArangoDBDocumentConfiguration",
    "settings": {
      "couchbase-host-1": "localhost",
      "couchbase-user": "root",
      "couchbase-password": "123456"
    }
  },
  {
    "description": "The arangodb key-value configuration",
    "name": "key-value",
    "provider": "org.jnosql.diana.arangodb.key.ArangoDBKeyValueConfiguration",
    "settings": {
      "couchbase-host-1": "localhost",
      "couchbase-user": "root",
      "couchbase-password": "123456"
    }
  }
]

Com base nessa configuração será possível injetar os gerenciadores de comunicação chave-valor e também do documento apenas utilizando uma anotação: o @ConfigurationUnit e para ensinar ao CDI como produzir uma instância dessa interface, basta criar um método que retorna a instância com a anotação @Produces, como mostra o código abaixo:

@ApplicationScoped
public class ArangoDBProducer {

   private static final String HEROES = "heroes";

   @Inject
   @ConfigurationUnit(name = "document", database = HEROES)
   private DocumentCollectionManager documentManager;

   @Inject
   @ConfigurationUnit(name = "key-value", database = HEROES)
   private BucketManager bucketManager;



   @Produces
   public ArangoDBDocumentCollectionManager getCollectionManager() {
       return (ArangoDBDocumentCollectionManager) documentManager;
   }

   @Produces
   public BucketManager getBucketManager() {
       return bucketManager;
   }

}

A configuração do Grafo requer uma configuração programática definido a partir de um builder. Para esse builder é importante informar o nome do grafo e vertex que serão configurados além dos Edges com as respectivas direções. O foco do artigo não será falar dos conceitos de grafos, porém, já tem um artigo falando desses conceitos de grafos no próprio InfoQ. Tão logo se tenha uma instância do Graph utilizaremos a mesma lógica do método anotado com Produces do CDI.

@ApplicationScoped
public class GraphProducer {

   private Graph graph;


   @PostConstruct
   public void init() {
       ArangoDBConfigurationBuilder builder = new ArangoDBConfigurationBuilder();
       builder.graph("marketing")
               .withVertexCollection("Person")
               .withEdgeCollection("knows")
               .configureEdge("knows", "Person", "Person");
       BaseConfiguration conf = builder.build();
       this.graph = GraphFactory.open(conf);
   }

   @Produces
   @ApplicationScoped
   public Graph getGraph() {
       return graph;
   }

   @Produces
   @ApplicationScoped
   public GraphTraversalSourceSupplier getSupplier() {
       return () -> graph.traversal();
   }

   public void close(@Disposes Graph graph) throws Exception {
       graph.close();
   }
}

Nessa última etapa do codigo, falta a comunicação com o banco de dados em si. A primeira API também será a mais simples, ou seja, o de chave-valor já que ele se baseia na inserção e na recuperação da chave do campo, como a interface java.util.Map.

@ApplicationScoped
public class VillainService {
    @Inject
    private KeyValueTemplate template;
    public void put(Villain villain) {
        template.put(villain);
    }
    public Optional<Villain> get(String name) {
        return template.get(name, Villain.class);
    }
}
public class App3 {
    public static void main(String[] args) {
        try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
            Villain lock = new Villain();
            lock.setId("lock");
            lock.setName("Lock");
            VillainService service = container.select(VillainService.class).get();
            service.put(lock);
            System.out.println(service.get("lock"));
        }
    }
    private App3() {
    }
}

A chave valor tem acesso ao recurso de repositórios, porém, vale salientar que como ele busca e insere apenas pela chave, de uma maneira geral, ele só terá suporte aos métodos já existentes na interface Repository.

public interface VillainRepository extends Repository<Villain, String> {
}
public class App4 {
    public static void main(String[] args) {
        try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
            Villain lock = new Villain();
            lock.setId("lock");
            lock.setName("Lock");
            Villain doom = new Villain();
            doom.setId("doom");
            doom.setName("Dc Doom");
            VillainRepository repository = container.select(VillainRepository.class, DatabaseQualifier.ofKeyValue()).get();
            repository.save(lock);
            repository.save(doom);
            System.out.println(repository.findById("lock"));
            System.out.println(repository.findById("doom"));
        }
    }
    private App4() {
    }
}

Com a API de documentos as buscas são facilitadas, uma vez que com ela é possível realizar queries a partir de outros campos que não sejam a chave ou o identificador único. No código, foi criado uma instância de hérois e foi realizado uma busca pelo nome. Um ponto importante é que, assim como a chave-valor, ele tem acesso aos recursos das interfaces de repositórios, porém, criar os recursos de “method by query” que se baseiam em criar queries nas interfaces repositórios, baseado numa convenção, e o próprio JNoSQL irá implementá-lo. Por exemplo, para buscar pelo nome basta criar um método “findByName” e o parâmetro do tipo String.

public class App {
    public static void main(String[] args) {
        try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
            Hero ironMan = Hero.builder().withRealName("Tony Stark").withName("iron_man")
                    .withAge(34).withPowers(Collections.singleton("rich")).build();
            DocumentTemplate template = container.select(DocumentTemplate.class).get();
            template.insert(ironMan);
            DocumentQuery query = select().from("Hero").where("name").eq("iron_man").build();
            List<Hero> heroes = template.select(query);
            System.out.println(heroes);
        }
    }
    private App() {
    }
}

A última API a ser exibida será a de grafos, com ela é possível, por exemplo, criar relações de maneiras muito mais complexas que os próprios bancos relacionais. Por exemplo, dentro do grafo existe um objeto que armazena uma relação e essa relação possui sentidos e propriedades e o seu maior case, certamente, é com os sistemas de recomendações. No exemplo abaixo, dado um número de desenvolvedores será necessário buscar os profissionais de computação a partir dos seguintes requisitos:

  • Desenvolvedores com salário maior que 3000 e idade entre 20 e 25 anos
  • Pessoas que conhecem desenvolvedores
  • Pessoas que conheçam os desenvolvedores e que tenham um sentimento de amor
public final class MarketingApp {


    private MarketingApp() {
    }


    public static void main(String[] args) {

        try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
            GraphTemplate graph = container.select(GraphTemplate.class).get();

            Person banner = graph.insert(builder().withAge(30).withName("Banner")
                    .withOccupation("Developer").withSalary(3_000D).build());

            Person natalia = graph.insert(builder().withAge(32).withName("Natalia")
                    .withOccupation("Developer").withSalary(5_000D).build());

            Person rose = graph.insert(builder().withAge(40).withName("Rose")
                    .withOccupation("Design").withSalary(1_000D).build());

            Person tony = graph.insert(builder().withAge(22).withName("tony")
                    .withOccupation("Developer").withSalary(4_500D).build());


            graph.edge(tony, "knows", rose).add("feel", "love");
            graph.edge(tony, "knows", natalia);

            graph.edge(natalia, "knows", rose);
            graph.edge(banner, "knows", rose);

            List<Person> developers = graph.getTraversalVertex()
                    .has("salary", gte(3_000D))
                    .has("age", between(20, 25))
                    .has("occupation", "Developer")
                    .<Person>stream().collect(toList());

            List<Person> peopleWhoDeveloperKnows = graph.getTraversalVertex()
                    .has("salary", gte(3_000D))
                    .has("age", between(20, 25))
                    .has("occupation", "Developer")
                    .out("knows")
                    .<Person>stream().collect(toList());

            List<Person> both = graph.getTraversalVertex()
                    .has("salary", gte(3_000D))
                    .has("age", between(20, 25))
                    .has("occupation", "Developer")
                    .outE("knows")
                    .bothV()
                    .<Person>stream()
                    .distinct()
                    .collect(toList());

            List<Person> couple = graph.getTraversalVertex()
                    .has("salary", gte(3_000D))
                    .has("age", between(20, 25))
                    .has("occupation", "Developer")
                    .outE("knows")
                    .has("feel", "love")
                    .bothV()
                    .<Person>stream()
                    .distinct()
                    .collect(toList());

            System.out.println("Developers has salary greater than 3000 and age between 20 and 25: " + developers);
            System.out.println("Person who the Developers target know: " + peopleWhoDeveloperKnows);
            System.out.println("The person and the developers target: " + both);
            System.out.println("Developers to Valentine days: " + couple);

        }
    }

}

Ao explicar todas as APIs em geral, é importante falar que o Eclipse JNoSQL também se preocupa com recursos específicos de cada banco de dados, no nosso caso, o ArangoDB. Essa extensão tem acesso aos famosos AQL tanto nas classes templates quanto nas interfaces do tipo repositório.

public class App1 {
    public static void main(String[] args) {
        try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
            Hero ironMan = Hero.builder().withRealName("Tony Stark").withName("iron_man")
                    .withAge(34).withPowers(Collections.singleton("rich")).build();
            ArangoDBTemplate template = container.select(ArangoDBTemplate.class).get();
            template.insert(ironMan);
            DocumentQuery query = select().from("Hero").where("_key").eq("iron_man").build();
            List<Hero> heroes = template.select(query);
            List<Hero> aql = template.aql("FOR h IN Hero FILTER  h.name == @id RETURN h", Collections.singletonMap("id", "iron_man"));
            System.out.println(heroes);
            System.out.println(aql);
        }
    }
    private App1() {
    }
}
public class App2 {
    public static void main(String[] args) {
        try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
            Hero ironMan = Hero.builder().withRealName("Tony Stark").withName("iron_man")
                    .withAge(34).withPowers(Collections.singleton("rich")).build();
            HeroRepository repository = container.select(HeroRepository.class).get();
            repository.save(ironMan);
            System.out.println(repository.findByName("iron_man"));
            System.out.println(repository.find("Tony Stark"));
        }
    }
    private App2() {
    }
}
public interface HeroRepository extends ArangoDBRepository<Hero, String> {
    List<Hero> findByName(String name);
    @AQL("select * from heroes where realName= @status")
    List<Hero> find(@Param("realName") String realName);
}

Os bancos do tipo multi-model apresentam uma excelente oportunidade de aumentar a complexidade das estruturas utilizadas dentro do uma aplicação sem que para isso seja necessário aumentar a complexidade de operações com mais instâncias de bancos de dados. Um ponto importante é que ele possui uma rica interface gráfica de maneira nativa, ou seja, é possível ver a saúde do nó, executar queries sem que para isso se instale um novo software. Ou seja, é um banco de dados que deixará feliz tanto o desenvolvedor quanto o time de operações.

Códigos exemplos: https://github.com/JNOSQL/artemis-demo

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT