BT

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

Contribuir

Tópicos

Escolha a região

Início Artigos Armadilhas de design NoSQL com Java

Armadilhas de design NoSQL com Java

Pontos Principais

 

Ref: Jacob Peter Gowy's The Flight of Icarus (1635-1637)

Banco de dados não relacional é um assunto mencionado quando falamos sobre uma nova modelagem ou persistência poliglota. Porém, quais são seus impactos nessa adoção? O objetivo desse artigo é cobrir os primeiros passos de como utilizar este tipo de banco de dados dentro de uma arquitetura corporativa.

Existem diversos artigos que abordam o que é um banco de dados não relacional, os tipos, etc. Porém, nesse artigo vou começar abordando o que não é NoSQL:

  • No-Security: Sim, indiferente da base de dados selecionada, a segurança ainda é um assunto importante. Ou seja, é crítico que as instâncias de bancos de dados não estejam expostas publicamente, além de que, utilizar recursos de usuário e senha do banco de dados é sempre importante. Além da validação no banco de dados o nível de software precisa estar protegido, ou seja, a senha não deve ser armazenada diretamente no código. No mundo ideal, o desenvolvedor não deve saber o usuário e senha do banco de dados de produção, tudo isso graças ao terceiro fator do The Twelve Factor App. Esse tópico não é novo, porém, vale mencionar uma vez que existem grandes problemas relacionado a esse tópico;
  • No-Data-Integrity: O recurso que existe na grande maioria dos bancos de dados não relacionais, certamente, é o schemaless, porém, isso não quer dizer que a integridade dos dados seja um ponto exclusivo para os bancos relacionais. É muito importante que todos dados sejam consistentes para o negócio e que sejam validados nesse nível. A convenção também é algo crítico, por exemplo, se existir um campo com o nome quantidade uma vez que a primeira inserção foi feita como um número não flutuante é necessário que as próximas inserções e atualizações sigam a mesma linha.Nos campos também existem um grau de complexidade que irá além de uma tipagem, por exemplo, o email ou o documento de identidade. Para isso, é possível que se tenha um tipo;
  • No-responsibility: Ao escolher uma nova camada física dentro da sua arquitetura é muito importante entender que ele demandará diversas atividades sem falar em um novo ponto de complexidade, que por consequência, um novo ponto de risco para o negócio. É extremamente importante entender que essa opção terá custos e eles precisam ser calculados e analisados antes de serem adicionados no escopo arquitetural. Por que ambiente de produção não é um laboratório e alguém precisa ser responsável por isso;
  • No-mistakes: Certamente você já participou de uma discussão e já ouviu: Escolhi essa solução porque ela me da escalabilidade. Na minha opinião, atualmente, escalabilidade se tornou uma palavra silver-bullets, ou seja, é a justificativa para fazer os maiores erros na aplicação. É importante entender que as soluções relacionais ainda estão vivas e ainda são bastante importantes, isso quer dizer que em algumas soluçõe o uso de NoSQL poderá ser um grande erro. Não existem soluções a prova de erros ou de más escolhas e o NoSQL é uma delas. Vale salientar que o termo "escalabilidade" é muito amplo e diversos pontos de análises, assim, é necessário entender os prós e contras, a vantagem é na escrita ou na leitura, verticalmente ou horizontalmente? Infelizmente, a grande maioria das pessoas que utilizam esse termo não saberão essas respostas. Por último, como diria o Donald Knuth a otimização prematura é a raiz do mal;
  • No-trade-off: O NoSQL não é a prova de falhas, além disso, ele também não é a prova de trade-offs, ou seja, ele terá vantagens e desvantagens e o teorema do CAP te lembrará isso em todos os momentos. É importante conhecer o banco da escolha uma vez que o seu desenho impactará no comportamento esperado. Muitas dessas escolhas são baseados em benchmarks, porém, verifique os detalhes deste testes e como eles foram feitos, no geral, um vendor não fará um teste que mostra que o concorrente é superior.

O mundo de persistência poliglota podem trazer diversos benefícios para uma solução, porém, tem que levar em consideração que a modelagem é chave para que isso aconteça. O erro de modelagem pode condenar a sua aplicação em performance e trabalho, afinal, uma vez que os dados estejam lá, toda e qualquer modificação terá que se levar em consideração o processo de migração dos dados para um novo formato. O que chegamos num comum acordo é que de forma geral os bancos de dados não relacionais, ou NoSQL, trabalham de maneira diferente do relacional, ou seja, ao invés de se trabalhar com as normalizações realizamos as queries com base em como queremos que as informações serão retornadas, então a modelagem é totalmente acoplável ao negócio, volumetria e o contexto que ela será inserida.

