Estou muito feliz em informar que o Couchbase agora tem um SDK Scala compatível, permitindo que você obtenha e busque documentos, execute consultas N1QL, faça análises e pesquisas de texto completo - tudo com Scala nativo.
Neste blog, abordarei os principais recursos e princípios de design do Scala SDK. Ou, se você quiser começar imediatamente, dê uma olhada no guia de introdução aqui. O Scala SDK está disponível para download agora mesmo, em uma versão alfa de pré-lançamento.
O Scala SDK usa o mesmo núcleo interno estável e de alto desempenho do novo Java SDK 3.x, portanto, embora seja novo, você pode ter certeza de que estará pronto para a produção no GA.
Tratamento funcional de erros - sem exceções!
O SDK do Scala apresenta uma interface funcional, com a maioria das operações retornando um Scala Try. Ele pode ser um Success e conter o valor esperado, ou um Failure contendo um Throwable. Embora a princípio isso pareça um pouco prolixo e exija alguma disciplina de programação para sempre tratar os erros, essa forma de tratamento de erros rapidamente se torna uma segunda natureza e permite que você desenvolva com segurança, sabendo que uma exceção aleatória não será lançada pelo SDK.
Aqui está uma demonstração simples de como inserir um documento:
|
1 2 3 |
val json = JsonObject("status" -> "Incrível!") val resultado: Tente[Resultado da mutação] = coleção.inserir("document-key" (chave do documento), json) |
E, em seguida, a correspondência de padrões no resultado do Try:
|
1 2 3 4 5 |
resultado partida { caso Sucesso(senhor) => println("Upsert foi bem-sucedido") caso Falha(erro: KeyAlreadyExistsException) => println("document-key already exists" (a chave do documento já existe)) caso Falha(erro) => println("Error: " + erro) } |
(É claro que um aplicativo de produção trataria os erros adequadamente, em vez de usar apenas println!)
Se você preferir algo semelhante ao Java SDK, que lança exceções, isso é fácil de simular, bastando chamar .get() em qualquer Try, que será lançado se for uma falha:
|
1 |
val resultado: Resultado da mutação = coleção.inserir("document-key" (chave do documento), json).obter() |
É fácil combinar vários Trys por meio do flatMap. Aqui, inserimos um documento, depois o obtemos, recuperamos seu conteúdo convertido na biblioteca JSON integrada JsonObjectSafe (falaremos mais sobre isso posteriormente) e, por fim, obtemos um campo chamado "status" a partir dele. Tudo isso em um estilo totalmente funcional, de modo que, por exemplo, a operação get só será tentada se o upsert for bem-sucedido.
|
1 2 3 4 5 6 7 8 9 |
val resultado: Tente[Cordas] = coleção.upsert("document-key" (chave do documento), json) .flatMap(_ => coleção.obter("document-key" (chave do documento))) .flatMap(_.contentAs[JsonObjectSafe]) .flatMap(_.str("status")) resultado partida { caso Sucesso(status) => println(s"O Couchbase é $status") caso Falha(erro) => println("Error: " + erro) } |
(Para fins de concisão, a maioria dos exemplos abaixo deixará de lado a verificação do resultado. Mas os aplicativos de produção devem, é claro, ABC - sempre estar verificando).
Visão de futuro
Imediatamente, o SDK é compatível com a nova durabilidade mais forte que estará disponível no Couchbase Server 6.5:
|
1 |
val resultado = coleção.upsert("docId", json, durabilidade = Durabilidade.Maioria) |
E oferece suporte a coleções, um novo método de organização de documentos dentro de compartimentos que será um recurso fundamental de uma versão futura do Couchbase Server (com uma visualização em breve). Em breve, o usuário poderá criar algumas coleções de brinquedo para brincar, mas, por enquanto, será suficiente abrir a coleção padrão:
|
1 2 |
val agrupamento = Aglomerado.conectar("cluster-ip", "nome de usuário", "senha") val coleção = agrupamento.balde("bucket-name" (nome do balde)).defaultCollection |
Essa lógica funcionará em todas as versões compatíveis do Couchbase Server (5.x e superior).
Além disso, está incluído o suporte ao OpenTracing (agora OpenTelemetry), a plataforma que está se tornando o padrão de fato para o rastreamento de sistemas distribuídos complexos:
|
1 |
val resultado = coleção.upsert("docId", json, parentSpan = Alguns(mySpan)) |
API simples
O SDK utiliza parâmetros nomeados e padrão em vez de sobrecargas, de modo que seu código pode ser tão simples quanto:
|
1 |
val resultado = coleção.upsert("docId", json) |
ou conforme personalizado:
|
1 |
val resultado = coleção.upsert("docId", json, durabilidade = Durabilidade.Maioria, expiração = 60 minutos, parentSpan = Alguns(mySpan)) |
como você precisa que seja.
Assíncrono
Assim como o Java SDK, são apresentadas três APIs, o que lhe dá flexibilidade para estruturar seu aplicativo da maneira que precisar.
A primeira é a API síncrona de bloqueio simples que você viu nos exemplos acima. Essa pode ser uma boa opção padrão, mas é claro que exige que você gerencie explicitamente seu próprio threading, se necessário.
A segunda é aquela criada com base na programação reativa, usando tipos reativos padrão (Mono e Flux) do Project Reactor.
Um exemplo de upserting de forma reativa:
|
1 2 3 |
val mono: Mono[Resultado da mutação] = coleção.reativo.upsert("document-key" (chave do documento), json) .doOnError(erro => println(s"Erro durante a inserção: ${err}")) .doOnNext(resultado da mutação => println("Sucesso")) |
Com a programação reativa, nada acontecerá até que assinemos o Mono. A maneira mais simples de fazer isso para fins de teste é usar block(), embora essa seja uma prática ruim para um aplicativo real:
|
1 |
val resultado: Resultado da mutação = mono.bloco() |
A programação reativa é um assunto complexo e profundo que não posso abordar muito aqui, mas, quando dominada, é uma ferramenta poderosa, especialmente para lidar com as complexidades dos sistemas distribuídos do mundo real. Em particular, a API reativa oferece contrapressão em consultas N1QL, FTS e analíticas; portanto, se o aplicativo estiver com dificuldades para acompanhar os dados, ele reduzirá automaticamente o consumo de linhas e evitará erros de falta de memória. Incentivo os curiosos a dar uma olhada na Documentação do Project Reactor para saber mais.
Por fim, há a API assíncrona criada com base no Scala Futures, que fornece um pouco do gerenciamento de threads e da capacidade de composição da programação reativa, mas em uma forma mais familiar para muitos desenvolvedores do Scala. Um exemplo simples de upsert tem a seguinte aparência:
|
1 2 3 4 5 6 |
val resultado: Futuro[Resultado da mutação] = coleção.assíncrono.upsert("document-key" (chave do documento), json) resultado onComplete { caso Sucesso(_) => println("Upserted successfully" (inserido com sucesso)) caso Falha(exceção) => println("Error: " + exceção) } |
Flexibilidade do JSON
Um dos principais objetivos era não prender você a nenhuma implementação JSON específica e, pronto para uso, o Scala SDK funcionará bem com várias bibliotecas Scala JSON populares, incluindo Circe, µPickle / µJson, Json4s e Jawn. Você pode ver trechos de como usar cada uma delas em os documentos JSONMas, para abrir seu apetite, aqui está um pouco do Circe em ação - observe como os tipos do Circe são enviados e retornados diretamente do SDK:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
importação io.circo.genérico.automático._ importação io.circo.sintaxe._ val usuário = Usuário("João Smith", 29) val json: io.circo.Json = usuário.asJson val resultado: Tente[io.circo.Json] = coleção.inserir("id", json) .flatMap(_ => coleção.obter("id")) .flatMap(doc => doc.contentAs[io.circo.Json]) resultado partida { caso Sucesso(conteúdo: io.circo.Json) => // Manipule o Circe Json aqui caso Falha(erro) => println("Error: " + erro) } |
JSON incorporado
Adotando uma abordagem de "baterias incluídas", há também uma biblioteca JSON simples incluída no SDK, a JsonObject. Seus principais objetivos são:
- Conveniência. Nem todo mundo quer avaliar várias bibliotecas JSON antes de começar. O JsonObject é uma opção padrão decente.
- Velocidade. Nosso benchmarking interno (fonte caso queira executá-los você mesmo) indica que o JsonObject pode ser até 20 vezes mais rápido do que a biblioteca Scala JSON mais próxima em algumas operações importantes. Ele consegue isso principalmente por ser construído em torno de estruturas de dados JVM simples, mas muito rápidas e mutáveis.
- Flexibilidade. A interface padrão JsonObject lança exceções (o único lugar no SDK que faz isso). Ou você pode facilmente convertê-la em uma JsonObjectSafe, que fornece uma interface funcional baseada em tentativas.
- Facilidade de uso e mutabilidade. Estamos de acordo com o autor de µJson que, embora a imutabilidade seja geralmente desejável, nem sempre é a melhor opção no caso específico do JSON. Lidar com JSON profundamente aninhado requer ferramentas funcionais, como lentes, que raramente são fáceis de ler e usar, sem mencionar o fato de que possivelmente incorrem em uma penalidade de desempenho. E o JSON é, na maioria das vezes, tratado de forma breve e em um escopo limitado (por exemplo, obter e modificar um documento), portanto, raramente se beneficia da segurança da imutabilidade. Portanto, o JsonObject apresenta uma API mutável simples.
Você pode se aprofundar no Documentos sobre JsonObject aqui. É claro que, se você preferir uma biblioteca JSON mais "tipicamente Scala" com dados imutáveis, tipos de dados algébricos, lentes, cursores e outros recursos funcionais, então uma das outras bibliotecas suportadas pode ser uma escolha melhor - a escolha é sua, e você pode facilmente misturar e combinar várias bibliotecas JSON no mesmo aplicativo.
Suporte direto à classe de casos
Embora o SDK do Scala tenha um ótimo suporte para JSON, às vezes é mais fácil e preferível trabalhar diretamente com as classes de caso do Scala.
Basta adicionar essa pequena parte do boilerplate à sua classe de caso para que o SDK possa gerar automaticamente a lógica de serialização:
|
1 2 3 4 5 |
caso classe Usuário(nome: Cordas, idade: Int) objeto Usuário { implícita val codec: Codec[Usuário] = Codecs.codec[Usuário] } |
E então você pode facilmente enviar essa classe de caso diretamente para o SDK e recebê-la de volta:
|
1 2 3 |
val r: Tente[Usuário] = coleção.inserir("docId", Usuário("João Smith", 29)) .flatMap(_ => coleção.obter("docId") .flatMap(doc => doc.contentAs[Usuário]) |
Por trás disso, isso é convertido e armazenado no Couchbase Server como o JSON que você espera:
|
1 |
{"name" (nome):"John Smith","idade":29} |
Trata-se de um JSON simples e comum, sem metadados extras de serialização ou similares, de modo que pode ser escrito livremente como uma classe de caso e lido em uma biblioteca JSON ou por outro SDK, ou vice-versa.
Confira o documentação sobre o uso de classes de casos para saber mais.
Consultas fáceis
Facilitamos ao máximo o uso do Couchbase Analytics, N1QL e Full Text Search. Aqui está um exemplo de uma consulta N1QL em que as linhas são convertidas em JsonObject.
|
1 2 3 4 5 6 7 8 |
agrupamento.consulta("""select * from `travel-sample` limit 10;""") .flatMap(_.todas as linhas[JsonObject]) partida { caso Sucesso(linhas: Sequência[JsonObject]) => linhas.antes de(fila => println(fila)) caso Falha(erro) => println(s"Error: $err") } |
E, é claro, é possível obter esses resultados como qualquer um dos tipos de JSON suportados acima ou diretamente como classes de caso do Scala.
E, conforme mencionado acima, o uso da API reativa significa que você obtém a contrapressão automática gratuitamente, garantindo que seu aplicativo consuma linhas em uma taxa gerenciável e não tenha problemas de falta de memória em consultas muito grandes.
Se você está animado para começar a usar o SDK do Couchbase Scala, confira o guia de início rápido. Ele está disponível agora mesmo, embora em uma forma alfa inicial. Pode haver algumas mudanças significativas antes de ir para o GA, mas, por outro lado, este é o momento perfeito para experimentá-lo e nos dar seu feedback, deixando um comentário abaixo ou enviando-o para Nossos fóruns, Twitter ou gitter. Nada é definitivo e agradecemos a chance de fazer algumas alterações e tornar este SDK o melhor possível.