Interrogation de Couchbase avec N1QL

Interrogation de Couchbase avec N1QL

1. Vue d'ensemble

Dans cet article, nous allons examiner l'interrogation d'un serveur Couchbase avecN1QL. De manière simplifiée, il s’agit de SQL pour bases de données NoSQL - dans le but de faciliter la transition des bases de données SQL / Relational vers un système de base de données NoSQL.

Il existe plusieurs façons d'interagir avec le serveur Couchbase; ici, nous utiliserons le SDK Java pour interagir avec la base de données, comme c'est généralement le cas pour les applications Java.

Lectures complémentaires:

Introduction à Spring Data Couchbase

Utilisation rapide et pratique de Spring Data Couchbase pour interagir avec un serveur de base de données Couchbase.

Read more

Opérations par lots asynchrones dans Couchbase

Apprenez à effectuer des opérations de traitement par lots efficaces dans Couchbase à l'aide de l'API Java asynchrone de Couchbase.

Read more

Introduction à Couchbase SDK pour Java

Une introduction rapide et pratique à l’utilisation du SDK Java Couchbase.

Read more

2. Dépendances Maven

Nous supposons qu'un serveur Couchbase local a déjà été configuré; si ce n’est pas le cas, cesguide peuvent vous aider à démarrer.

Ajoutons maintenant la dépendance du SDK Java Couchbase àpom.xml:


    com.couchbase.client
    java-client
    2.5.0

La dernière version du SDK Java Couchbase se trouve surMaven Central.

Nous utiliserons également la bibliothèque Jackson pour mapper les résultats renvoyés par les requêtes; ajoutons également sa dépendance àpom.xml:


    com.fasterxml.jackson.core
    jackson-databind
    2.9.1

La dernière version de la bibliothèque Jackson peut être trouvée surMaven Central.

3. Connexion à un serveur Couchbase

Maintenant que le projet est configuré avec les bonnes dépendances, connectons-nous à Couchbase Server depuis une application Java.

Tout d'abord, nous devons démarrer le serveur Couchbase - s'il ne fonctionne pas déjà.

Un guide pour démarrer et arrêter un serveur Couchbase peut être trouvéhere.

Connectons-nous à une CouchbaseBucket:

Cluster cluster = CouchbaseCluster.create("localhost");
Bucket bucket = cluster.openBucket("test");

Ce que nous avons fait, c'est de nous connecter à la CouchbaseCluster, puis d'obtenir l'objetBucket.

Le nom du bucket dans le cluster Couchbase esttest et peut être créé à l'aide de la console Web Couchbase. Lorsque nous avons terminé toutes les opérations de base de données, nous pouvons fermer le bucket particulier que nous avons ouvert.

D'autre part, nous pouvons nous déconnecter du cluster - ce qui finira par fermer tous les compartiments:

bucket.close();
cluster.disconnect();

4. Insertion de documents

Couchbase est un système de base de données orienté document. Ajoutons un nouveau document au buckettest:

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);

Tout d'abord, nous avons créé un JSONpersonObj et fourni quelques données initiales. Les clés peuvent être vues sous forme de colonnes dans un système de base de données relationnelle.

À partir de l'objet personne, nous avons créé un document JSON à l'aide deJsonDocument.create(), que nous insérerons dans le bucket. Notez que nous générons unid aléatoire en utilisant la classejava.util.UUID.

Le document inséré peut être vu dans la console Web Couchbase àhttp://localhost:8091 ou en appelant lebucket.get() avec sesid:

System.out.println(bucket.get(id));

5. Requête de base N1QLSELECT

N1QL est un sur-ensemble de SQL, et sa syntaxe est naturellement similaire.

Par exemple, le N1QL pour sélectionner tous les documents dans lestest bucket est:

SELECT * FROM test

Exécutons cette requête dans l'application:

bucket.bucketManager().createN1qlPrimaryIndex(true, false);

N1qlQueryResult result
  = bucket.query(N1qlQuery.simple("SELECT * FROM test"));

