Alura > Cursos de Data Science > Cursos de NoSQL > Conteúdos de NoSQL > Primeiras aulas do curso Datomic: banco filtrado e histórico

Datomic: banco filtrado e histórico

Organização - Introdução

Boas vindas a mais um curso de Datomic! Nele, vamos organizar nosso código na parte de acesso, organizando e estruturando o que se liga com o framework interno de entidade, com configuração do banco, e cada um dos nossos modelos, criaremos mais destes modelos para que possamos trabalhar em cima deles.

Além disso, lidaremos com queries mais complexas, que envolvem questões de alteração de valores do banco, pois à medida em que o fazemos, teoricamente perdemos informações, mas o Datomic estará armazenando este histórico para nós, a partir do qual poderemos recuperá-las.

Veremos como fazê-lo, seja por meio de queries ou rodando um processo de batch para atualizar o nosso banco. Utilizaremos nosso histórico para descobrir valores, pois aprenderemos a navegar nele, consultando transações, alterações, vendas, e tudo o mais.

Também veremos como o Datomic fornece funcionalidades bastante utilizadas no dia a dia de desenvolvimento de aplicações modernas, por meio de suas features, como na sincronização de um aplicativo mobile com um servidor, em que é necessário transferir informações.

Isto não quer dizer que, se adicionássemos um novo contato em uma agenda, teríamos que reenviar todas as pessoas, apenas o que existe de diferente, aquelas que forem cadastradas a partir da última sincronização. O Datomic facilita muito este tipo de processo, por conta de filtros a serem executados em nossos bancos.

Este será o grande foco deste curso: filtragem dos nossos bancos.

Organização - Organizando nosso código

Começaremos partindo do projeto do curso anterior, mas se você não fez os cursos anteriores e quer começar deste, basta baixar o código correspondente no GitHub. Abriremos o diretório contendo o projeto no IntelliJ, contendo diversas aulas. Lembrando que, na prática, apenas o conteúdo da última aula será utilizado, assim, removeremos os quatro primeiros arquivos de "ecommerce", aula1.clj, aula2.clj, aula3.clj e aula4.clj.

No arquivo remanescente, aula5.clj, substituiremos todos os aula5 por aula1, após o qual refatoraremos para o nome adequado, por sugestão do próprio programa. Anteriormente, estávamos fazendo a configuração para a validação de schema à medida em que vamos explorando, apagando o banco, abrindo uma conexão, criando schema, dados de exemplo, fazendo buscas, e assim por diante. Vamos imprimir todos os produtos, substituindo def produtos por pprint em (pprint (db/todos-os-produtos (d/db conn))).

Em seguida, removeremos todo o código que vem depois desta linha, e teremos o seguinte conteúdo em ecommerse.aula1:

(ns ecommerce.aula1
 (:use clojure.pprint)
 (:require [datomic.api :as d]
        [ecommerce.db :as db]
        [ecommerce.model :as model]
        [schema.core :as s]))

(s/set-fn-validation! true)

(db/apaga-banco!)
(def conn (db/abre-conexao!))
(db/cria-schema! conn)
(db/cria-dados-de-exemplo conn)

(pprint (dv/todos-os-produtos (d/db conn)))

Para executarmos este código, acessaremos e inicializaremos o Datomic, rodando o Transactor, que transaciona com o banco. Assim que isto for feito, abriremos o REPL clicando com o lado direito do mouse sobre project.clj no menu lateral esquerdo e em "Run 'REPL for ecommerce'".

Há um detalhe: estes arquivos estão bastante desorganizados, como se pode perceber dos nossos arquivos de modelo (model.clj) e banco (db.clj), isso porque tudo se encontra em um único local. Já passamos do ponto em que precisamos organizar isso — neste curso, faremos esta organização para melhorar nosso código em vez de avançarmos em alguns assuntos.

Recarregaremos este arquivo para garantir que tudo funciona conforme esperado enquanto o Transactor está rodando, e então começaremos a quebrar estes arquivos. Limparemos o REPL e, em vez de termos um arquivo db.clj que centraliza tudo, queremos um arquivo de configuração do banco, outros que separam funções relativas à persistência do produto, da categoria, da variação, e assim por diante.

Incluiremos outro namespace dentro de "ecommerce", denominado simplesmente db, dentro do qual criaremos outro namespace, db/config, como o nome indica, referente a configurações. Nele, incluiremos os requerimentos, copiando e colando de ecommerce.db, e adequando o código. Recortaremos todo o código que vem após estas primeiras linhas para colarmos em e.db.config.

