Entre para a LISTA VIP da Black Friday

00

DIAS

00

HORAS

00

MIN

00

SEG

Clique para saber mais
Alura > Cursos de Data Science > Cursos de NoSQL > Conteúdos de NoSQL > Primeiras aulas do curso Datomic Queries: avançando com o modelo e pesquisas

Datomic Queries: avançando com o modelo e pesquisas

Identificadores únicos e identidade - Introdução

Boas vindas a mais um curso de Datomic, em que discutiremos bastante sobre queries e suas variedades. A medida que formos usar cada query teremos de retrabalhar nosso esquema, de modo que possamos realizar agrupamentos e agregações, dessa maneira, iremos definir tipos de relacionamento entre entidades. Evoluiremos nossos esquemas, adicionaremos novos tipos de valores, suportaremos outros tipos de relacionamento.

Entenderemos como as relações funcionam e são armazenadas dentro do Datomic para que possamos fazer buscas específicas relativas a atributos das relações. Em um sistema em que é importante sabermos quem efetuou uma determinada ação, essa busca específica é bem importante. Neste caso, armazenaremos em todas as transações quem é o responsável pela ação, seja o nome do usuário, id, IP da máquina e assim por diante.

Como no Datomic os valores estão inseridos em uma grande tabela que define as entidades - lembrando que as transações também são entidades- veremos como utilizá-las para armazenar informações que serão úteis para criação de querys.

Vamos começar?

Identificadores únicos e identidade - Pull puro e identificadores

Este curso é uma continuação, então é importante que você já tenha tido acesso aos conteúdos anteriores. Dito isso, abriremos o projeto em que estávamos trabalhando. Caso você já conheça o conteúdo do curso anterior e queria começar a partir daqui, basta realizar o download do projeto no GitHub, o link se encontra disponível no menu de atividades.

Uma vez que o projeto estiver aberto, clicaremos sobre "project.clj > Run 'REPL for...'". Feito isso, queremos executar também o Datomic. Com o terminal aberto no diretório do Datmic-pro, executaremos o comando bin/transactor e o arquivo de configurações do transactor. Quando o processo estiver finalizado, teremos tanto o Datomic rodando quanto REPL.

Temos os arquivios das aulas antigas, nós manteremos aqueles que criam o banco com o esquema antigo e adiciona alguns produtos. Na aula6.clj, criamos a conexão com o banco, adicionamos alguns produtos e buscamos por eles logo em seguida.

(def conn (db/abre-conexao))

(db/cria-schema conn)


(let [computador (model/novo-produto "Computador Novo", "/computador-novo", 2500.10M)
      celular (model/novo-produto "Celular Caro", "/celular", 888888.10M)
      calculadora {:produto/nome "Calculadora com 4 operações"}
      celular-barato (model/novo-produto "Celular Barato", "/celular-barato", 0.1M)]
  (pprint @(d/transact conn [computador, celular, calculadora, celular-barato])))

Reutilizaremos esse código em aula1.clj, além de mantermos o namespace. Também adicionaremos o código que realiza uma busca que traz todos os produtos ((pprint (db/todos-os-produtos (d/db conn)))), além disso, adicionaremos também o código de apagar o banco, caso seja do nosso interesse (;(db/apaga-baco)). O arquivo ficará com o seguinte conteúdo:

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


(def conn (db/abre-conexao))

(db/cria-schema conn)


(let [computador (model/novo-produto "Computador Novo", "/computador-novo", 2500.10M)
      celular (model/novo-produto "Celular Caro", "/celular", 888888.10M)
      calculadora {:produto/nome "Calculadora com 4 operações"}
      celular-barato (model/novo-produto "Celular Barato", "/celular-barato", 0.1M)]
  (pprint @(d/transact conn [computador, celular, calculadora, celular-barato])))

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

;(db/apaga-baco)

Deletaremos todos os arquivos restantes que não usuaremos. Estamos o usando o banco ecommerce, mas não sabemos o que há dentro dele, como podemos fazer essa exploração? No Datomic existe a ferramenta Datomic console, em que podemos fazer essa exploração pela linha de comando. Na documentação da ferramenta, encontraremos um exemplo de utilização:

bin/console -p 8080 dev datomic:dev://localhost:4343/

Abriremos uma nova aba na linha de comando, acesaremos o diretório do Datmoc e executaremos o comando com as devidas alterações. Terminado esse processo, poderemos acessar o logalhost, e então copiaremos esse endereço no navegador. Acessaremos, assim, o console ou explorador visual do Datomic.

Na página, teremos uma série de informações. Na opção "DB" teremos o banco "ecommerce", que utilizamos no curso anterior. Nesta opção será exibido todos os bancos que nós temos. Quando selecionamos o banco de dados, também teremos acesso aos identificadores básicos que havíamos criado,além dos padrão do Datomic.

banco de dados - console do Datonic. Opção "DB" selecionada como "ecommerce". Na área "Schema" são visíveis os indetificadores básicos

Temos inclusive os produtos disponívies nesse esquema. É importante frisar que esse esquema, apesar de ser inserido em um determinado momento, ele é atemporal, funciona para todos os momentos do banco.