Tout d'abord, nous créons un index primaire en utilisant lescreateN1qlPrimaryIndex(), il sera ignoré s'il a été créé auparavant; sa création est obligatoire avant qu'une requête puisse être exécutée.

Ensuite, nous utilisons lesbucket.query() pour exécuter la requête N1QL.

N1qlQueryResult est un objetIterable<N1qlQueryRow>, et ainsi nous pouvons imprimer chaque ligne en utilisantforEach():

result.forEach(System.out::println);

À partir desresult renvoyés, nous pouvons obtenir l'objetN1qlMetrics en appelantresult.info(). À partir de l'objet de métriques, nous pouvons obtenir des informations sur le résultat renvoyé - par exemple, le résultat et le nombre d'erreurs:

System.out.println("result count: " + result.info().resultCount());
System.out.println("error count: " + result.info().errorCount());

Sur lesresult renvoyés, nous pouvons utiliser lesresult.parseSuccess() pour vérifier si la requête est syntaxiquement correcte et analysée avec succès. Nous pouvons utiliser lesresult.finalSuccess() pour déterminer si l'exécution de la requête a réussi.

6. Instructions de requête N1QL

Jetons un coup d'œil aux différentes instructions de requête N1QL et aux différentes manières de les exécuter via le SDK Java.

6.1. InstructionSELECT

L'instructionSELECT dans NIQL est comme un SQL standardSELECT. Il se compose de trois parties:

  • SELECT définit la projection des documents à renvoyer

  • FROM décrit l'espace de clés à partir duquel récupérer les documents; keyspace est synonyme de nom de table dans les systèmes de base de données SQL

  • WHERE spécifie les critères de filtrage supplémentaires

Le serveur Couchbase est livré avec quelques exemples de compartiments (bases de données). S'ils n'ont pas été chargés lors de la configuration initiale, la sectionSettings de la console Web dispose d'un onglet dédié pour les configurer.

Nous utiliserons le buckettravel-sample. Le buckettravel-sample contient des données sur les compagnies aériennes, les points de repère, les aéroports, les hôtels et les itinéraires. Le modèle de données peut être trouvéhere.

Sélectionnons 100 enregistrements de compagnies aériennes parmi les données d'échantillons de voyage:

String query = "SELECT name FROM `travel-sample` " +
  "WHERE type = 'airport' LIMIT 100";
N1qlQueryResult result1 = bucket.query(N1qlQuery.simple(query));

Comme on peut le voir ci-dessus, la requête N1QL ressemble beaucoup à SQL. Notez que le nom de l'espace-clé doit être placé dans backtick (`) car il contient un trait d'union.

N1qlQueryResult est juste un wrapper autour des données JSON brutes renvoyées par la base de données. Il étendIterable<N1qlQueryRow> et peut être bouclé.

L'appel deresult1.allRows() renverra toutes les lignes d'un objetList<N1qlQueryRow>. Ceci est utile pour traiter les résultats avec l'APIStream et / ou accéder à chaque résultat via un index:

N1qlQueryRow row = result1.allRows().get(0);
JsonObject rowJson = row.value();
System.out.println("Name in First Row " + rowJson.get("name"));

Nous avons obtenu la première ligne des résultats retournés, et nous utilisonsrow.value() pour obtenir unJsonObject - qui mappe la ligne à une paire clé-valeur, et la clé correspond au nom de la colonne.

Nous avons donc obtenu la valeur de la colonne,name, pour la première ligne en utilisant lesget(). C'est aussi simple que ça.

Jusqu'ici, nous avons utilisé une requête simple N1QL. Regardons l'instructionparameterized dans N1QL.

Dans cette requête, nous allons utiliser le symbole générique (*) pour sélectionner tous les champs dans les enregistrementstravel-sampletype est unairport.

Lestype seront transmis à l'instruction - en tant que paramètre. Ensuite, nous traitons le résultat renvoyé:

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));

Nous avons créé un JsonObject pour contenir les paramètres sous forme d'une paire clé-valeur. La valeur de la clé ‘type', dans l’objetpVal, sera utilisée pour remplacer l’espace réservé$type dans la chaînequery.