Feito isso, poderemos excluir o arquivo db.clj, pois distribuiremos seu conteúdo em vários outros arquivos. Não queremos um namespace que tenha exatamente o mesmo nome que o namespace que o contém. Voltaremos a ecommerce.aula1 e acrescentaremos .config em ecommerce.db e db.config dentre os requerimentos.

Assim, tudo o que tiver a ver com as configurações ficará em db.config, e db/todos-os-produtos ficará como db.produto/todos-os-produtos. Além disso, importaremos um ecommerce.db.produto.

(ns ecommerce.aula1
 (:use clojure.pprint)
 (:require [datomic.api :as d]
        [ecommerce.db.config :as db.config]
        [ecommerce.db.produto :as db.produto]
        [ecommerce.model :as model]
        [schema.core :as s]))

(db.config/apaga-banco!)
(def conn (db.config/abre-conexao!))
(db.config/cria-schema! conn)
(db.config/cria-dados-de-exemplo conn)

(pprint (db.produto/todos-os-produtos (d/db conn)))

Feito isso, clicaremos sobre o diretório "db" para criar um namespace denominado produto, e tudo o que estiver ligado ao produto ficará neste arquivo, como um-produto e um-produto!, adiciona-ou-altera-produtos!, começando pelos requires. Removeremos os comentários, recortaremos e colaremos também o código referente a todos-os-produtos. todos-os-produtos-vendaveis utiliza regras, portando recortaremos e colaremos ambos em e.db.produto.

Faremos o mesmo com um-produto-vendavel, todos-os-produtos-nas-categorias, todos-os-produtos-nas-categorias-e-digital, atualiza-preco!, atualiza-produto!, total-de-produtos, remove-produto!. Não estamos colocando a transação no nosso banco, portanto removeremos o trecho abaixo:

(s/defn visualizacao! [conn produto-id :- java.util.UUID]
 (d/transact conn [[:incrementa-visualizacao produto-id]]))

Esta forma de organização é arbitrária, fique à vontade para fazer da maneira que achar melhor para o seu modelo de negócio.

Lembrando que quando pedimos todos os produtos, solicitamos o datomic para a entidade, que está em config.clj. Entidade é algo comum a todos os modelos, portanto criaremos outro namespace chamado entidade, que terá as funções relacionadas à entidade, como dissoc-db-id e datomic-para-entidade com dissoc-db-id.

Em e.db.produto, acrescentaremos dentre os requerimentos [ecommerce.db.entidade :as db.entidade]. Com isso, em todos os lugares do código onde estiver datomic-para-entidade, devemos deixar db.entidade/datomic-para-entidade. Vamos criar o namespace categoria, para onde vão os trechos correspondentes a todas-as-categorias, atribui-categorias!, db-adds-de-atribuicao-de-categorias. Também criaremos o namespace variacao.

Como teremos muitas alterações feitas de uma vez, pausaremos nosso REPL e testaremos novamente, do zero. Porém, repare que há muitos elementos inutilizados, se passarmos o mouse sobre eles; o programa nos informa o que não está sendo necessário. Em ecommerce.aula1 não estamos utilizando model, sendo assim poderemos remover [ecommerce.model :as model]. Faremos o mesmo com todas as linhas inutilizadas, em todos os arquivos do projeto.

Com o REPL rodando, tentaremos carregar aula1. Obteremos um erro indicando que adiciona-categoria não funciona. Outro erro comum possível de acontecer é realizarmos um requerimento e isto acabar fazendo um requerimento de volta, fazendo um ciclo, e o programa acusará dependência cíclica nos requires.

Em e.db.config, há algumas ações que criam dados de exemplo, e com isso são chamados nova-categoria e adiciona-categorias!. O config utilizará, então, db.categoria para adicionar categorias:

(db.categoria/adiciona-categorias! conn [eletronicos, esporte])

Também acrescentaremos db.categoria/ antes de atribui-categorias! nas duas últimas linhas de e.db.config. E, antes de adiciona-ou-altera-produtos! no mesmo arquivo, incluiremos db.produto/. Falta fazer a importação de ambos:

(ns ecommerce.db.config
 (:use clojure.pprint)
 (:require [datomic.api :as d]
        [ecommerce.model :as model]
        [ecommerce.db.categoria :as db.categoria]
        [ecommerce.db.produto :as db.produto]))