Nosso objetivo é fazer uma query. Selecionaremos a aba "query", e poderemos fazer uma serie de configurações visuais.

query - area de configuracao da query que contém campos a seeremo preenchidos. Dentro desta área, encontraremos a subdivisão "Where", em que teremos o campo ":db/doc"

Ao mantermos a opção ":db/doc", poderemos trazer, por exemplo, produto/nome. Então executaremos essa query, e o valor de ":db/doc" será "?doc", o terceiro parâmetro da query. Na opção "Find" encontraremos tanto a entidade quanto no doc, portanto teremos "?e?doc".

Teremos várias entidades, inclusive a 72, que é o nome de um produto. A query está funcional, mas poderia ter sido feita de todos os produtos, por exemplo. Ao invés de ":db/doc", escreveremos ":produto/nome". No valor escreveremos "?nome". No campo "Find" escreveremos "?e ?nome".

Como resultado teremos todos os nossos quatro produtos: celular barato, celular caro computador novo e calculadora com 4 operações. Podemos também buscar o preço de cada um dos produtos incluindo "produto/preco", valor "?preco" e no "Find" escreveremos "?e?nome?preco". Agora teremos os nomes e preços do produto, lembrando que calculadora de 4 dígitos não possuía um preço estipulado, portanto não surgiu na tela. O console é útil para executarmos querys e explorarmos algumas configurações de acordo com a necessidade do projeto.

Ao retornarmos para o nosso código, podemos apagar o banco de dados e adicioná-lo novamente, completamente limpo. Feito isso, abriremos a conexão de novo com o banco e criamos novamente o esquema, também limpo. Ao executarmos novamente a query e buscarmos por nome do produto, não encontraremos nada.

Com o banco limpo, adicionaremos os quatro produtos ao executarmos nosso código. Eles estarão novamente visíveis no console.

E se ao invés de buscarmos todos os produtos, nosso interesse seja em apenas um específico, como "celular barato"? Nós tempos o identificador desse produto, e então queremos trazer os dados desse produto baseado nessa informação. Nós queremos simplesmente fazer um pull na numeração do id, mas via console isso ficará complicado, pois não estamos interessados nos outros campos da query.

É possível realizar só o pull. Em nosso código, escreveremos a conexão com o banco mais o id do produto:

(pprint (db/um-produto (d/db conn) 17592186045421))

No banco, criaremos a função um-produto que receberá o banco e produto-id.

(defn um-produto [db produto-id])
    (d/pull db '[*] produto-id))

Neste ponto precisamos ficar atentos. No "Find" o padrão das queries deve ser escapado, o mesmo ocorre aqui. Feito isso, recarregaremos o arquivo, e ao executarmos o código veremos que o produto foi capturado com sucesso.

Existem outras maneiras que conseguir coletar um único produto, como coletar o primeiro deles:

(def produtos (db/todos-os-produtos (d/db conn)))
(def primeiro-produto-id (-> produtos 
                          first 
                          first
                          :db/id)
                          ))
(println "O id do primeiro produto é" primeiro-produto-id)
(print (db/um-produto (d/db conn) primeiro-produto-id))

É razoavelmente comum que tenhamos a busca só pelo id das entidades e algumas vezes já temos o pull, que transforma os dados de acordo com o modelo estabelecido.

Identificadores únicos e identidade - Identificadores únicos e UUIDs

É importante dizer que nem sempre queremos utilizar o id gerado pelo Datomic, afinal ele é sequencial. Cada entidade nova que for adicionada ao banco terá um id novo. Um possível problema nessa lógica é que usuários ou hackers podem tirar conclusões a respeito dos seus dados de acordo com esses ids.

Dentre os tipos de geradores de ids que existem, um muito famoso é o UUID. É difícil que se tire conclusões a respeito dos caracteres gerados desse modelo, pois eles são aleatórios.

Portanto, a ideia é que todas as vezes que formos chamar um produto, passaremos também o UUID, mas esse processo não está sobre responsabilidade do Datomic. Teremos o seguinte modelo ( que pode variar de acordo com o padrão da empresa):

(ns ecommerce.model)

(defn novo-produto [uuid nome slug preço]
    {:produto/id    uuid
     :produto/nome  nome
     :produto/slug  slug
     :produto/preco preco})

Primeiramente, devemos atualizar o esquema, relatando que temos um atributo novo : produto/id, o db.type é uuid. Essa informação está disponível na documentação. Então em ecomerce.db, escreveremos:

:db/cardinality : db.cardinality/many} 
{:db/ident :produto/id
 :db/valueType :db.type/uuid}
 :db/cardinality :db.cardinality/one}
])

Temos uma questão: os ids precisam ser únicos, cada produto deve ter uma identificação inédita, muito embora o UUID trabalhe com idetificadores únicos. Mas como esclarecemos para o Datomic que esse atributo deve ser único para todos que tenham produto/id? Na documentação do Datomic, na parte de "schema" encontraremos o tópico "Identity and Uniqueness", em que seremos orientados a utilizar o db.unique/identity:

 :db/cardinality  :db.cardinality/many} 
{:db/ident        :produto/id
 :db/valueType    :db.type/uuid}
 :db/cardinality  :db.cardinality/one
 :db/unique       :db.unique/identity}])