N1qlQuery.parameterized() accepte une chaîne de requête qui contient un ou plusieurs espaces réservés et unJsonObject comme illustré ci-dessus.

Dans l'exemple de requête précédent ci-dessus, nous ne sélectionnons qu'une colonne -name.. Cela facilite le mappage du résultat renvoyé en unJsonObject.

Mais maintenant que nous utilisons le caractère générique (*) dans l'instruction select, ce n'est pas si simple. Le résultat renvoyé est une chaîne JSON brute:

[
  {
    "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"
    }
  },

Nous avons donc besoin d’un moyen de mapper chaque ligne à une structure nous permettant d’accéder aux données en spécifiant le nom de la colonne.

Par conséquent, créons une méthode qui accepteraN1qlQueryResult, puis mappons chaque ligne du résultat à un objetJsonNode.

Nous choisissonsJsonNode car il peut gérer un large éventail de structures de données JSON et nous pouvons facilement y naviguer:

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());
}

Nous avons traité chaque ligne du résultat à l'aide de l'APIStream. Nous avons mappé chaque ligne sur un objetJsonNode, puis renvoyons le résultat sous forme deList deJsonNodes.

Nous pouvons maintenant utiliser la méthode pour traiter le résultat renvoyé par la dernière requête:

List list = extractJsonResult(r2);
System.out.println(
  list.get(0).get("travel-sample").get("airportname").asText());

À partir de l'exemple de sortie JSON montré précédemment, chaque ligne a une clé qui correspond au nom de l'espace de clés spécifié dans la requêteSELECT - qui esttravel-sample dans ce cas.

Nous avons donc obtenu la première ligne du résultat, qui est unJsonNode. Ensuite, nous traversons le nœud pour accéder à la cléairportname, qui est ensuite imprimée sous forme de texte.

L'exemple de sortie JSON brut partagé précédemment fournit davantage de clarté quant à la structure du résultat renvoyé.

6.2. InstructionSELECT utilisant N1QL DSL

Outre l'utilisation de littéraux de chaîne bruts pour la création de requêtes, nous pouvons également utiliser N1QL DSL, fourni avec le SDK Java que nous utilisons.

Par exemple, la requête de chaîne ci-dessus peut être formulée avec le DSL ainsi:

Statement statement = select("*")
  .from(i("travel-sample"))
  .where(x("type").eq(s("airport")))
  .limit(100);
N1qlQueryResult r3 = bucket.query(N1qlQuery.simple(statement));

Le DSL est fluide et peut être interprété facilement. Les classes et méthodes de sélection de données sont dans la classecom.couchbase.client.java.query.Select.

Les méthodes d'expression telles quei(), eq(), x(), s() sont dans la classecom.couchbase.client.java.query.dsl.Expression. En savoir plus sur les DSLhere.

N1QL select statements can also have OFFSET, GROUP BY and ORDER BY clauses. La syntaxe est assez proche de celle du SQL standard, et sa référence peut être trouvéehere.

La clauseWHERE de N1QL peut prendre les opérateurs logiquesAND,OR etNOT dans ses définitions. En plus de cela, N1QL a prévu des opérateurs de comparaison comme>, ==,! =,IS NULL etothers.

Il existe également d'autres opérateurs qui facilitent l'accès aux documents stockés - lesstring operators peuvent être utilisés pour concaténer des champs pour former une seule chaîne, et lesnested operators peuvent être utilisés pour découper des tableaux et sélectionner des champs ou des éléments.

Voyons cela en action.

Cette requête sélectionne la colonnecity, concatène les colonnesairportname etfaa en tant queportname_faa du compartimenttravel-sample où la colonnecountry se termine par‘States' ', et lelatitude de l'aéroport est supérieur ou égal à 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));

Nous pouvons faire la même chose avec 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));
//...

Examinons d'autres instructions dans N1QL. Nous allons nous appuyer sur les connaissances que nous avons acquises dans cette section.

6.3. InstructionINSERT

La syntaxe de l'instruction insert dans N1QL est la suivante:

INSERT INTO `travel-sample` ( KEY, VALUE )
VALUES("unique_key", { "id": "01", "type": "airline"})
RETURNING META().id as docid, *;

