Como fazer joins no XTDB usando Clojure

2021-10-24

Aviso: Vou seguir a partir do que foi construído no guia Começando com XTDB e Clojure.

##Posts

Digamos que um usuário pode ser o autor de diversos posts e que cada post possui um título, um estado de publicação (se está publicado ou se é rascunho) e um id global (o :xt/id).

Vamos modelar um post como um conjunto destes atributos:

  • :post/title: Título do post
  • :post/author: eid do autor do post
  • :post/state: Estado da publicação do post, pode ser :draft ou :published

Para criar um post, é necessário que já exista pelo menos um usuário no banco. Vamos criar um:

(xt/submit-tx
 node
 [[::xt/put {:xt/id "user123"
             :user/name "User 123"
             :user/email "user123@email.com"}]])

O eid desse usuário é "user123". É com esse identificador que vamos relacionar o usuário com o post. Vamos agora criar um post no banco:

(xt/submit-tx
 node
 [[::xt/put {:xt/id "post-1"
             :post/title "Post 1"
             :post/state :draft
             :post/author "user123"}]])

##Query relacional

Toda vez que criamos uma relação entre dois documentos no XTDB, a relação é de via dupla. A partir de um post, conseguimos encontrar o seu autor. A partir do usuário conseguimos encontrar todos os posts em que este usuário é autor.

Primeiro, precisamos encontrar todos os autores cujo eid seja "user123". Como existe apenas um documento para cada eid (o id global). Com o autor, representado por ?post, podemos procurar por todos os posts cujo :post/author seja igual ao ?author.

Em uma query com Datalog, podemos traduzir o que eu acabei de escrever na seguinte query:

(xt/q (xt/db node)
      '{:find [?post]
        :where [[?post :post/author ?author]
                [?author :xt/id "user123"]]})

Ao realizar a query, no REPL tenho o seguinte retorno:

#{["post-1"]}

Aqui, ao buscar pelos posts do usuário user123, o XTDB nos retornou apenas o eid desse post. Isso é muito útil ao compor queries relacionais por vários documentos no banco de dados. Mas nesse caso queremos ter acesso a todas as propriedades do post.

Para automaticamente pegar todas as propriedades do documento, podemos usar a sintaxe do pull. pull não é uma função, mas sim uma sintaxe que o parser de query do XTDB entende como "busque todas as propriedades desse documento no banco".

Vamos reescrever a query:

(xt/q (xt/db node)
      '{:find [(pull ?post [*])]
        :where [[?post :post/author ?author]
                [?author :xt/id "user123"]]})

Ao escrever (pull ?post [*]) estamos indicando que queremos todas as propriedades da última versão do documento associado ao ?post, não importa o tamanho desse documento.

No REPL, obtemos o seguinte resultado:

#{[{:post/title "Post 1", :post/state :draft, :post/author "user123", :xt/id "post-1"}]}

Algumas vezes talvez possa ser o caso de buscar apenas as propriedades que nos interessam de documentos. Por exemplo, podemos buscar apenas o título e estado de cada post:

(xt/q (xt/db node)
      '{:find [(pull ?post [:post/title :post/state])]
        :where [[?post :post/author ?author]
                [?author :xt/id "user123"]]})

No REPL:

#{[#:post{:title "Post 1", :state :draft}]}

##Mais posts

Vamos criar mais alguns posts no banco de dados:

(xt/submit-tx
 node
 [[::xt/put {:xt/id "post-2"
             :post/title "Post 2"
             :post/state :draft
             :post/author "user123"}]])

(xt/submit-tx
 node
 [[::xt/put {:xt/id "post-3"
             :post/title "Post 3"
             :post/state :published
             :post/author "user123"}]])

Ao buscar novamente pelos posts associados ao usuário user123, encontramos os três:

#{[#:post{:title "Post 3", :state :published}]
  [#:post{:title "Post 1", :state :draft}]
  [#:post{:title "Post 2", :state :draft}]}

Vamos alterar o primeiro post "Post 1" de :draft para :published:

(let [post (xt/entity (xt/db node) "post-1")]
  (xt/submit-tx
   node
   [[::xt/put
     (assoc post :post/state :published)]]))

Mesmo alterando o estado do primeiro post, a associação dele com o usuário user123 ainda é válida, vamos buscar pelo histórico do post-1:

[#:xtdb.api{:tx-time #inst "2021-10-24T20:20:07.321-00:00",
            :tx-id 49,
            :valid-time #inst "2021-10-24T20:20:07.321-00:00",
            :content-hash
            #xtdb/id "34097d66814a2c1d502155cf2e6346b020aa4a44",
            :doc
            {:post/title "Post 1",
             :post/state :published,
             :post/author "user123",
             :xt/id "post-1"}}
 #:xtdb.api{:tx-time #inst "2021-10-24T19:35:39.885-00:00",
            :tx-id 43,
            :valid-time #inst "2021-10-24T19:35:39.885-00:00",
            :content-hash
            #xtdb/id "1dee6841b99b99e59c9ae70b6652cfda4dec13bb",
            :doc
            {:post/title "Post 1",
             :post/state :draft,
             :post/author "user123",
             :xt/id "post-1"}}]

Encontramos todas as versões e a relação é mantida.

Usando as features de bitemporalidade do XTDB conseguimos saber de quantos posts esse usuário é autor ao longo do tempo. Digamos que queremos saber quantos posts o usuário user123 tinha no instante de tempo #inst "2021-10-24T19:40:00.000-00:00":

(pprint (xt/q (xt/db node #inst "2021-10-24T19:40:00.000-00:00")
              '{:find [(pull ?post [:post/title :post/state])]
                :where [[?post :post/author ?author]
                        [?author :xt/id "user123"]]}))

Obtemos o resultado:

#{[#:post{:title "Post 1", :state :draft}]}

Se buscarmos pelos posts desse usuário em um outro instânte de tempo, por exemplo:

(pprint (xt/q (xt/db node #inst "2021-10-24T20:19:00.000-00:00")
              '{:find [(pull ?post [:post/title :post/state])]
                :where [[?post :post/author ?author]
                        [?author :xt/id "user123"]]}))

Obtemos

#{[#:post{:title "Post 3", :state :published}]
  [#:post{:title "Post 1", :state :draft}]
  [#:post{:title "Post 2", :state :draft}]}

Porém se buscarmos usando como base o instânte de tempo #inst "2021-10-24T20:30:00.000-00:00":

(pprint (xt/q (xt/db node #inst "2021-10-24T20:30:00.000-00:00")
              '{:find [(pull ?post [:post/title :post/state])]
                :where [[?post :post/author ?author]
                        [?author :xt/id "user123"]]}))

Obtemos como resultado os dois posts publicados:

#{[#:post{:title "Post 3", :state :published}]
  [#:post{:title "Post 1", :state :published}]
  [#:post{:title "Post 2", :state :draft}]}
#clojure#xtdb

📝 Edite esta página