Consultando o Couchbase com o N1QL
1. Visão geral
Neste artigo, veremos como consultar um servidor Couchbase comN1QL. De maneira simplificada, trata-se de bancos de dados SQL para NoSQL - com o objetivo de facilitar a transição dos bancos de dados SQL / Relacionais para um sistema de banco de dados NoSQL.
Existem algumas maneiras de interagir com o servidor Couchbase; aqui, usaremos o Java SDK para interagir com o banco de dados - como é típico para aplicativos Java.
Leitura adicional:
Introdução ao Spring Data Couchbase
Rápido e prático sobre o uso do Spring Data Couchbase para interagir com um servidor de banco de dados do Couchbase.
Operações de lote assíncronas no Couchbase
Aprenda a executar operações em lote eficientes no Couchbase usando a API Java assíncrona do Couchbase.
Introdução ao SDK do Couchbase para Java
Uma introdução rápida e prática ao uso do Java Couchbase SDK.
2. Dependências do Maven
Presumimos que um servidor Couchbase local já foi configurado; se não for esse o caso, esteguide pode ajudá-lo a começar.
Vamos agora adicionar a dependência para Couchbase Java SDK apom.xml:
com.couchbase.client
java-client
2.5.0
A versão mais recente do Couchbase Java SDK pode ser encontrada emMaven Central.
Também usaremos a biblioteca Jackson para mapear os resultados retornados de consultas; vamos adicionar sua dependência apom.xml também:
com.fasterxml.jackson.core
jackson-databind
2.9.1
A versão mais recente da biblioteca Jackson pode ser encontrada emMaven Central.
3. Conectando a um servidor Couchbase
Agora que o projeto está configurado com as dependências certas, vamos nos conectar ao Couchbase Server a partir de um aplicativo Java.
Primeiro, precisamos iniciar o Couchbase Server - se ainda não estiver em execução.
Um guia para iniciar e parar um Couchbase Server pode ser encontradohere.
Vamos conectar a um CouchbaseBucket:
Cluster cluster = CouchbaseCluster.create("localhost");
Bucket bucket = cluster.openBucket("test");
O que fizemos foi conectar ao CouchbaseClustere obter o objetoBucket.
O nome do bucket no cluster Couchbase éteste pode ser criado usando o Couchbase Web Console. Quando terminarmos com todas as operações do banco de dados, podemos fechar o intervalo específico que abrimos.
Por outro lado, podemos desconectar do cluster - o que acabará por fechar todos os buckets:
bucket.close();
cluster.disconnect();
4. Inserindo documentos
O Couchbase é um sistema de banco de dados orientado a documentos. Vamos adicionar um novo documento ao intervalotest:
JsonObject personObj = JsonObject.create()
.put("name", "John")
.put("email", "[email protected]")
.put("interests", JsonArray.from("Java", "Nigerian Jollof"));
String id = UUID.randomUUID().toString();
JsonDocument doc = JsonDocument.create(id, personObj);
bucket.insert(doc);
Primeiro, criamos um JSONpersonObje fornecemos alguns dados iniciais. As chaves podem ser vistas como colunas em um sistema de banco de dados relacional.
A partir do objeto de pessoa, criamos um documento JSON usandoJsonDocument.create(), que inseriremos no intervalo. Observe que geramos umid aleatório usando a classejava.util.UUID.
O documento inserido pode ser visto no Couchbase Web Console emhttp://localhost:8091 ou chamandobucket.get() com seuid:
System.out.println(bucket.get(id));
5. Consulta N1QLSELECT básica
O N1QL é um superconjunto do SQL e sua sintaxe, naturalmente, parece semelhante.
Por exemplo, o N1QL para selecionar todos os documentos emtest bucket é:
SELECT * FROM test
Vamos executar esta consulta no aplicativo:
bucket.bucketManager().createN1qlPrimaryIndex(true, false);
N1qlQueryResult result
= bucket.query(N1qlQuery.simple("SELECT * FROM test"));
Primeiro, criamos um índice primário usandocreateN1qlPrimaryIndex(), ele será ignorado se tiver sido criado antes; criá-lo é obrigatório antes que qualquer consulta possa ser executada.
Em seguida, usamosbucket.query() para executar a consulta N1QL.
N1qlQueryResult é um objetoIterable<N1qlQueryRow> e, portanto, podemos imprimir todas as linhas usandoforEach():
result.forEach(System.out::println);
A partir doresult retornado, podemos obter o objetoN1qlMetrics chamandoresult.info(). No objeto métricas, podemos obter informações sobre o resultado retornado - por exemplo, o resultado e a contagem de erros:
System.out.println("result count: " + result.info().resultCount());
System.out.println("error count: " + result.info().errorCount());
Noresult retornado, podemos usarresult.parseSuccess() para verificar se a consulta está sintaticamente correta e analisada com êxito. Podemos usarresult.finalSuccess() para determinar se a execução da consulta foi bem-sucedida.
6. Instruções de consulta N1QL
Vamos dar uma olhada nas diferentes instruções de consulta N1QL e nas diferentes maneiras de executá-las por meio do Java SDK.
6.1. Declaração deSELECT
A instruçãoSELECT em NIQL é semelhante a um SQLSELECT padrão. Consiste em três partes:
-
SELECT– define a projeção dos documentos a serem devolvidos
-
FROM– descreve o keyspace do qual buscar os documentos; keyspace é sinônimo de nome de tabela em sistemas de banco de dados SQL
-
WHERE– especifica os critérios de filtragem adicionais
O Couchbase Server vem com alguns buckets de amostra (bancos de dados). Se eles não foram carregados durante a configuração inicial, a seçãoSettings do console da Web tem uma guia dedicada para configurá-los.
Estaremos usando o intervalotravel-sample. O intervalotravel-sample contém dados de companhias aéreas, pontos de referência, aeroportos, hotéis e rotas. O modelo de dados pode ser encontradohere.
Vamos selecionar 100 registros de companhias aéreas a partir dos dados de amostra de viagem:
String query = "SELECT name FROM `travel-sample` " +
"WHERE type = 'airport' LIMIT 100";
N1qlQueryResult result1 = bucket.query(N1qlQuery.simple(query));
A consulta N1QL, como pode ser visto acima, é muito semelhante ao SQL. Observe que o nome do espaço da chave deve ser colocado em backtick (`) porque contém um hífen.
N1qlQueryResult é apenas um invólucro em torno dos dados JSON brutos retornados do banco de dados. Ele estendeIterable<N1qlQueryRow>e pode ser executado em loop.
Invocarresult1.allRows() retornará todas as linhas em um objetoList<N1qlQueryRow>. Isso é útil para processar resultados com a APIStream e / ou acessar cada resultado via índice:
N1qlQueryRow row = result1.allRows().get(0);
JsonObject rowJson = row.value();
System.out.println("Name in First Row " + rowJson.get("name"));
Obtemos a primeira linha dos resultados retornados e usamosrow.value() para obter umJsonObject - que mapeia a linha para um par de valores-chave, e a chave corresponde ao nome da coluna.
Portanto, obtivemos o valor da colunaname, para a primeira linha usandoget(). É tão fácil assim.
Até agora, estamos usando uma consulta N1QL simples. Vejamos a instruçãoparameterized em N1QL.
Nesta consulta, vamos usar o símbolo curinga (*) para selecionar todos os campos nos registrostravel-sample, ondetype é umairport.
Otype será passado para a instrução - como um parâmetro. Em seguida, processamos o resultado retornado:
JsonObject pVal = JsonObject.create().put("type", "airport");
String query = "SELECT * FROM `travel-sample` " +
"WHERE type = $type LIMIT 100";
N1qlQueryResult r2 = bucket.query(N1qlQuery.parameterized(query, pVal));
Criamos um JsonObject para armazenar os parâmetros como um par de valores-chave. O valor da chave ‘type', no objetopVal, será usado para substituir o marcador$type na stringquery.
N1qlQuery.parameterized() aceita uma string de consulta que contém um ou mais marcadores de posição e umJsonObject conforme demonstrado acima.
No exemplo de consulta anterior acima, selecionamos apenas uma coluna -name.. Isso torna mais fácil mapear o resultado retornado emJsonObject.
Mas agora que usamos o curinga (*) na instrução select, isso não é tão simples. O resultado retornado é uma sequência JSON bruta:
[
{
"travel-sample":{
"airportname":"Calais Dunkerque",
"city":"Calais",
"country":"France",
"faa":"CQF",
"geo":{
"alt":12,
"lat":50.962097,
"lon":1.954764
},
"icao":"LFAC",
"id":1254,
"type":"airport",
"tz":"Europe/Paris"
}
},
Portanto, precisamos de uma maneira de mapear cada linha para uma estrutura que nos permita acessar os dados especificando o nome da coluna.
Portanto, vamos criar um método que aceitaráN1qlQueryResulte mapeará todas as linhas do resultado para um objetoJsonNode.
EscolhemosJsonNode porque ele pode lidar com uma ampla gama de estruturas de dados JSON e podemos navegar facilmente:
public static List extractJsonResult(N1qlQueryResult result) {
return result.allRows().stream()
.map(row -> {
try {
return objectMapper.readTree(row.value().toString());
} catch (IOException e) {
logger.log(Level.WARNING, e.getLocalizedMessage());
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
Processamos cada linha no resultado usando a APIStream. Mapeamos cada linha para um objetoJsonNode e, em seguida, retornamos o resultado como umList deJsonNodes.
Agora podemos usar o método para processar o resultado retornado da última consulta:
List list = extractJsonResult(r2);
System.out.println(
list.get(0).get("travel-sample").get("airportname").asText());
A partir da saída JSON de exemplo mostrada anteriormente, cada linha tem uma chave correlacionada ao nome do keyspace especificado na consultaSELECT - que étravel-sample neste caso.
Portanto, obtivemos a primeira linha do resultado, que é aJsonNode. Em seguida, atravessamos o nó para chegar à chaveairportname, que é então impressa como um texto.
O exemplo de saída JSON bruta compartilhada anteriormente fornece mais clareza conforme a estrutura do resultado retornado.
6.2. InstruçãoSELECT usando N1QL DSL
Além de usar literais de cadeia bruta para criar consultas, também podemos usar o DS1 N1QL que vem com o Java SDK que estamos usando.
Por exemplo, a consulta de cadeia acima pode ser formulada com o DSL assim:
Statement statement = select("*")
.from(i("travel-sample"))
.where(x("type").eq(s("airport")))
.limit(100);
N1qlQueryResult r3 = bucket.query(N1qlQuery.simple(statement));
O DSL é fluente e pode ser interpretado facilmente. As classes e métodos de seleção de dados estão na classecom.couchbase.client.java.query.Select.
Métodos de expressão comoi(), eq(), x(), s() estão na classecom.couchbase.client.java.query.dsl.Expression. Leia mais sobre o DSLhere.
N1QL select statements can also have OFFSET, GROUP BY and ORDER BY clauses. A sintaxe é muito parecida com a do SQL padrão e sua referência pode ser encontradahere.
A cláusulaWHERE de N1QL pode aceitar Operadores lógicosAND,OR eNOT em suas definições. Além disso, o N1QL tem provisão para operadores de comparação como>, ==,! =,IS NULLeothers.
Existem também outros operadores que facilitam o acesso a documentos armazenados - ostring operators pode ser usado para concatenar campos para formar uma única string e onested operators pode ser usado para dividir matrizes e selecionar campos ou elementos.
Vamos ver isso em ação.
Esta consulta seleciona a colunacity, concatena as colunasairportnameefaa comoportname_faa do intervalotravel-sample onde a colunacountry termina com‘States' ', e olatitude do aeroporto é maior ou igual a 70:
String query2 = "SELECT t.city, " +
"t.airportname || \" (\" || t.faa || \")\" AS portname_faa " +
"FROM `travel-sample` t " +
"WHERE t.type=\"airport\"" +
"AND t.country LIKE '%States'" +
"AND t.geo.lat >= 70 " +
"LIMIT 2";
N1qlQueryResult r4 = bucket.query(N1qlQuery.simple(query2));
List list3 = extractJsonResult(r4);
System.out.println("First Doc : " + list3.get(0));
Podemos fazer a mesma coisa usando o N1QL DSL:
Statement st2 = select(
x("t.city, t.airportname")
.concat(s(" (")).concat(x("t.faa")).concat(s(")")).as("portname_faa"))
.from(i("travel-sample").as("t"))
.where( x("t.type").eq(s("airport"))
.and(x("t.country").like(s("%States")))
.and(x("t.geo.lat").gte(70)))
.limit(2);
N1qlQueryResult r5 = bucket.query(N1qlQuery.simple(st2));
//...
Vejamos outras declarações em N1QL. Vamos construir com base no conhecimento que adquirimos nesta seção.
6.3. Declaração deINSERT
A sintaxe para a instrução insert no N1QL é:
INSERT INTO `travel-sample` ( KEY, VALUE )
VALUES("unique_key", { "id": "01", "type": "airline"})
RETURNING META().id as docid, *;
Ondetravel-sample é o nome do keyspace,unique_key é a chave não duplicada necessária para o objeto de valor que a segue.
O último segmento é a instruçãoRETURNING que especifica o que é retornado.
Neste caso, oid do documento inserido é retornado comodocid. O curinga (*) significa que outros atributos do documento adicionado devem ser retornados também - separadamente dedocid. Veja o resultado da amostra abaixo.
Executar a seguinte instrução na guia Consulta do console da Web Couchbase irá inserir um novo registro no depósitotravel-sample:
INSERT INTO `travel-sample` (KEY, VALUE)
VALUES('cust1293', {"id":"1293","name":"Sample Airline", "type":"airline"})
RETURNING META().id as docid, *
Vamos fazer a mesma coisa em um aplicativo Java. Primeiro, podemos usar uma consulta bruta como esta:
String query = "INSERT INTO `travel-sample` (KEY, VALUE) " +
" VALUES(" +
"\"cust1293\", " +
"{\"id\":\"1293\",\"name\":\"Sample Airline\", \"type\":\"airline\"})" +
" RETURNING META().id as docid, *";
N1qlQueryResult r1 = bucket.query(N1qlQuery.simple(query));
r1.forEach(System.out::println);
Isso retornaráid do documento inserido comodocid separadamente e o corpo completo do documento separadamente:
{
"docid":"cust1293",
"travel-sample":{
"id":"1293",
"name":"Sample Airline",
"type":"airline"
}
}
No entanto, como estamos usando o Java SDK, podemos fazer isso da maneira de objeto criando umJsonDocument que é então inserido no intervalo por meio da APIBucket:
JsonObject ob = JsonObject.create()
.put("id", "1293")
.put("name", "Sample Airline")
.put("type", "airline");
bucket.insert(JsonDocument.create("cust1295", ob));
Instead of using the insert() we can use upsert() which will update the document if there is an existing document with the same unique identifier cust1295.
Como está agora, usarinsert() lançará uma exceção se esse mesmo id exclusivo já existir.
Oinsert(), entretanto, se for bem-sucedido, retornará umJsonDocument que contém o id único e as entradas dos dados inseridos.
A sintaxe para inserção em massa usando o N1QL é:
INSERT INTO `travel-sample` ( KEY, VALUE )
VALUES("unique_key", { "id": "01", "type": "airline"}),
VALUES("unique_key", { "id": "01", "type": "airline"}),
VALUES("unique_n", { "id": "01", "type": "airline"})
RETURNING META().id as docid, *;
Podemos executar operações em massa com o Java SDK usando Java reativo que sublinha o SDK. Vamos adicionar dez documentos em um intervalo usando o processo em lote:
List documents = IntStream.rangeClosed(0,10)
.mapToObj( i -> {
JsonObject content = JsonObject.create()
.put("id", i)
.put("type", "airline")
.put("name", "Sample Airline " + i);
return JsonDocument.create("cust_" + i, content);
}).collect(Collectors.toList());
List r5 = Observable
.from(documents)
.flatMap(doc -> bucket.async().insert(doc))
.toList()
.last()
.toBlocking()
.single();
r5.forEach(System.out::println);
Primeiro, geramos dez documentos e os colocamos em umList;, em seguida, usamos RxJava para realizar a operação em massa.
Por fim, imprimimos o resultado de cada inserção - que foi acumulado para formar umList.
6.4. Declaração deUPDATE
N1QL também tem a instruçãoUPDATE. Ele pode atualizar documentos identificados por suas chaves exclusivas. Podemos usar a instrução de atualização para valoresSET (atualizar) de um atributo ouUNSET (remover) um atributo completamente.
Vamos atualizar um dos documentos que inserimos recentemente no intervalotravel-sample:
String query2 = "UPDATE `travel-sample` USE KEYS \"cust_1\" " +
"SET name=\"Sample Airline Updated\" RETURNING name";
N1qlQueryResult result = bucket.query(N1qlQuery.simple(query2));
result.forEach(System.out::println);
Na consulta acima, atualizamos o atributoname de uma entradacust_1 no depósito paraSample Airline Updated,e instruímos a consulta a retornar o nome atualizado.
Conforme afirmado anteriormente, também podemos alcançar a mesma coisa construindo umJsonDocument com o mesmo id e usar a APIupsert() deBucket para atualizar o documento:
JsonObject o2 = JsonObject.create()
.put("name", "Sample Airline Updated");
bucket.upsert(JsonDocument.create("cust_1", o2));
Nesta próxima consulta, vamos usar o comandoUNSET para remover o atributoname e retornar o documento afetado:
String query3 = "UPDATE `travel-sample` USE KEYS \"cust_2\" " +
"UNSET name RETURNING *";
N1qlQueryResult result1 = bucket.query(N1qlQuery.simple(query3));
result1.forEach(System.out::println);
A sequência JSON retornada é:
{
"travel-sample":{
"id":2,
"type":"airline"
}
}
Anote o atributoname ausente - ele foi removido do objeto de documento. Referência de sintaxe de atualização N1QL pode ser encontradahere.
Então, vamos dar uma olhada na inserção de novos documentos e atualização de documentos. Agora, vamos dar uma olhada na peça final da sigla CRUD -DELETE.
6.5. Declaração deDELETE
Vamos usar a consultaDELETE para excluir alguns dos documentos que criamos anteriormente. Usaremos o id único para identificar o documento com a palavra-chaveUSE KEYS:
String query4 = "DELETE FROM `travel-sample` USE KEYS \"cust_50\"";
N1qlQueryResult result4 = bucket.query(N1qlQuery.simple(query4));
A instrução N1QLDELETE também aceita uma cláusulaWHERE. Portanto, podemos usar condições para selecionar os registros a serem excluídos:
String query5 = "DELETE FROM `travel-sample` WHERE id = 0 RETURNING *";
N1qlQueryResult result5 = bucket.query(N1qlQuery.simple(query5));
Também podemos usarremove() da API do bucket diretamente:
bucket.remove("cust_2");
Muito mais simples né? Sim, mas agora também sabemos como fazer isso usando o N1QL. O documento de referência para a sintaxeDELETE pode ser encontradohere.
7. Funções e subconsultas N1QL
O N1QL não se parecia apenas com o SQL apenas com relação à sintaxe; ele vai até algumas funcionalidades. No SQL, temos algumas funções comoCOUNT() que podem ser usadas na string de consulta.
O N1QL, da mesma maneira, tem suas funções que podem ser usadas na string de consulta.
Por exemplo, esta consulta retornará o número total de registros de referência que estão no intervalotravel-sample:
SELECT COUNT(*) as landmark_count FROM `travel-sample` WHERE type = 'landmark'
Nos exemplos anteriores acima, usamos a funçãoMETA na instruçãoUPDATE para retornar oid do documento atualizado.
Existem métodos de string que podem aparar espaços em branco à direita, criar letras maiúsculas e minúsculas e até verificar se uma string contém um token. Vamos usar algumas dessas funções em uma consulta:
Vamos usar algumas dessas funções em uma consulta:
INSERT INTO `travel-sample` (KEY, VALUE)
VALUES(LOWER(UUID()),
{"id":LOWER(UUID()), "name":"Sample Airport Rand", "created_at": NOW_MILLIS()})
RETURNING META().id as docid, *
A consulta acima insere uma nova entrada no depósitotravel-sample. Ele usa a funçãoUUID() para gerar um id aleatório único que foi convertido para minúsculas usando a funçãoLOWER().
O métodoNOW_MILLIS() foi usado para definir a hora atual, em milissegundos, como o valor do atributocreated_at. A referência completa das funções N1QL pode ser encontradahere.
Às vezes, as subconsultas são úteis e o N1QL fornece provisões para elas. Ainda usando o baldetravel-sample, vamos selecionar o aeroporto de destino de todas as rotas de uma determinada companhia aérea - e obter o país em que estão localizadas:
SELECT DISTINCT country FROM `travel-sample` WHERE type = "airport" AND faa WITHIN
(SELECT destinationairport
FROM `travel-sample` t WHERE t.type = "route" and t.airlineid = "airline_10")
A subconsulta na consulta acima está entre parênteses e retorna o atributodestinationairport, de todas as rotas associadas aairline_10, como uma coleção.
Os atributosdestinationairport correlacionam-se ao atributofaa em documentosairport no depósitotravel-sample. A palavra-chaveWITHIN faz parte decollection operators em N1QL.
Agora, que temos o aeroporto do país de destino de todas as rotas paraairline_10. Vamos fazer algo interessante procurando hotéis dentro desse país:
SELECT name, price, address, country FROM `travel-sample` h
WHERE h.type = "hotel" AND h.country WITHIN
(SELECT DISTINCT country FROM `travel-sample`
WHERE type = "airport" AND faa WITHIN
(SELECT destinationairport FROM `travel-sample` t
WHERE t.type = "route" and t.airlineid = "airline_10" )
) LIMIT 100
A consulta anterior foi usada como uma subconsulta na restriçãoWHERE da consulta externa. Observe a palavra-chaveDISTINCT - ela faz a mesma coisa que em SQL - retorna dados não duplicados.
Todos os exemplos de consulta aqui podem ser executados usando o SDK, conforme demonstrado anteriormente neste artigo.
8. Conclusão
O N1QL leva o processo de consulta ao banco de dados baseado em documentos, como o Couchbase, para outro nível. Isso não apenas simplifica esse processo, mas também torna a mudança de um sistema de banco de dados relacional muito mais fácil.
Vimos a consulta N1QL neste artigo; a documentação principal pode ser encontradahere. E você pode aprender sobre Spring Data Couchbasehere.
Como sempre, o código-fonte completo está disponívelover on Github.