travel-sample est le nom de l'espace de clés,unique_key est la clé non dupliquée requise pour l'objet de valeur qui le suit.

Le dernier segment est l'instructionRETURNING qui spécifie ce qui est renvoyé.

Dans ce cas, leid du document inséré est renvoyé sous la formedocid. Le caractère générique (*) signifie que les autres attributs du document ajouté doivent également être renvoyés - séparément dedocid. Voir le exemple de résultat ci-dessous.

L'exécution de l'instruction suivante dans l'onglet Requête de Couchbase Web Console insérera un nouvel enregistrement dans le buckettravel-sample:

INSERT INTO `travel-sample` (KEY, VALUE)
VALUES('cust1293', {"id":"1293","name":"Sample Airline", "type":"airline"})
RETURNING META().id as docid, *

Faisons la même chose depuis une application Java. Tout d'abord, nous pouvons utiliser une requête brute comme celle-ci:

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);

Cela renverra lesid du document inséré en tant quedocid séparément et le corps du document complet séparément:

{
  "docid":"cust1293",
  "travel-sample":{
    "id":"1293",
    "name":"Sample Airline",
    "type":"airline"
  }
}

Cependant, puisque nous utilisons le SDK Java, nous pouvons le faire à la manière de l'objet en créant unJsonDocument qui est ensuite inséré dans le bucket via l'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.

Dans l'état actuel des choses, l'utilisation deinsert() lèvera une exception si ce même identifiant unique existe déjà.

Leinsert(), cependant, s'il réussit, renverra unJsonDocument qui contient l'identifiant unique et les entrées des données insérées.

La syntaxe pour l'insertion en bloc à l'aide de N1QL est la suivante:

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, *;

Nous pouvons effectuer des opérations en bloc avec le SDK Java en utilisant Java réactif qui souligne le SDK. Ajoutons dix documents dans un bucket à l'aide du traitement par lots:

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);

Tout d'abord, nous générons dix documents et les mettons dans unList; puis nous avons utilisé RxJava pour effectuer l'opération en bloc.

Enfin, nous imprimons le résultat de chaque insert - qui a été accumulé pour former unList.

La référence pour effectuer des opérations en bloc dans le SDK Java peut être trouvéehere. En outre, la référence pour l'instruction d'insertion peut être trouvéehere.

6.4. InstructionUPDATE

N1QL a également l'instructionUPDATE. Il peut mettre à jour des documents identifiés par leurs clés uniques. Nous pouvons utiliser l'instruction de mise à jour soit pour les valeursSET (mise à jour) d'un attribut, soit pourUNSET (supprimer) un attribut.

Mettons à jour l'un des documents que nous avons récemment insérés dans le buckettravel-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);

Dans la requête ci-dessus, nous avons mis à jour l'attributname d'une entréecust_1 dans le compartiment enSample Airline Updated, et nous demandons à la requête de renvoyer le nom mis à jour.

Comme indiqué précédemment, nous pouvons également réaliser la même chose en construisant unJsonDocument avec le même id et en utilisant l'APIupsert() deBucket pour mettre à jour le document:

JsonObject o2 = JsonObject.create()
  .put("name", "Sample Airline Updated");
bucket.upsert(JsonDocument.create("cust_1", o2));

Dans cette requête suivante, utilisons la commandeUNSET pour supprimer l'attributname et renvoyer le document concerné:

String query3 = "UPDATE `travel-sample` USE KEYS \"cust_2\" " +
  "UNSET name RETURNING *";
N1qlQueryResult result1 = bucket.query(N1qlQuery.simple(query3));
result1.forEach(System.out::println);

La chaîne JSON renvoyée est:

{
  "travel-sample":{
    "id":2,
    "type":"airline"
  }
}

Prenez note de l'attributname manquant - il a été supprimé de l'objet document. La référence de syntaxe de mise à jour N1QL peut être trouvéehere.

Nous envisageons donc d’insérer de nouveaux documents et de les mettre à jour. Regardons maintenant le dernier élément de l'acronyme CRUD -DELETE.

6.5. InstructionDELETE