Recarregaremos, e tudo roda conforme esperado, isto é, nosso código funciona após a refatoração. Há, ainda, outras alterações que poderemos fazer: se já estamos chamando todos-os-produtos de produto, não precisamos mais de nomes tão longos. Anteriormente, quando começamos a falar sobre Clojure, Datomic, ORM ou ferramentas que estão tentando fazer o mapeamento de um mundo, ou objeto, mapas para outros mundos, vimos sobre a questão de repetirmos nomes dos tipos com os quais estamos trabalhando, ou entidades, ou o termo que você utilza para agrupar umas ou outras características.

Assim, todos é o suficiente. A linha de ecommerce.aula1 ficará da seguinte forma:

(pprint (db.produto/todos (d/db conn)))

E em e.db.produto, tudo que que tiver "-produtos" no fim terá esta palavra removida. Isto é, adiciona-ou-altera-produtos! passará a ser adiciona-ou-altera! apenas, um-produto será um, e assim por diante. Não estamos utilizando todas as funções, mas estamos organizando-as para entendermos como elas ficariam em um "mundo real".

Aplicaremos o mesmo processo em e.db.categoria, e removeremos "-categorias" de atribui-categorias! para manter apenas atribui!. Feitas estas alterações, precisaremos atualizar e.db.config, corrigindo todos estes nomes. Rodaremos o arquivo mais uma vez, e tudo parece funcionar bem.

Outro ponto possível de se analisar de acordo com as necessidades da empresa, além da questão dos namespaces e dos nomes dos métodos e funções em si, é: passaremos às funções a conexão ou o db? Neste caso, a própria função se responsabilizará por extrai-lo? Depende. Se passarmos o db, quem tem a lógica de negócios em ecommerce.aula1 é o responsável por decidir qual é o momento no tempo em que a query será executada.

Se passarmos conn, então a camada que acessa o Datomic que decide em que momento do seu tempo a query será executada. Não existe resposta certa ou errada e, na maior parte das vezes, a camada do Datomic saberá quando executar esta query. Não é tão comum assim que a maioria das queries sejam executadas em qualquer ponto do tempo, até porque o banco possui seus índices atualizados e otimizados sempre para o momento atual, e não para o passado. Então, quando vamos fazer queries no passado, não necessariamente os índices estarão otimizados para realizar buscas no passado, e sim no presente.

Limpamos os requires, os arquivos, que acabamos "fragmentando" por motivos de organização. Não tenha medo, pois do mesmo modo como criamos o db, poderíamos ter um namespace chamado "model" com "produto", "variacao", "categoria", de forma separada. Em nosso caso, por ainda estamos lidando com uma quantidade pequena de diretórios e arquivos, meu julgamento pessoal diz que tudo bem estarem no mesmo arquivo.

O problema do tempo - Suportando vendas de produtos

Continuando, criaremos um namespace denominado "aula2", bastante similar à "aula1", de que poderemos copiar o conteúdo. Desta vez, não buscaremos todos os produtos apenas. Se queremos o primeiro deles, por exemplo, substituiremos a linha (pprint (db.produto/todos (d/db conn))) pelo trecho a seguir:

(def produtos (db.produto/todos (d/db conn)))
(def primeiro (first produtos))
(pprint primeiro)

Utilizaremos "Cmd + Shift + L" para recarregarmos o código, com o qual o primeiro produto será impresso. Para cadastrarmos uma venda deste produto, criaremos um modelo para representá-la. Clicaremos em cria-schema de ecommerce.aula2 e, da mesma forma que tivemos categorias e afins, teremos uma venda. Poderemos inclusive duplicar seu código e fazer as devidas alterações:

{:db/ident            :venda/produto
:db/valueType            :db.type/ref
:db/cardinality            :db.cardinality/one}
{:db/ident            :venda/quantidade
:db/valueType            :db.type/long
:db/cardinality            :db.cardinality/one}
{:db/ident            :venda/id
:db/valueType            :db.type/uuid
:db/cardinality            :db.cardinality/one
:db/unique            :db.unique/identity}

Com isso, teremos a quantidade, o produto vendido e o ID desta venda. Neste caso, para registrar 3 produtos vendidos, teremos que, de alguma maneira, criar um modelo que referencie Produto. Faremos isto em model.clj, logo abaixo deste.

(def Venda
 {:venda/id java.util.UUID
  (s/optional-key :venda/produto) Produto
    (s/optional-key :venda/quantidade) s/Int})