Precisamos agora adicionar o UUID para os produtos, mas como podemos gera-lo? Existem diversas maneiras de gerar o UUID, a mais simples é utilizar um código Java que nos auxiliará nesse processo. Em ecomerce.model escreveremos:

(ns ecommerce.model)

(defn uuid)(java.util.UUID/randomUUID)

(defn novo-produto [unid nome slug preco]
    {:produto/id   uuid
    :produto/nome  nome
    :produto/slug  slug
    :produto/preco preco})

Surge-nos uma questão: o novo produto recebe o UUID ou gera o UUID? Cada uma das opções apresenta vantagens e desvantagens. Ainda podemos criar uma versão dupla do método, ora recebe o UUID ora não. Isso varia de acordo com a necessidade do projeto.

Nos forçaremos todo mundo que evoca um novo produto a utilizar o model/uuid, porque existe a possibilidade do UUID tenha vindo de algum outro lugar. É possível que alguém do outro lado do sistema realize uma requisição http e enviado esse UUID.

Em ecommerce.aula1, adicionaremos então (model/uuid) em metade dos produtos:


(let [computador (model/novo-produto (model/uuid) "Computador Novo", "/computador-novo", 2500.10M)
      celular (model/novo-produto (model/uuid) "Celular Caro", "/celular", 888888.10M)
      calculadora {:produto/nome   "Calculadora com 4 operações"}
      celular-barato (model/novo-produto  "Celular Barato", "/celular-barato", 0.1M)]
  (pprint @(d/transact conn [computador, celular, calculadora, celular-barato])))

De volta a ecommerce.model, também podemos criar uma versão que não recebe o UUID, e sim apenas nome slug preco:

(ns ecommerce.model)

(defn uuid) [] (java.util.UUID/randomUUID)

(defn novo-produto
    ([nome slug preco]
    (defn novo-produto [unid nome slug preco]
        {:produto/id   uuid
        :produto/nome  nome
        :produto/slug  slug
        :produto/preco preco})}

Apagaremos nosso banco de dados, e executaremos os arquivos ecommerce.model e ecommerce.db. Dessa maneira, ao executarmos o código, veremos impresso o id dos produtos, e o uuid gerado para aqueles que receberam essa instrução.

Colocamos o UUID, nosso próprio gerador de id. Temos a opção de gerar ids dinamicamente e de utilizar ids que já foram gerados antes.

Dissemos que os ids devem ser únicos e que são identificadores ( no schema utilizamos o db.unique/identity), e como identificadores não poderíamos fazer uma busca por ele? Poderíamos.

Como, então, podemos fazer um pull do nosso próprio identificador. Em ecommerce.db, renomearemos a função um-produto para um-produto-por-dbid. Feito isso, criaremos a função um-produto, que procurará pelo produto-id

(dfn um-produto-por-dbid [db db-id]
    (d/pull db '[*] produto-dbid))

(dfn um-produto [db produto-id]
    (d/pull db '[*] produto-dbid))

Então precisamos fazer a alteração em ecommerce.aula1. Se quisermos procurar pelo produto-id, precisaremos extrair o valor. Temos a função ffirst que faz o first duas vezes.

(def produtos (db/todos-os-produtos (d/db conn)))
(pprint produtos)
(def primeiro-dbid (-> produtos
                             ffirst
                             :db/id
                             ))

(println "o id do primeiro produto é" primeiro-dbid)
(pprint (db/um-produto-por-dbid (d/db conn) primeiro-dbid))


(def produtos (db/todos-os-produtos (d/db conn)))
(pprint produtos)
(def primeiro-produto-id (-> produtos
                             ffirst
                             :produto/id
                             ))

(println "o id do primeiro produto é" primeiro-produto-id)
(pprint (db/um-produto-por-dbid (d/db conn) primeiro-produto-id))


(db/apaga-banco)

Conseguimos coletar os produtos sem problemas. Mas será que conseguimos realizar buscas utilizando o UUID? Isso não é possível, já que por padrão o pull utiliza o identificador do banco, e não o nosso. Então de alguma maneira precisamos comunicar que o produto-id que estamos passando é referente ao atributo produto/id, trata-se de um localizador, como se estivéssemos realizando um find. Podemos fazer isso pelo produto/id, pois ele retorna um elemento.

(dfn um-produto-por-dbid [db db-id]
    (d/pull db '[*] produto-dbid))

(dfn um-produto [db produto-id]
    (d/pull db '[*] [:produto/id produto-id]))

Podemos apagar o banco e refazer o processo para realizar testes. Notaremos que tudo está funcional. Com isso, passamos a utilizar o poder de uma identidade: se temos um identificador próprio - é sempre uma boa prática der o UUID - podemos um pull pelo parâmetro específico. Esse processo é chamado de busca por referência, na documentação encontraremos como lookup refs.

Sobre o curso Datomic Queries: avançando com o modelo e pesquisas

O curso Datomic Queries: avançando com o modelo e pesquisas possui 142 minutos de vídeos, em um total de 42 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