Utilisons la requêteDELETE pour supprimer certains des documents que nous avons créés précédemment. Nous utiliserons l'identifiant unique pour identifier le document avec le mot cléUSE KEYS:

String query4 = "DELETE FROM `travel-sample` USE KEYS \"cust_50\"";
N1qlQueryResult result4 = bucket.query(N1qlQuery.simple(query4));

L'instruction N1QLDELETE prend également une clauseWHERE. Nous pouvons donc utiliser des conditions pour sélectionner les enregistrements à supprimer:

String query5 = "DELETE FROM `travel-sample` WHERE id = 0 RETURNING *";
N1qlQueryResult result5 = bucket.query(N1qlQuery.simple(query5));

Nous pouvons également utiliser directement lesremove() de l'API du bucket:

bucket.remove("cust_2");

Beaucoup plus simple non? Oui, mais maintenant nous savons aussi comment le faire en utilisant N1QL. Le document de référence pour la syntaxeDELETE peut être trouvéhere.

7. Fonctions et sous-requêtes N1QL

N1QL ne ressemble pas simplement à SQL en ce qui concerne la syntaxe seule; il va jusqu'à certaines fonctionnalités. En SQL, nous avons des fonctions telles queCOUNT() qui peuvent être utilisées dans la chaîne de requête.

De la même manière, N1QL a ses fonctions qui peuvent être utilisées dans la chaîne de requête.

Par exemple, cette requête renverra le nombre total d'enregistrements de repère qui se trouvent dans le compartimenttravel-sample:

SELECT COUNT(*) as landmark_count FROM `travel-sample` WHERE type = 'landmark'

Dans les exemples précédents ci-dessus, nous avons utilisé la fonctionMETA dans l'instructionUPDATE pour renvoyer lesid du document mis à jour.

Il existe une méthode de chaîne qui permet de supprimer les espaces, les lettres majuscules et minuscules et même de vérifier si une chaîne contient un jeton. Utilisons certaines de ces fonctions dans une requête:

Utilisons certaines de ces fonctions dans une requête:

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, *

La requête ci-dessus insère une nouvelle entrée dans le compartimenttravel-sample. Il utilise la fonctionUUID() pour générer un identifiant aléatoire unique qui a été converti en minuscules à l'aide de la fonctionLOWER().

La méthodeNOW_MILLIS() a été utilisée pour définir l'heure actuelle, en millisecondes, comme valeur de l'attributcreated_at. La référence complète des fonctions N1QL peut être trouvéehere.

Les sous-requêtes sont parfois utiles, et N1QL les prend en charge. Toujours en utilisant le buckettravel-sample, sélectionnons l'aéroport de destination de toutes les routes pour une compagnie aérienne en particulier - et obtenons le pays dans lequel ils se trouvent:

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")

La sous-requête de la requête ci-dessus est placée entre parenthèses et renvoie l'attributdestinationairport, de toutes les routes associées àairline_10, sous forme de collection.

Les attributsdestinationairport correspondent à l'attributfaa sur les documentsairport dans le compartimenttravel-sample. Le mot cléWITHIN fait partie decollection operators dans N1QL.

Maintenant, nous avons le pays d’aéroport de destination de toutes les routes pourairline_10. Faisons quelque chose d'intéressant en recherchant des hôtels dans ce pays:

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

La requête précédente était utilisée comme sous-requête dans la contrainteWHERE de la requête la plus externe. Prenez note du mot cléDISTINCT - il fait la même chose qu'en SQL - renvoie des données non dupliquées.

Tous les exemples de requête ici peuvent être exécutés à l'aide du SDK, comme indiqué précédemment dans cet article.

8. Conclusion

N1QL prend le processus d'interrogation de la base de données basée sur les documents comme Couchbase à un autre niveau. Cela ne simplifie pas seulement ce processus, mais facilite également le passage d’un système de base de données relationnelle.

Nous avons examiné la requête N1QL dans cet article; la documentation principale peut être trouvéehere. Et vous pouvez en apprendre davantage sur Spring Data Couchbasehere.

Comme toujours, le code source complet est disponibleover on Github.