O Apache CouchDB é um banco de dados orientado a documentos de software livre e baseado em Erlang. O CouchDB é sem esquema, no sentido de que cada documento é independente e não exige campos específicos (além de um identificador e uma revisão). Todas as ações, da consulta ao banco de dados à criação ou mudança dos dados nele, são executadas por meio de uma API baseada em REST. O CouchDB pode ser uma excelente alternativa aos bancos de dados relacionais para muitos aplicativos, especialmente aqueles que envolvem dados menos estruturados. Este artigo aborda o uso de Clojure para executar operações básicas de CouchDB, consultas ao CouchDB usando visualizações e replicação de banco de dados. Os exemplos de código mostram como acessar as APIs REST do CouchDB a partir do Clojure em um nível mais alto, usando a API Clutch, e em nível mais baixo, usando uma biblioteca HTTP mais fundamental: clj-http.
O código de exemplo do artigo foi escrito para CouchDB 1.0.1, Clojure 1.2.0, Clutch 0.2.4 e clj-http 0.1.2. Foi usada a ferramenta de desenvolvimento Leiningen para fazer o download e configurar as dependências para o código de exemplo. Os exemplos são escritos da perspectiva de codificação no Clojure REPL.
Antes de começar, confirme se o CouchDB está instalado (veja Recursos para obter informações de instalação); binários pré-empacotadas estão disponíveis para muitos sistemas operacionais, e talvez o seu os inclua por padrão. Para configurar seu ambiente a fim de executar o código, primeiro instale o Leiningen (veja Recursos onde se encontra o link de download). Depois, crie um novo projeto do Leiningen com lein new couchdb-from-clojure. Acrescente Clutch e clj-http ao arquivo project.clj, para que ele fique semelhante à Listagem 1:
Listagem 1. CouchDB com Clojure project.clj
(defproject couchdb-with-clojure "1.0.0-SNAPSHOT"
:description "CouchDB from Clojure Examples"
:dependencies [[org.clojure/clojure "1.2.0"]
[org.clojure/clojure-contrib "1.2.0"]
[com.ashafa/clutch "0.2.4"]
[clj-http "0.1.2"]])
|
A seguir, execute lein deps para fazer o download dos arquivos JAR necessários. Pode-se executar o código a partir de um REPL em qualquer ambiente desejado. É possível abrir um Leiningen via lein repl ou do IDE à sua escolha. A partir do prompt de REPL, digite as instruções mostradas na sessão REPL da Listagem 2 para incluir namespaces que o artigo usa:
Listagem 2. Solicitando clj-http, contrib.json e Clutch
user> (require ['com.ashafa.clutch :as 'clutch]) nil user> (require ['clj-http.client :as 'client]) nil user> (require ['clojure.contrib.json :as 'json]) nil user> (def movies-db "http://localhost:5984/movies") #'user/movies-db |
A última instrução da Listagem 2 define a URL que será usada para acessar o banco de dados CouchDB usado para os exemplos do artigo. A URL padrão para o CouchDB instalado localmente é http://localhost:5984. Mude o número da porta conforme necessário em sua sessão REPL, se sua cópia do CouchDB estiver configurada de forma diferente.
Os dados no CouchDB são estruturados em formato de documento JavaScript Object Notation (JSON) autônomo — uma diferença significativa em relação a bancos de dados relacionais. Vejamos o caso de um banco de dados de filmes. Para modelar filmes em um banco de dados relacional, provavelmente será preciso ter uma tabela de informações específicas dos filmes, como título e data de release. Filmes têm atores, diretores, produtores, e assim por diante, mas provavelmente essas informações não serão incluídas na tabela de filmes. Em vez disso, haveria uma tabela de atores ou algo mais geral (como uma tabela de quem participou do filme) e uma referência da tabela de filmes para uma linha na tabela de atores. Mesmo essa estrutura talvez seja simples demais. Provavelmente, será preciso configurar um relacionamento de muitos para muitos com uma tabela unida, exigindo várias ligações para determinar os atores de determinado filme. Esse processo, chamado de normalização, reestrutura os dados para limitar a redundância. Os bancos de dados relacionais são ajustados para lidar com os dados dessa forma.
Em um banco de dados de filmes em CouchDB, todas as informações de determinado filme seriam contidas em um único documento. Isso pode causar certa duplicação, em comparação com uma estrutura mais normalizada. Por exemplo, o nome de um ator apareceria em um documento para cada filme no qual ele teve um papel. A Listagem 3 mostra a aparência de um documento de filmes armazenado ou enviado do CouchDB:
Listagem 3. Exemplo de documento JSON para dados de filmes
{"movie-title":"Psycho",
"director":"Alfred Hitchcock",
"runtime":109,
"year-released":1960,
"studio":"Shamley Productions"
"actors":["Anthony Perkins" "Vera Miles" "John Gavin" "Janet Leigh"]}
|
O formato JSON distingue entre objetos, arrays e literais. Chaves {} indicam um objeto, e colchetes [] , um array. Os literais são cadeias de caractere, como "Psycho" e números inteiros, como 1960. Essa formatação coincide bem com as estruturas de dados persistentes do Clojure, em que as chaves são usadas para mapas e colchetes para vetores; os literais também têm a mesma sintaxe. A Tabela 1 mostra exemplos dos tipos de dados JSON e seus equivalentes em Clojure:
Tabela 1. Tipos de dados equivalentes em JSON e Clojure
| Tipo de dados | Exemplo em JSON | Exemplo em Clojure | Descrição |
|---|---|---|---|
| Número | 1 | 1 | Tipo para representação de números inteiros e reais |
| Cadeia de caractere | "Cadeia de caractere de exemplo" | "Cadeia de caractere de exemplo" | Tipo para representação de cadeia de caractere |
| Variáveis booleanas | true/false | true/false | Tipo de variáveis booleanas |
| Array | [1, 2, 3, 4] | [1 2 3 4] | Array de JSON; vetor de Clojure |
| Objeto | {"key1" : "value1",
"key2" : "value2"} | {:key1 "value1"
:key2 "value2"}
| Objeto JSON; mapa Clojure |
A Listagem 4 é um documento JSON (do CouchDB) seguido por uma representação equivalente de mapa Clojure:
Listagem 4. Comparando objetos JSON e mapas Clojure
;;JSON object for Psycho
{"_id":"Psycho"
"Director":"Alfred Hitchcock",
"runtime":109,
"year-released":1960,
"studio":"Shamley Productions"}
;;Clojure map for Psycho
{:_id "Psycho"
:director "Alfred Hitchcock"
:runtime 109
:year-released 1960
:studio "Shamley Productions"}
|
A biblioteca clojure.contrib.json facilita o trabalho do JSON em Clojure. Essa biblioteca pega uma estrutura de dados Clojure e a converte para uma cadeia de caractere JSON, e vice-versa. Ela converte as chaves nas entradas do mapa de objetos JSON para palavras-chave Clojure, que são mais idiomáticas para o trabalho com Clojure. Os exemplos baseados em HTTP que mostrarei usam clojure.contrib.json. A API Clutch usa essa biblioteca em segundo plano, protegendo você dos detalhes do JSON.
Suponhamos que seja interessante pedir ao CouchDB todas as informações de que ele dispõe sobre o filme Psycho. Para isso, é preciso armazenar um documento como o mostrado na Listagem 3. O CouchDB pode conter muitos bancos de dados, de modo que a primeira etapa é criar um banco de dados e, depois, acrescentar o documento. A Listagem 5 cria um banco de dados na URL movies-db e um documento:
Listagem 5. Criando um documento CouchDB com Clutch
user> (clutch/create-database movies-db)
{:ok true...}
user> (clutch/with-db movies-db
(clutch/create-document {:director "Alfred Hitchcock"
:runtime 109
:year-released 1960
:studio "Shamley Productions"}
"Psycho"))
{:_id "Psycho" ... }
|
A Listagem 5 mostra um pouco do que o Clutch faz por trás da API para criar o documento CouchDB. Note que o último argumento passado para create-document é o ID de documento.
A Listagem 6 mostra o código equivalente em clj-http:
Listagem 6. Criando um documento CouchDB com clj-http
user> (client/put movies-db) ;; Create Database
{:status 201 ... :body "{\"ok\":true}\n"}
user> (->> {:director "Alfred Hitchcock"
:runtime 109
:year-released 1960
:studio "Shamley Productions"}
json/json-str
(hash-map :body)
(client/put (str movies-db "/Psycho")))
{:status 201... :body "{\"ok\":true,
\"id\":\"Psycho\",\"rev\":\"1-ba6b110617a1a8920903b648f208a8fac\"}\n"}
|
O CouchDB não permite a criação do mesmo banco de dados duas vezes. Se quiser executar os exemplos da Listagem 5 e da Listagem 6 ao mesmo tempo, exclua o banco de dados usando (client/delete movies-db) antes de recriá-lo.
A Listagem 6 cria um hashmap com as informações do filme e, em seguida, converte-o do hashmap Clojure para um documento JSON (usando json/json-str). O documento é colocado dentro de outro hashmap que o clj-http reconhece como corpo da solicitação. O código, por fim, emite uma solicitação PUT para que o CouchDB armazene o documento. Note que a URL usada para PUT do documento é a URL do banco de dados de filmes, seguida pelo ID do documento CouchDB (Psycho , nesse caso).
Para verificar se o documento foi persistido com sucesso, pode-se recuperá-lo programaticamente do CouchDB e examiná-lo. A Listagem 7 mostra como recuperar um documento usando o Clutch, que converte da resposta JSON para um mapa Clojure:
Listagem 7. Recuperando um documento do CouchDB usando Clutch
user> (clutch/with-db movies-db
(clutch/get-document "Psycho"))
{:_id "Psycho",
:_rev "1-a6b110617a1a8920903b648f208a8fac",
:director "Alfred Hitchcock",
:runtime 109,
:year-released 1960,
:studio "Shamley Productions"}
|
A Listagem 8 mostra um exemplo similar que usa o clj-http:
Listagem 8. Recuperando um documento do CouchDB usando clj-http
user> (-> (str movies-db "/Psycho")
client/get
:body
json/read-json)
{:_id "Psycho",
:_rev "1-a6b110617a1a8920903b648f208a8fac",
:director "Alfred Hitchcock",
:runtime 109,
:year-released 1960,
:studio "Shamley Productions"}
|
O código clj-http na Listagem 8 converte da resposta JSON, usando o corpo da resposta e chamando json/read-json nele.
Também é possível verificar facilmente seu trabalho fora do código, de diversas maneiras. Um modo é simplesmente digitar as mesmas URLs REST usadas no seu código em um navegador, ou usar cURL ou uma ferramenta similar. Insira a URL de GET usada antes: http://localhost:5984/movies/Psycho.
O modo mais fácil é usar o aplicativo Futon do CouchDB (veja Recursos). Ele é enviado com o CouchDB e pode ser visualizado pela URL http://localhost:5984/_utils. (Na URL, substitua pelo número de porta correto se sua instalação do CouchDB não estiver configurada para usar a porta padrão). Também é possível usar Futon para a criação de documento e de visualização, replicação, e muito mais.
A seguir, vamos nos aprofundar um pouco mais em como acrescentar documentos ao CouchDB.
Criando um documento — em detalhes
Na seção Criando um documento CouchDB , criamos um registro para o filme Psycho. Para ilustrar a importância de escolher um bom ID de documento, acrescente um novo filme ao banco de dados. Psycho teve uma refilmagem em 1998, por isso, tente acrescentar a nova versão ao banco de dados, como mostrado na Listagem 9:
Listagem 9. Acrescentando um documento com ID conflitante
user> (clutch/with-db movies-db
(clutch/create-document {:director "Gus Van Sant"
:runtime 105
:year-released 1998
:studio "Universal Pictures"}
"Psycho"))
;;409 Conflict
|
O código na Listagem 9 causa um erro, pois tenta usar um ID existente no banco de dados. Todos os documentos do CouchDB são armazenados por ID, e cada ID de documento deve ser exclusivo. O título do filme que, nesse caso, talvez parecesse exclusivo, — não era. Basear o ID no título do filme foi uma configuração propensa a conflitos. (Nesse caso, há um conflito em um único banco de dados. Em um ambiente distribuído, isso pode ser ainda mais comum. Vamos tratar dessas questões de replicação em mais detalhes logo adiante). Por isso, é preciso refazer o modo de especificar o ID do documento. A maneira recomendada é usar algo que tenha a garantia de exclusividade, como um identificador exclusivo universal (UUID). Com o Clutch, se não um ID for especificado, ele será gerado.
A Listagem 10 é um documento novo, refatorado para levar em conta a necessidade de um ID exclusivo:
Listagem 10. Escolhendo um melhor ID de filme (exemplo de Clutch)
user> (clutch/with-db movies-db
(clutch/create-document {:movie-title "Psycho"
:director "Gus Van Sant"
:runtime 105
:year-released 1998
:studio "Universal Pictures"}))
{:_id "d6993381eb5ede34fded2f018b9f10b0",
:_rev "1-29ff788958134c2023d9be94a9231528",
:movie-title "Psycho",
:director "Gus Van Sant",
:runtime 105,
:year-released 1998,
:studio "Universal Pictures"}
|
O documento na Listagem 10 move o ID original, Psycho, para movie-title, não deixando nenhum par de chaves de ID. Note os dois campos adicionais no documento. Um é _rev, que vamos analisar mais tarde. O outro é _id, um valor exclusivo gerado automaticamente que evita o problema da Listagem 9. Note que seus próprios valores de _id e _rev serão diferentes daqueles encontrados na Listagem 10, pois foram gerados.
A Listagem 11 é o código clj-http similar:
Listagem 11. Escolhendo um melhor ID de filme (exemplo de clj-http)
user> (->> {:movie-title "Psycho"
:director "Gus Van Sant"
:runtime 105
:year-released 1998
:studio "Universal Pictures"}
json/json-str
(hash-map :body)
(client/put (str movies-db "/" (java.util.UUID/randomUUID)))
:body
json/read-json)
{:ok true,
:id "f043a641-045b-4316-83f5-67c8f9bb99c3",
:rev "1-29ff788958134c2023d9be94a9231528"}
|
A maior parte da Listagem 11 é idêntica ao código clj-http anterior; uma diferença é a origem do ID. A Listagem 11 usa um UUID criado por JVM. Se você executar POST no documento da Listagem 11 para CouchDB, este também gerará automaticamente um UUID e o acrescentará ao documento, usando sua própria estratégia de geração de UUID. Também é possível obter UUIDs gerados pelo CouchDB na URL http://localhost:5984/_uuids.
Para confirmar se o documento foi criado corretamente, pode-se recuperar uma lista de todos os IDs de documento do banco de dados. A Listagem 12 faz isso com o Clutch:
Listagem 12. Obtendo IDs de documento de banco de dados com Clutch
user> (clutch/with-db movies-db
(->> (clutch/get-all-documents-meta)
:rows
(map :id)))
("d6993381eb5ede34fded2f018b9f10b0" "Psycho")
|
A Listagem 13 usa clj-http para recuperar uma lista de IDs:
Listagem 13. Obtendo IDs de documento de banco de dados com clj-http
user> (->> (str movies-db "/_all_docs"
client/get
:body
json/read-json
:rows
(map :id))
("d6993381eb5ede34fded2f018b9f10b0" "Psycho")
|
As chamadas na Listagem 12 e na Listagem 13 pedem ao CouchDB todos os metadados do documento no banco de dados de filmes, retornando um ID gerado automaticamente e outro chamado Psycho. Por questões de consistência, a Listagem 14 exclui o documento Psycho e o acrescenta de volta com um ID gerado:
Listagem 14. Excluindo um documento com chave antiga e reacrescentando
(clutch/with-db movies-db
(let [original-psycho (clutch/get-document "Psycho")]
(clutch/delete-document original-psycho)
(-> original-psycho
(assoc :movie-title (:_id original-psycho))
(dissoc :_id)
clutch/create-document)))
{:_id "84bbfce1b0e4cf6c9aa2f4196909f39d", :movie-title "Psycho"...}
|
A Listagem 14 recupera o documento Psycho atual, exclui-o do CouchDB e o recria acrescentando uma nova chave movie-title com o valor atual de ID, e removendo o ID do mapa. O ID deve ser removido, pois o Clutch criará um documento com esse ID, se ele for incluído.
Atualizando o documento CouchDB
Atualizar um documento é como inserir um, com uma ligeira diferença. Ao criar um documento, ele recebe automaticamente uma revisão. A Listagem 15 mostra a saída de um filme recém-criado:
Listagem 15. Documento de exemplo com revisão
user> (clutch/with-db movies-db
(clutch/create-document {:movie-title "Rear Window"
:director "Alfred Hitchcock",
:runtime 112,
:year-released 1955,
:studio "Paramount Pictures"}))
{:_id "1f91c6a2e1af23fa89ca640e889bbdb6",
:_rev "1-43386b891e9ad538de0d16fcb66aff5e",
:movie-title "Rear Window"...}
|
A revisão é a entrada de mapa _rev na Listagem 15. Na verdade, essa revisão é o hash MD5 do documento, acrescentado automaticamente pelo CouchDB. Toda vez que o documento é modificado, esse hash muda. Essa revisão é sempre necessária ao atualizar um documento CouchDB, para que o CouchDB saiba qual versão do documento sua mudança está atualizando. A Listagem 16 obtém o documento de filme Rear Window , faz a mudança e atualiza esse documento para acrescentar um título alternativo ao filme:
Listagem 16. Atualizando um documento
user> (clutch/with-db movies-db
(-> (clutch/get-document "1f91c6a2e1af23fa89ca640e889bbdb6")
(clutch/update-document {:alternate-titles ["La ventana indiscreta"]})))
=> {:alternate-titles ["La ventana indiscreta"]
:_id "1f91c6a2e1af23fa89ca640e889bbdb6",
:_rev "2-6601a377a55d733c0bd111539801edc8",
:movie-title "Rear Window"...}
|
Observe que a Listagem 16 consulta o documento por UUID. Assim, verifique na Listagem 12 (ou Listagem 13) seu próprio UUID, e use-o na Listagem 16.
A chamada update-document na Listagem 16 transfere dois argumentos. O primeiro é o documento original e o segundo, um hashmap que é mesclado com o documento original antes de o documento atualizado ser armazenado no CouchDB. A função update-document é realmente um multimétodo, e serve de espelho para muitas abordagens padrão do Clojure para manipulação de mapas, como mesclagem em pares de chaves/valores e atualização de estruturas aninhadas, como update-in. Ela também aceita como argumento um único mapa que já fez as mudanças necessárias (mas deixando a revisão e o ID intactos).
A abordagem da Listagem 16 é um pouco otimista da perspectiva de simultaneidade. Agora, avalie o código na Listagem 17:
Listagem 17. Atualizando com conflito
user> (clutch/with-db movies-db
(let [client1-rw (clutch/get-document
"1f91c6a2e1af23fa89ca640e889bbdb6")
client2-rw (clutch/get-document
"1f91c6a2e1af23fa89ca640e889bbdb6")]
(clutch/update-document client1-rw
#(conj % "Fenêtre sur cour")
[:alternate-titles])
(clutch/update-document client2-rw
#(conj % "Arka pencere")
[:alternate-titles])))
;; 409 Conflict Error
|
Aqui, o documento é recuperado duas vezes, e a primeira atualização continua sem problemas. A segunda atualização falha com um erro 409 — um código de erro de conflito HTTP que os aplicativos usam para informar os visitantes que a operação não pôde ser concluída, devido a um conflito com o estado atual do recurso. Quando os documentos foram recuperados, eles possuíam o ID de revisão correto, de modo que a primeira atualização foi bem-sucedida; mas a segunda atualização falha, pois há uma nova versão do documento que não foi vista pelo segundo atualizador. O CouchDB não permitirá a atualização de um documento se seu número de revisão estiver desatualizado. O que o segundo atualizador pode fazer? Infelizmente, a resposta é que depende de seus objetivos. Uma maneira de reduzir a possibilidade desse erro é sempre recuperar o documento imediatamente antes da atualização. Se o código na Listagem 17 for modificado como mostrado na Listagem 18, a atualização funcionará:
Listagem 18. Atualização de dois clientes sem conflito
(clutch/with-db movies-db
(-> (clutch/get-document "1f91c6a2e1af23fa89ca640e889bbdb6")
(clutch/update-document #(conj % "Fenêtre sur cour")
[:alternate-titles]))
(-> (clutch/get-document "1f91c6a2e1af23fa89ca640e889bbdb6")
(clutch/update-document #(conj % "Arka pencere")
[:alternate-titles])))
{:movie-title "Rear Window",
:alternate-titles ["La ventana indiscreta"
"Fenêtre sur cour"
"Arka pencere"]
...}
|
Embora isso resolva o problema imediato, ele ainda pode ocorrer. É preciso escrever código para lidar com esse cenário. Dependendo de seus requisitos, a solução pode ser tão fácil como recuperar novamente o documento e mesclar novamente com essa nova versão. Outra possibilidade é devolver um erro para o usuário (por exemplo, se ele estiver tentando comprar um item que não está mais disponível).
Um último ponto sobre a atualização de documentos é que o CouchDB não tem noção de que parte de um documento mudou, apenas de que o documento foi alterado. Isso porque qualquer alteração no documento resultará em um novo hash dele (que o CouchDB usa para criar o ID de revisão). Alterações puramente aditivas, excluir partes de documentos e modificar documentos são tratados da mesma maneira, e resultarão em uma nova revisão. Isso também é importante na replicação de bancos de dados.
O CouchDB não é consultado via SQL com um banco de dados relacional. O modo principal para a recuperação de dados é através de um código em estilo MapReduce chamado views. Pode-se escolher muitas linguagens para escrever visualizações. (A linguagem padrão é JavaScript. Os exemplos a seguir também funcionarão com JavaScript, mas o código MapReduce específico é diferente).
Para este artigo, será usado o Clojure como linguagem de visualização por meio do servidor de visualização Clojure, incluído no Clutch. Para usar o servidor de visualização, é preciso tê-lo instalado em sua cópia do CouchDB (veja Recursos , onde há um link para informações de instalação no Web site do Clutch). Note que o servidor de visualização é um acréscimo ao servidor, não ao seu código de cliente.
Começaremos criando e executando uma visualização, e depois veremos mais detalhes. Primeiro, para ter mais itens no banco de dados para consulta, acrescente alguns outros documentos, como mostrado na Listagem 19:
Listagem 19. Acrescentar documentos em grande quantidade com Clutch
user> (clutch/with-db movies-db
(clutch/bulk-update
[{:movie-title "The Godfather"
:director "Francis Ford Coppola"
:runtime 175
:year-released 1972
:studio "Paramount"}
{:movie-title "The Godfather II"
:director "Francis Ford Coppola"
:runtime 200
:year-released 1974
:studio "Paramount"}
{:movie-title "The Godfather III"
:director "Francis Ford Coppola"
:runtime 162
:year-released 1990
:studio "Paramount"}]))
|
A Listagem 19 usa o recurso do CouchDB de atualização em massa. Atualizações em massa funcionam para documentos recém-criados e para atualizar vários documentos existentes.
O código na Listagem 20 consulta todos os documentos para criar uma visualização temporária, que mostra os tempos de execução de todos os filmes no banco de dados:
Listagem 20. Exemplo de visualização temporária
user> (clutch/with-db movies-db
(clutch/ad-hoc-view
(clutch/with-clj-view-server
{:map (fn [doc] (when (and (:movie-title doc)
(:runtime doc))
[[(:movie-title doc)
(:runtime doc)]]))})))
{:total_rows 6,
:rows [{:id "d6993381eb5ede34fded2f018b9f10b0",
:key "Psycho",
:value 105}
{:id "84bbfce1b0e4cf6c9aa2f4196909f39d",
:key "Psycho",
:value 109}
...]}
|
Para cada filme, o título é retornado com seu tempo de execução associado. Note que também verificamos a existência de movie-title e runtime. Isso porque o código será executado em cada documento, incluindo os recém-criados. O CouchDB não usa esquemas definidos, de modo que não é necessário que todos os documentos contenham os mesmos campos. É uma boa ideia proteger suas visualizações contra a possibilidade de que os campos consultados não existam em todos os documentos.
A função na Listagem 20 retorna um vetor de vetores. Isso porque cada documento poderia gerar zero em muitas entradas de mapa, e cada uma destas é representada como um vetor. O primeiro item no vetor interno é a chave (nesse caso, movie-title) e o segundo, o valor (nesse caso, um número inteiro que representa o tempo de execução).
Essa visualização envia algo semelhante aos documentos criados. Neste caso, envia o nome do filme e o valor do tempo de execução em vez de um mapa, mas conceitualmente é a mesma coisa. O valor de um filme pode ser um mapa, se necessário. Note também que, embora a saída seja semelhante a um documento, o requisito de exclusividade não se aplica. No caso de visualizações, tudo que é emitido como chave (o primeiro item no vetor interno) é pareado internamente com o ID do documento de onde veio a chave . Pode-se ver esse ID na saída de visualização da Listagem 20. A saída é o esperado, mostrando todos os tempos de execução dos filmes no banco de dados.
Existem alguns problemas com a abordagem que acabamos de demonstrar na execução da visualização. Primeiro, os problemas são temporários, voltados para o desenvolvimento. Cada documento será reexaminado no banco de dados toda vez que for executado, mesmo que não tenha sido alterado desde a última vez em que houve uma execução. Ao "reexaminar cada documento", cada documento será enviado para a função e o resultado será acrescentado ao mapa de saída. Segundo, estamos limitados à execução da consulta apenas por meio do código Clojure.
Para resolver esses dois problemas, é possível pode persistir a visualização, como mostrado na Listagem 21:
Listagem 21. Armazenando uma visualização do CouchDB via Clutch
user> (clutch/with-db movies-db
(clutch/save-view "movies" "runtimes"
(clutch/with-clj-view-server
{:map (fn [doc] (when (and (:movie-title doc)
(:runtime doc))
[[(:movie-title doc)
(:runtime doc)]]))})))
{:_id "_design/movies",
:language "clojure",
:views {"runtimes" ...}}
user> (clutch/with-db movies-db
(clutch/get-view "movies" "runtimes"))
{:total_rows 6,
:rows [{:id "d6993381eb5ede34fded2f018b9f10b0",
:key "Psycho",
:value 105}
{:id "84bbfce1b0e4cf6c9aa2f4196909f39d",
:key "Psycho",
:value 109}
...]}
|
Ao persistir a visualização, é necessário salvar os resultados da consulta na primeira vez em que a executar (a atualização é feita apenas quando há mudanças nos documentos). E torne-a disponível para que todos os usuários executem a consulta (do Clojure, de um navegador da Web, de outro idioma, e assim por diante). O código na Listagem 21 retorna os mesmos resultados da Listagem 20 , mas é mais inteligente no que se refere ao armazenamento em cache, e pode ser reutilizado em outras linguagens, em Futon ou no navegador.
Visualizações do CouchDB— em detalhes
O uso de visualizações gera evidentes ganhos de desempenho em relação à consulta manual de dados. Previamente neste artigo, consultamos o banco de dados apenas por chave de documento, tendo encontrado as chaves ao obter uma lista de documentos ou (quando o banco de dados tinha chaves por título do filme) conhecendo a chave de forma antecipada. Quando o ID do documento específico que procuramos é conhecido, a consulta é executada rapidamente. Caso contrário (o cenário mais provável), ela é lenta. O banco de dados de filmes contém apenas alguns documentos, de modo que até mesmo visualizações temporárias têm retorno rápido. Em bancos de dados com milhares ou centenas de milhares de documentos, a execução da função map em cada documento pode ser demorada. O armazenamento dos resultados é essencial para que sejam úteis.
Para salvar uma visualização usando Clutch, a Listagem 21 usou a função save-view , enviando duas cadeias de caractere e um mapa de Clojure com um único par de chaves/valores de map , com uma função para o valor. O Clutch faz um bom trabalho ao abstrair os detalhes sem importância do salvamento do documento de visualização. Essas visualizações, na verdade, são armazenadas como documentos CouchDB regulares, mas com nomes especiais.
A Listagem 22 é um exemplo de criação de um documento de visualização usando clj-http:
Listagem 22. Armazenando uma visualização com clj-http
user> (->> {:language "clojure"
:views {:runtimes
{:map "(fn [doc]
(when (and (:movie-title doc)
(:runtime doc))
[[(:movie-title doc)
(:runtime doc)]]))"}}}
json/json-str
(hash-map :body)
(client/put (str movies-db "/_design/movies/")))
{:status 201
...
:body "{\"ok\":true...}\n"}
user> (-> (str movies-db "/_design/movies/_view/runtimes")
client/get
:body
json/read-json
:rows)
[{:id "d6993381eb5ede34fded2f018b9f10b0",
:key "Psycho",
:value 105}
...]
|
Note algumas coisas interessantes sobre o código na Listagem 22. Primeiro, é um documento CouchDB semelhante a tudo que já foi armazenado no CouchDB até agora. O que o diferencia é que é armazenado em um documento com nome especial, _design/movies nesse caso. Os documentos de design do CouchDB são documentos que contêm visualizações. Seus nomes começam com _design. Os documentos de design têm uma propriedade language (nesse caso Clojure) e uma propriedade views , que contém um mapa de visualizações específicas encontradas no documento de design. Quando a Listagem 21 chama save-view via Clutch, os primeiros dois parâmetros definem o documento de design e o nome da visualização. Essa seção de visualização do mapa destina-se a abrigar muitas consultas relacionadas. A visualização runtimes tem um mapa associado a ela, que se assemelha à função original definida por meio da API do Clutch. Agora, foi criada uma visualização, ao criar um documento com nome especial no CouchDB. A segunda parte da Listagem 22 obtém os resultados da visualização que usa as URLs especiais.
Consultando itens em uma visualização
Os exemplos anteriores mostram como recuperar todos os resultados retornados por uma visualização. Isso é útil, mas talvez não seja o que se deseja. Adote o raciocínio original por trás da seleção do título do filme como chave para o banco de dados. Parece razoável querer consultar o banco de dados de filmes pelos títulos, mesmo que estes não sejam exclusivos. Com esse objetivo em mente, pode-se criar uma visualização que retorne o documento completo sobre o filme, tendo como chave o título do filme. Isso é semelhante ao código já visto, só que desta vez, em vez de retornar um único número, retornaremos o documento completo. A Listagem 23 mostra o código para criar e consultar uma visualização por título do filme:
Listagem 23. Consulta por título de filme com Clutch
user> (clutch/with-db movies-db
(clutch/save-view "movies" "by_title"
(clutch/with-clj-view-server
{:map (fn [doc] (when (and (:movie-title doc)
(:runtime doc))
[[(:movie-title doc)
doc]]))})))
user> (clutch/with-db movies-db
(:rows (clutch/get-view "movies" "by_title" {:key "Psycho"})))
[{:id "d6993381eb5ede34fded2f018b9f10b0",
:key "Psycho",
:value {:_id "d6993381eb5ede34fded2f018b9f10b0",
:movie-title "Psycho",
:director "Gus Van Sant",
...}
...}]
|
A única diferença entre consultar um filme específico e consultar todos os filmes é um parâmetro de consulta. O código Clutch na Listagem 23 usa um mapa de parâmetro de consulta que resulta apenas em uma cadeia de caractere de consulta na URL, como no caso da consulta de clj-http na Listagem 24:
Listagem 24. Consulta por título de filme com clj-http
user> (->> "\"Psycho\""
java.net.URLEncoder/encode
(str movies-db "/_design/movies/_view/by_title?key=")
client/get
:body
json/read-json
:rows)
[{:id "d6993381eb5ede34fded2f018b9f10b0",
:key "Psycho",
:value {...}
...}]
|
O CouchDB possui diversas opções de consulta, inclusive classificação em ordem crescente/decrescente, intervalos de chaves e limites. As chaves também podem ser outras estruturas de JSON, como uma lista ou um mapa. Para obter mais informações sobre visualizações do CouchDB, veja Recursos.
Consultar dados por meio de visualizações usando apenas funções de mapa provavelmente abrange a maioria das necessidades de desenvolvedores. Mas, às vezes, queremos obter informações agregadas. Médias, somas e outros tipos de resumos de dados não são possíveis apenas com uma função de mapa. O CouchDB fornece uma função de redução com esse objetivo. A Listagem 25 mostra um exemplo de criação de visualização, que mostra o número total de filmes no banco de dados para determinado estúdio:
Listagem 25. Visualização com função de redução
user> (clutch/with-db movies-db
(clutch/save-view "movies" "studio"
(clutch/with-clj-view-server
{:map (fn [doc] (when (:studio doc)
[[(:studio doc) 1]]))
:reduce (fn [keys vals rereduce]
(if rereduce
(apply + vals)
(count vals)))})))
user> (clutch/with-db movies-db
(clutch/get-view "movies" "studio"))
{:rows [{:key nil, :value 6}]}
user> (clutch/with-db movies-db
(clutch/get-view "movies" "studio" {:key "Paramount"}))
{:rows [{:key nil, :value 3}]}
|
A função reduce mostrada na Listagem 25 assume três argumentos:
- O primeiro,
keys, é uma lista das chaves criadas na funçãomap. Note que a lista de chaves consiste não apenas nostudioemitido na Listagem 25, mas no studio e no ID. - O segundo argumento,
vals, é uma lista dos valores para as chaves que foram transferidas para a função. Nesse caso, é uma série daqueles que foram emitidos. - O terceiro argumento,
rereduce, tem a ver com o fato de essa funçãoreduceestar operando ou não com base em informações agregadas ou em resultados brutos do mapa (aqueles da funçãomap).
É necessário entender um pouco como o CouchDB armazena esses resultados para compreender essa função reduce . Os resultados da chamada de redução são armazenados em uma B-Tree; quanto mais próximos da raiz estiverem os dados na B-Tree, de mais alto será o nível do resumo. A primeira chamada para a visualização studio retorna 6. Essa é a visualização a partir da raiz dessa árvore. Se você tiver emitido as chaves nesse ponto, também será possível ver um par de [studio doc-id]para cada documento no banco de dados. Ao percorrer mais a raiz pela árvore, outros resumos (inferiores ao resumo na raiz) podem ser emitidos. A segunda chamada na Listagem 25 para a visualização solicita filmes do estúdio "Paramount" . Ela percorrerá a árvore (em tempo logarítmico) e encontrará a soma mais próxima do nó da raiz de filmes daquele estúdio. Essa estrutura é voltada para o desempenho. Quando os dados mudam ou os valores precisam ser computados, os resumos podem ser usados sem necessariamente executar novamente todos os cálculos. Essa estrutura também está por trás do parâmetro rereduce . O parâmetro rereduce é verdadeiro quando os filhos do nó que está sendo calculado já foram calculados (e, nesse exemplo, não são apenas os valores 1 individuais).
O CouchDB pode ser escalado para muitas instâncias e ser replicado de forma incremental entre os vários nós do cluster. Tudo isso é desenvolvido sobre a capacidade de replicação do CouchDB, o que é útil até mesmo fora da escalada de bancos de dados distribuídas do CouchDB. Replicar dados no CouchDB é um processo em uma só etapa da perspectiva da API. A replicação pode ocorrer em bancos de dados locais, remotos, ou em qualquer combinação destes. O CouchDB torna isso mais fácil ao conceder a capacidade de replicar em nível de banco de dados usando a mesma interface REST usada até agora. Vamos nos concentrar aqui no suporte à replicação do CouchDB e no uso dele de forma programática, em vez de em como ele pode ser aplicado à escalada no CouchDB. (Para obter mais informações sobre como escalar o CouchDB, veja Recursos.)
A replicação do CouchDB ocorre entre dois bancos de dados existentes (locais ou remotos). Não há requisitos de linhagem entre os dois bancos de dados antes da replicação. A replicação pode ser feita por meio do aplicativo Futon ou de forma programática. Digamos que você esteja experimentando alguns problemas de desempenho e precise adicionar uma segunda instância do seu banco de dados CouchDB para acompanhar a demanda. Para facilitar os testes, pode-se simplesmente replicar em outro banco de dados local, como mostrado na Listagem 26:
Listagem 26. Exemplo de replicação com Clutch
user> (def moviesb-db "http://localhost:5984/movies-b")
#'user/moviesb-db
user> (clutch/create-database moviesb-db)
{:ok true,...}
user> (clutch/replicate-database "movies" "movies-b")
{:ok true...}
user> (let [movie-ids (clutch/with-db movies-db
(->> (clutch/get-all-documents-meta)
:rows
(map :id)))
movie-b-ids (clutch/with-db moviesb-db
(->> (clutch/get-all-documents-meta)
:rows
(map :id)))]
(= movie-ids movie-b-ids))
true
|
O código na Listagem 26 cria um banco de dados (vazio) chamado movies-b (porque dois bancos de dados não podem ter o mesmo nome) e depois replica o banco de dados movies nele. Então, ele recebe todos os IDs de documento e confirmam se são iguais. Visto que a replicação acabou de ser feita, devem ser. A Listagem 27 é o mesmo exemplo, usando clj-http:
Listagem 27. Exemplo de replicação com clj-http
user> (def moviesb-db "http://localhost:5984/movies-b")
#'user/moviesb-db
user> (client/put moviesb-db)
{:ok true,...}
user> (->> {:source "movies" :target "movies-b"}
json/json-str
(hash-map :body)
(client/post "http://localhost:5984/_replicate"))
{:ok true...}
user> (let [movie-ids (->> (str movies-db "/_all_docs")
client/get
:body
json/read-json
:rows
(map :id))
movie-b-ids (->> (str moviesb-db "/_all_docs")
client/get
:body
json/read-json
:rows
(map :id))]
(= movie-ids movie-b-ids))
true
|
Na Listagem 27, postamos um documento JSON que contém origem e destino. O CouchDB continua a partir daí. Agora, temos as vantagens e desvantagens de duas cópias dos dados. Com a força extra de um segundo banco de dados, o CouchDB atende mais rapidamente às solicitações, mas o que acontece quando o banco de dados é alterado? Como exemplo, acrescente um novo filme a cada banco de dados e replique-os, como mostrado na Listagem 28:
Listagem 28. Acrescentando documentos novos a ambos os bancos de dados
user> (clutch/with-db movies-db
(clutch/create-document
{:movie-title "Vertigo"
:director "Alfred Hitchcock",
:runtime 128,
:year-released 1958,
:studio "Paramount Pictures"}))
{:_id "728b2293180e0be566cea3f3127b6cf3"...}
user> (clutch/with-db moviesb-db
(clutch/create-document
{:movie-title "North by Northwest"
:director "Alfred Hitchcock",
:runtime 131,
:year-released 1959,
:studio "MGM"}))
{:_id "386d0400e336e54933a47aec656289c4"...}
(clutch/replicate-database "movies" "movies-b")
(clutch/replicate-database "movies-b" "movies")
|
Após as primeiras duas instruções na Listagem 28, as duas cópias do banco de dados são diferentes. O banco de dados movies tem um documento Vertigo que movies-b não tem, e movies-b tem o documento North by Northwest , que movies não tem. Esses dois documentos possuem IDs diferentes (gerados) e representam filmes diferentes. Visto que os documentos são diferentes, a replicação é concluída sem problemas. Note que a replicação é apenas unidirecional. Ao replicar de movies para movies-b, nenhum dos documentos de movie-b é movido. De modo que é preciso replicar também de movies-b de volta para movies (para obter uma cópia do documento North by Northwest ). Para verificar se os novos documentos existem em ambos os bancos de dados, reutilize o código da Listagem 27 que compara as listas de documentos dos dois bancos de dados.
Resolvendo conflitos de replicação
O cenário descrito acima não apresenta problemas. Trata apenas de novos documentos, sem conflitos. Outro cenário sem problemas é a replicação de documentos que foram atualizados na origem, mas não no destino. O que acontece quando um documento é modificado em dois bancos de dados e depois replicado? Esse cenário pode causar problemas de replicação. O modo mais fácil de resolver esse problema é projetar o banco de dados de forma a evitar atualizações conflitantes. Em um projeto de banco de dados orientado a documentos, é melhor tornar os documentos o mais autocontidos possível. O banco de dados também pode ser concebido de modo que as novas informações sejam colocadas em novos documentos, para evitar atualizações do documento? Isso nem sempre é possível, mas quando é, torna muito mais fácil a replicação. O exemplo na Listagem 29 acrescenta uma nova chave a um documento em cada banco de dados — uma com o ano de refilmagem e outra com informações sobre a mixagem de som do filme:
Listagem 29. Replicação conflitante
user> (clutch/with-db movies-db
(-> (clutch/get-document "386d0400e336e54933a47aec656289c4")
(clutch/update-document {:re-released 1996})))
{:re-released 1996
:movie-title "North by Northwest"...}
user> (clutch/with-db moviesb-db
(-> (clutch/get-document "386d0400e336e54933a47aec656289c4")
(clutch/update-document {:sound-mix "Mono"})))
{:sound-mix "Mono"
:movie-title "North by Northwest"...}
user> (clutch/replicate-database "movies" "movies-b")
{:ok true...}
|
Tudo parece ter sido replicado de forma adequada, até verificarmos os resultados na Listagem 30:
Listagem 30. Verificando replicação conflitante
user> (clutch/with-db moviesb-db
(keys (clutch/get-document "386d0400e336e54933a47aec656289c4")))
(:movie-title :director :_conflicts :_rev :language
:runtime :studio :_id :sound-mix :year-released)
|
A atualização de movies parece ter desaparecido, pois não há chave re-released . O que aconteceu foi, quando movies foi replicado com movies-b, ocorreu um conflito. Isso aconteceu porque foi feita uma tentativa de replicar revisões diferentes de um mesmo documento. Quando ocorre um conflito como esse com o CouchDB, nenhuma informação é perdida. Em vez disso, o Couch cria um novo registro de conflito associado ao documento, que pode ser recuperado com a consulta do Clutch mostrada na Listagem 31:
Listagem 31. Examinando um conflito (exemplo de Clutch)
user> (clutch/with-db moviesb-db
(clutch/get-document "386d0400e336e54933a47aec656289c4" {:conflicts true}))
{:movie-title "North by Northwest",
...
:_conflicts ["2-ac7e4d143dff32f7be437de99a659ba1"]
...}
|
A Listagem 32 mostra o código clj-http equivalente:
Listagem 32. Examinando um conflito (exemplo de clj-http)
user> (-> (str moviesb-db "/386d0400e336e54933a47aec656289c4?conflicts=true")
client/get
:body
json/read-json)
{:movie-title "North by Northwest"
...
:_conflicts ["2-ac7e4d143dff32f7be437de99a659ba1"]
...}
|
A Listagem 31 e a Listagem 32 mostram que ocorreu um conflito, e que se deve a uma revisão específica (nesse caso, 2-ac7e4d143dff32f7be437de99a659ba1) do documento. Pode-se, então, puxar essa revisão conflitante específica e atualizá-la pessoalmente. A Listagem 33 faz isso com o Clutch:
Listagem 33. Obtendo documento conflitante (Clutch)
user> (clutch/with-db moviesb-db
(keys (clutch/get-document "386d0400e336e54933a47aec656289c4"
{:rev "2-ac7e4d143dff32f7be437de99a659ba1"}
#{:rev})))
(:_id :_rev :re-released :movie-title :director
:runtime :year-released :studio)
|
A Listagem 34 mostra o código clj-http equivalente:
Listagem 34. Obtendo documento conflitante (clj-http)
user> (-> (str moviesb-db
"/386d0400e336e54933a47aec656289c4"
"?rev=2-ac7e4d143dff32f7be437de99a659ba1")
client/get
:body
json/read-json
keys)
(:re-released :movie-title :director :_rev :language
:runtime :studio :_id :year-released)
|
Observe que a lista keys não contém o par de chaves/valores sound-mix , mas contém o par de chaves/valores re-released . Cabe ao desenvolvedor resolver esses conflitos. Também, aplicam-se aqui as mesmas regras da seção Atualizando o documento CouchDB . Qualquer mudança no documento causa um novo ID de revisão, e pode criar um conflito. Até mesmo mudanças em diferentes áreas de um documento não serão resolvidas automaticamente. As etapas para resolver esse problema são:
- Ler o documento atual.
- Ler a versão mais antiga (conflitante)
- Aplicar a lógica de mesclagem específica do domínio.
- Atualizar o documento para a versão nova (mesclada).
- Remover a versão de documento conflitante.
A etapa 5 é a mesma que excluir qualquer outro documento, com o parâmetro de revisão adicional servindo de espelho a como o documento foi recuperado.
O ponto-chave sobre conflitos de replicação é o fato de que o tratamento de erros é específico de domínio. No caso desse exemplo, queremos mesclar as atualizações, combinando os campos de título alternativo. Há muitas alternativas a essa lógica. Outra opção é sempre usar a atualização mais recente. Isso faria sentido em documentos como receita do filme, pois provavelmente seria interessante ter apenas as informações mais recentes sobre a receita de um filme. Em outros casos, talvez seja melhor obter a primeira atualização vencedora.
O formato simples de documento JSON, as APIs REST e o ótimo suporte a Clojure do Clutch são fortes razões para o uso do Clojure para acessar o CouchDB. A habilidade de escrever visualizações do CouchDB no Clojure significa que precisamos de uma linguagem a menos para oferecer suporte em um aplicativo, e temos maior continuidade no código. A abstração fornecida pelo Clutch, juntamente com o conhecimento sólido dos pontos básicos do REST no CouchDB podem resultar em um aplicativo com desenvolvimento rápido e de fácil manutenção, baseado em CouchDB.
Aprender
-
CouchDB - The Definitive Guide (J. Chris Anderson, Jan Lehnardt e Noah Slater, O'Reilly Media, January 2010): Explore esse livro (disponível de graça em versão on-line) para obter informações independentes de linguagem no CouchDB. Consulte o Capítulo 19 mais informações sobre cluster no CouchDB.
-
"Introducing clj-http, a Clojure HTTP Client" (Mark McGranaghan, agosto de 2010): Uma introdução a clj-http.
-
CouchDB Wiki: Acesse o site do CouchDB do Apache, incluindo as seções sobre instalação do CouchDB, Futon e a API de Visualização em HTTP
-
Clojure: Veja a documentação on-line do Clojure.
-
"A linguagem de programação Clojure" (Michael Galpin, developerWorks, setembro de 2009): inicie com o Clojure, aprenda sobre sua sintaxe e tire proveito do plug-in Clojure do Eclipse.
-
"Desenvolvimento Java 2.0: REST com CouchDB e o RESTClient do Groovy: " (Andrew Glover, developerWorks, novembro de 2009): Saiba mais sobre pontos básicos do CouchDB e sobre como trabalhar com o CouchDB a partir de Groovy.
-
"Série de podcasts técnicos da zona de tecnologia Java" (Andrew Glover, developerWorks): Essa série de podcasts técnicos sobre Java e tecnologias relacionadas inclui entrevistas com Stuart Halloway sobre o Clojure, e Aaron Miller e Nitin Borwankar sobre CouchOne.
-
Clutch on Github: Veja o arquivo README para obter instruções de configuração do servidor e alguns exemplos de uso.
-
Navegue pela livraria de tecnologia para ver livros sobre este e outros tópicos técnicos.
-
developerWorks Java technology: Encontre centenas de artigos sobre quase todos os aspectos da programação Java.
Obter produtos e tecnologias
-
CouchDB: Obtenha o CouchDB.
-
Clojure: Faça o download do Clojure.
-
clj-http on Github: Faça o download do clj-http.
-
Clutch: Faça o download do Clutch.
-
Leiningen: Faça o download da ferramenta de desenvolvimento Leiningen para Clojure.
Discutir
- Participe da comunidade do developerWorks. Entre em contato com outros usuários do developerWorks, enquanto explora os blogs, fóruns, grupos e wikis orientados ao desenvolvedor.
Ryan Senior é engenheiro senior da Revelytix e desenvolve software semântico para a Web usando Clojure. Anteriormente, ele trabalhou como desenvolvedor Java em diversos segmentos de mercado, incluindo os segmentos de manufatura, financeiro e de assistência médica. Ele tem mestrado em Ciência da Computação pela Universidade de Illinois em Urbana-Champaign e bacharelado em Ciência da Computação pela Universidade de Illinois Ocidental. Ryan também é membro da equipe principal Strange Loop . Siga-o no Twitter em @objcmdo e no blog Object Commando.