Modelagem com Documentos (MongoDB)

Para falar de modelagem, começaremos falando de NoSQL do tipo documento. Existem diversas dicas sobre as melhores modelagem, porém, as minha favoritas são a do Oren Eini em uma palestra brilhante realizada em 2017 e do Elemar Júnior em seu blog, no geral, ele menciona que para realizar uma modelagem é importante levar em consideração três características muito simples caso já esteja familiarizado com o DDD.

  • Coerência: é a capacidade de um documento ser compreensível e de maneira única. Numa analogia com o relacional seria como trazer as informações sem que seja necessário realizar os joins;
  • Independência: É um documento que faz sentido existir mesmo que sozinho ou possui razão própria para existência;
  • Isolamento: A modificação de um dado não deve implicar na alteração de outro banco.

Para colocar isso em prática temos o seguinte modelagem, o exemplo que citaremos é o controle de pedido dentro de um e-commerce. Utilizaremos o Jakarta NoSQL, porém, isso é aplicável a qualquer framework de mapeamento como o Spring Data.

@Entity
public class Order {
  @Id
  private ObjectId id;

  @Column
  private LocalDateTime orderedAt;

  @Column
  private List<Product> items;

  @Column
  private Address shipTo;
}

@Entity
public class Product {
  @Column
  private String name;

  @Column
  @Convert(MoneyConverter.class)
  private MonetaryAmount value;
}

@Entity
public class Address {
  @Column
  private String city;

  @Column
  private String country;

  @Column
  private String postalCode;
}

Com a modelagem pronta, podemos utilizá-la na aplicação:

public class App {
  public static void main(String[] args) {
    try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
      DocumentTemplate template = container.select(DocumentTemplate.class).get();

      Address address = Address.builder()
        .withCity("Salvador")
        .withCountry("Brazil")
        .withPostalCode("40235220")
        .build();

      CurrencyUnit dollar = Monetary.getCurrency(Locale.US);
      Order order = new Order(address);
      order.add(new Product("Pen", Money.of(1, dollar)));
      order.add(new Product("Notebook", Money.of(1_000, dollar)));
      order.add(new Product("Smartphone", Money.of(1_500, dollar)));

      template.insert(order);

      System.out.println(order);
    }
  }
}

É bem interessante reparar que o pedido (Order) possui o endereço do usuário e isso tende a duplicar as informações. Porém, o que acontece se o usuário alterar o endereço? Posteriormente? Simplesmente, não precisa modificar, afinal, se ele fez um pedido em 2019 com um endereço o fato dele ter um novo endereço em 2020 tende a não impactar o pedido realizado mesmo que ele tenha feito diversos pedidos em 2019. Vale lembrar o velho clichê quando falamos no banco de dados NoSQL: A desnormalização é a sua melhor amiga.

A conclusão na modelagem para os bancos de dados de documento é que é possível fazer-lo por meio do conceito de DDD.

Existe uma grande discussão sobre transações no MongoDB, porque é possível utilizar-lo no lugar de um banco de dados relacional, porém, é importante entender que a própria documentação deixa claro que esse recurso gera um alto custo na escrita em cluster, reafirmando que caso utilize muito esse recurso ou senão modelou o banco de dados corretamente, esse tipo de banco não é para o seu caso de uso.

Modelagem com Família de Colunas (Cassandra)

Saindo de um banco de dados do tipo documento e indo para um banco de dados do tipo família de colunas, no caso o Cassandra. É importante, primeiramente, entender o Cassandra e as possibilidades de queries mais limitadas que um banco de dados documentos e um relacional. Entender o Cassandra é importante, principalmente, para evitar os erros mais clássicos: load balancer e achar que o funcionamento de chave composto no Cassandra é idêntico ao relacional.

Uma vez compreendido o funcionamento básico do Cassandra, podemos partir para a modelagem. Dentro do Cassandra, existem dois objetivos: espalhar os dados entre os clusters e ser um sniper nas buscas, ou seja, uma query que retorna tudo que se necessite no negócio. Repare que essas duas regras requerem um certo equilíbrio, afinal, colocar todas as informações em um único cluster quebraria a primeira regra que é justamente espalhar os dados entre os clusters.