quantidade é um long? Analisando o s/Int do schema, teremos que trata-se de um inteiro (integer) qualquer. Usar o optional-key dependerá de sua escolha pessoal, já discutimos anteriormente sobre as vantagens e desvantagens de colocarmos este mapa como optional, mas reparem que neste caso o fazemos antecipadamente, prevendo que teremos momentos com apenas a chave produto, e não quantidade, ou somente id e não os outros dois. Estaremos assumindo esta probabilidade. Será que isso acontecerá mesmo, ou será que é um overengineering?

Seguimos este padrão independentemente de utilizá-lo ou não, ou fazemos da maneira mais simples, forçando venda/produto e venda/quantidade até o momento em que fizer sentido ser um opcional? Depende da sua empresa, da regra de negócio aplicada. Pessoalmente prefiro seguir o caminho mais simples, porque a atualização fica mais fácil, mas se isto leva os desenvolvedores da equipe a ir pelo caminho do erro, de atualizar de maneira errada, é melhor deixar opcional.

Voltaremos a ecommerce.aula2 para adicionar tal venda:

(pprint (db.venda/adiciona! conn (:produto/id primeiro) 3))

É indicado que adiciona! não existe, pois ainda não o acrescentamos dentre os requerimentos. Adicionaremos, portanto, a linha [ecommerce.db.venda :as db.venda], após o qual criaremos um namespace chamado venda, bastante similar ao namespace produto, uma vez que utilizaremos o Datomic API, dentre outros. Substituiremos produto do cabeçalho por venda.

Como queremos realizar uma transação, incluiremos:

(defn adiciona!
 [conn produto-id quantidade]
 (d/transact conn [{:venda/produto   [:produto/id produto-id]
                 :venda/quantidade   quantidade
                 :venda/id   (model/uuid)}]))

O trecho acima possui os três valores da venda, isto é, produto, quantidade e id, os quais transacionamos, e então devolvemos os datoms de adiciona!. Voltando a ecommerce.aula2, tentaremos executar isso após o carregarmos integralmente, e obteremos uma promise. Para não termos que analisá-la inteiramente, dereferenciaremos quando formos executar um adiciona!, portanto:

(pprint @(db.venda/adiciona! conn (:produto/id primeiro) 3))

Executaremos a promise e teremos seu resultado, então acrescentaremos uma linha idêntica a esta acima e trocaremos a quantidade de vendas de 3 para 4. Porém, repare que é difícil obter maiores informações sobre esta venda, pois não sabemos qual foi a venda criada. Quando adicionamos algo no banco, é comum querermos saber o que foi criado, então, em adiciona!, se não passamos o ID, queremos pelo menos devolver o ID criado.

Em e.db.venda, trocaremos (model/uuid) por id e acrescentaremos um let, primeiramente definindo o ID, fazendo a transação e por fim, devolvendo o ID. Além disso, em vez de tempids, que são números grandes, poderemos definir um ID, isto é, lidar com valores específicos, inclusive temporários, como "venda", neste caso.

(defn adiciona!
 [conn produto-id quantidade]
 (let [id (model/uuid)]
  (d/transact conn [{:db/id   "venda"
                 :venda/produto   [:produto/id produto-id]
                 :venda/quantidade   quantidade
                 :venda/id   id}])
  id))

Com isso, não precisaremos mais dos @ colocado em ecommerce.aula2, pois estamos executando e devolvendo id, portanto os removeremos. Vamos deixar o trecho de código da seguinte maneira:

(def venda1 (db.venda/adiciona! conn (:produto/id primeiro) 3))
(def venda2 (db.venda/adiciona! conn (:produto/id primeiro) 4))
(pprint venda1)

Ao rodarmos, teremos o ID da primeira venda, além das informações do produto. Por enquanto, criamos modelo, que colocamos no Datomic, permitimos a sua adição no banco, e vimos algumas questões em relação ao ID. Um último ponto de refatoração que acabou faltando anteriormente é que cria-dados-de-exemplo tem efeito colateral, sendo assim, incluiremos um ponto de exclamação (!) nele, isto é, cria-dados-de-exemplo!. Faremos esta alteração também em e.db.config.

O próximo passo consiste em sabermos quanto custou a venda. Veremos isso a seguir!

Sobre o curso Datomic: banco filtrado e histórico

O curso Datomic: banco filtrado e histórico possui 104 minutos de vídeos, em um total de 28 atividades. Gostou? Conheça nossos outros cursos de NoSQL em Data Science, ou leia nossos artigos de Data Science.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda NoSQL acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas