Como fazer joins no XTDB usando Clojure
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}]}