Em termo de recursos de buscas é importante compreender que as queries tendem a funcionar na seguinte ordem:

Para facilitar a sua modelagem dentro do Cassandra com base na modelagem orientada a queries, o Cassandra tem acesso de alguns tipos especiais:

  • Set: Semelhante ao Set do Java, que permite a inserção de um único elemento;
  • List: Semelhante ao List do Java, é possível colocar um elemento não único dentro da lista. Uma vez que a ordem é importante a lista acaba tendo o efeito read-before-write, assim, sempre que possível utilize o Set;
  • Map: Semelhante ao Map dentro no mundo Java ou um dicionário de dados.
  • UDT: Penso como um tipo customizável dentro do Cassandra.

Para ilustrar a modelagem com o Cassandra, será exemplificado em três cenários:

O primeiro é uma simples relação de contatos, no qual será salvo o nome, data de aniversário e os detalhes do contato em que seria uma relação do tipo e a sua informação.

Vale lembrar que o Cassandra precisa executar a query de criação da família de coluna primeiro antes de inserir os dados.

Para esse caso teremos a seguinte query de criação:

CREATE KEYSPACE IF NOT EXISTS samples WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};
CREATE TYPE IF NOT EXISTS samples.address(city text, country text, postalCode text);
CREATE COLUMNFAMILY IF NOT EXISTS samples.Contact ("_id" text PRIMARY KEY, birthday date, details map<text, text>, address address);

Dentro da modelagem teremos:

@Entity
class Contact {
  @Id
  private String name;

  @Column
  private LocalDate birthday;

  @Column
  private Map<String, String> details;

  @UDT("address")
  @Column
  private Address address;
}

@Entity
class Address {
  @Column
  private String city;

  @Column
  private String country;

  @Column("postalcode")
  private String postalCode;
}

Assim, é possível ver o código funcionando, novamente, no modo Java SE:

import jakarta.nosql.column.ColumnQuery;
import org.eclipse.jnosql.artemis.cassandra.column.CassandraTemplate;
import javax.enterprise.inject.se.SeContainer;
import javax.enterprise.inject.se.SeContainerInitializer;
import java.time.LocalDate;
import static jakarta.nosql.column.ColumnQuery.select;
import static java.time.Month.MARCH;

public class ContactApp {
  public static void main(String[] args) {
    try(SeContainer container = SeContainerInitializer.newInstance().initialize()) {
      CassandraTemplate template = container.select(CassandraTemplate.class).get();

      Address address = Address.builder()
        .withCity("Sao Paulo")
        .withCountry("Brazil")
        .withPostalCode("01312001")
        .build();

      Contact contact = Contact.builder()
        .withAddress(address)
        .withBirthday(LocalDate.of(1992, MARCH, 27))
        .withName("Poliana").build();

      contact.put("twitter", "twitter");
      contact.put("phone", "123456789");
      contact.put("facebook", "poliana_facebook");

      template.insert(contact);

      System.out.println(contact);

      ColumnQuery query = select().from("Contact").where("_id").eq("Poliana").build();
      template.select(query).forEach(System.out::println);
    }
  }
}

Um outro exemplo, é o cadastro do carro, no qual temos informações como placa, cidade, cor, além das informações do dono. Levando em consideração que a busca do carro retorna o dono, é possível utilizar o UDT para representar o dono do carro, por exemplo.

Para esse caso, a query de criação:

CREATE KEYSPACE IF NOT EXISTS samples WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};
CREATE TYPE IF NOT EXISTS samples.owner(name text, license text);
CREATE COLUMNFAMILY IF NOT EXISTS samples.Car ("_id" text PRIMARY KEY, city text, color text, owner owner);

Dentro da modelagem teremos:

@Entity
class Car {
  @Id
  private String plate;

  @Column
  private String city;

  @Column
  private String color;

  @UDT("owner")
  @Column
  private Owner owner;
}

@Entity
class Owner {
  @Column
  private String name;

  @Column
  private String license;
}

Para o terceiro e último exemplo de modelagem dentro do Cassandra armazenaremos receitas, cada receita terá as informações básicas além dos seus ingredientes de modo que o ingrediente terá o nome, quantidade e unidade de medida.

Para esse caso, a query de criação:

CREATE KEYSPACE IF NOT EXISTS samples WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};
CREATE TYPE IF NOT EXISTS samples.ingredient(name text, quantity decimal, unit text);
CREATE COLUMNFAMILY IF NOT EXISTS samples.Recipe ("_id" text PRIMARY KEY, city text, ingredients set<frozen<ingredient>>);

Dentro da modelagem teremos:

@Entity
public class Recipe {
  @Id
  private String name;

  @Column
  private String city;

  @Column
  @UDT("ingredient")
  private Set<Ingredient> ingredients;
}

@Entity
public class Ingredient {
  @Column
  private String name;

  @Column
  private BigDecimal quantity;

  @Column
  private String unit;
}

O código executado será apresentado a seguir, lembrando, que para evitar o efeito do read-before-write na lista, utilizamos o tipo Set.

import jakarta.nosql.column.ColumnQuery;
import org.eclipse.jnosql.artemis.cassandra.column.CassandraTemplate;
import javax.enterprise.inject.se.SeContainer;
import javax.enterprise.inject.se.SeContainerInitializer;

public class RecipeApp {
  public static void main(String[] args) {
    try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
      CassandraTemplate template = container.select(CassandraTemplate.class).get();
      Recipe recipe = new Recipe("Bauru", "Bauru");
      recipe.add(new Ingredient("French bun with crumb", 1D, "unit"));
      recipe.add(new Ingredient("Cheese", 300D, "grams"));
      recipe.add(new Ingredient("Roast beef", 500D, "grams"));
      recipe.add(new Ingredient("Pickled cucumber", 150D, "grams"));
      recipe.add(new Ingredient("Tomato", 100D, "grams"));

      template.insert(recipe);

      ColumnQuery query = ColumnQuery.select().from("Recipe").where("_id").eq("Bauru").build();
      template.select(query).forEach(System.out::println);
    }
  }
}

Facilitando a manutenção e não reinventando a roda com as nuvens

Além da modelagem e das dicas do que não é uma banco de dados não relacional é importante falar da manutenção do banco de dados em si. Uma das boas perspectivas é o recurso de cloud integrado com o banco de dados. Dentro de uma visão arquitetural e a partir da escolha do tipo de serviços teremos trade-offs. Por exemplo, o kubernetes, não existe dúvidas que ele se tornou bastante popular no mundo atualmente, porém, existem vários relatos da complexidade em sua manutenção e lembre-se de que complexidade tende a resultar em riscos. Ainda há uma grande discussão sobre Docker e banco de dados e, no geral, existem algumas recomendações para que não se use no ambiente de produção. Sem falar em outros pontos, por exemplo, o backup dos dados.

Para facilitar a manutenção desses tipos de bancos de dados também temos o Database-as-a-Service (DBaaS), em que toda a operações, manutenção, backup é mantida pelo fornecedor. Assim, não é necessário se preocupar com essa complexidade. Exemplos:

Com isso falamos sobre os conceitos básicos para começar a utilizar os bancos de dados não relacionais dentro de uma arquitetura corporativa com Java. Começamos definindo o que não é um banco de dados NoSQL, modelagem para finalmente citar que não existe problema em usar o conceito de DBaaS para diminuir os riscos no lado da arquitetura. Um ponto interessante é que existem recursos bem interessantes como: bean validation e mapper que devem ser utilizados com parcimônia.

Porém, vale salientar que sempre existirá uma impedância de paradigma, ou seja, existem coisas em orientação a objetos que não serão possíveis aplicar no banco de dados e use-o com moderação. E finalizamos falando um pouco sobre as ferramentas que são discutidos de maneira bastante forte dentro das conferências. As conferências são ótimos pontos para se aprender e conhecer novas ferramentas, porém, vale salientar que as palestras muitas vezes não refletem a realidade, afinal, uma aplicação em produção tem muito mais requisitos do que uma aplicação que precisa ser apresentada em 50 minutos.

O código exemplo: https://github.com/soujava/nosql-design-pitfalls

Sobre o autor

Otávio Santana é engenheiro de software, com grande experiência em desenvolvimento opensource, com diversas contribuições ao JBoss Weld, Hibernate, Apache Commons e outros projetos. Focado no desenvolvimento poliglota e aplicações de alto desempenho, trabalhou em grandes projetos nas áreas de finanças, governamental, mídias sociais e e-commerce. Membro do comitê executivo do JCP e de vários Expert Groups de JSRs, é também um Java Champion, recebendo os prêmios de JCP Outstanding Award e Duke's Choice Award.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT