Couchbase mit N1QL abfragen

1. Übersicht

In diesem Artikel werden wir uns mit der Abfrage eines Couchbase-Servers mit N1QL befassen. Vereinfacht ausgedrückt ist dies SQL für NoSQL-Datenbanken - mit dem Ziel, den Übergang von SQL / relationalen Datenbanken zu einem NoSQL-Datenbanksystem zu vereinfachen.

Es gibt verschiedene Möglichkeiten, mit dem Couchbase-Server zu interagieren. Hier verwenden wir das Java SDK, um mit der Datenbank zu interagieren - wie es für Java-Anwendungen typisch ist.

2. Maven-Abhängigkeiten

Wir gehen davon aus, dass bereits ein lokaler Couchbase-Server eingerichtet wurde. Wenn dies nicht der Fall ist, kann Ihnen dieser Leitfaden den Einstieg erleichtern.

Fügen wir nun die Abhängigkeit für das Couchbase Java SDK zu pom.xml hinzu :

 com.couchbase.client java-client 2.5.0 

Die neueste Version des Couchbase Java SDK finden Sie in Maven Central.

Wir werden auch die Jackson-Bibliothek verwenden, um die von Abfragen zurückgegebenen Ergebnisse abzubilden. Fügen wir die Abhängigkeit auch zu pom.xml hinzu :

 com.fasterxml.jackson.core jackson-databind 2.9.1 

Die neueste Version der Jackson-Bibliothek finden Sie auf Maven Central.

3. Herstellen einer Verbindung zu einem Couchbase-Server

Nachdem das Projekt mit den richtigen Abhängigkeiten eingerichtet wurde, stellen wir über eine Java-Anwendung eine Verbindung zu Couchbase Server her.

Zuerst müssen wir den Couchbase-Server starten - falls er noch nicht ausgeführt wird.

Eine Anleitung zum Starten und Stoppen eines Couchbase-Servers finden Sie hier.

Stellen wir eine Verbindung zu einem Couchbase- Eimer her :

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

Wir haben uns mit dem Couchbase- Cluster verbunden und dann das Bucket- Objekt erhalten.

Der Name des Buckets im Couchbase-Cluster lautet test und kann mit der Couchbase-Webkonsole erstellt werden. Wenn wir mit allen Datenbankoperationen fertig sind, können wir den bestimmten Bucket schließen, den wir geöffnet haben.

Auf der anderen Seite können wir die Verbindung zum Cluster trennen - wodurch schließlich alle Buckets geschlossen werden:

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

4. Dokumente einfügen

Couchbase ist ein dokumentenorientiertes Datenbanksystem. Fügen wir dem Test- Bucket ein neues Dokument hinzu :

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

Zuerst haben wir eine JSON- PersonObj erstellt und einige Anfangsdaten bereitgestellt. Schlüssel können als Spalten in einem relationalen Datenbanksystem angesehen werden.

Aus dem person-Objekt haben wir mit JsonDocument.create () ein JSON-Dokument erstellt, das wir in den Bucket einfügen. Beachten Sie, dass wir mit der Klasse java.util.UUID eine zufällige ID generieren .

Das eingefügte Dokument kann in der Couchbase-Webkonsole unter // localhost: 8091 oder durch Aufrufen von buck.get () mit der folgenden ID angezeigt werden :

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

5. Grundlegende N1QL SELECT- Abfrage

N1QL ist eine Obermenge von SQL, und seine Syntax sieht natürlich ähnlich aus.

Das N1QL zum Auswählen aller Dokumente im Test-Bucket lautet beispielsweise:

SELECT * FROM test

Lassen Sie uns diese Abfrage in der Anwendung ausführen:

bucket.bucketManager().createN1qlPrimaryIndex(true, false); N1qlQueryResult result = bucket.query(N1qlQuery.simple("SELECT * FROM test"));

Zuerst erstellen wir einen Primärindex mit createN1qlPrimaryIndex () . Dieser wird ignoriert, wenn er zuvor erstellt wurde. Das Erstellen ist obligatorisch, bevor eine Abfrage ausgeführt werden kann.

Dann verwenden wir die Bucket.query () , um die N1QL-Abfrage auszuführen.

N1qlQueryResult ist ein iterierbares Objekt, und daher können wir jede Zeile mit forEach () ausdrucken :

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

Aus dem zurückgegebenen Ergebnis können wir das N1qlMetrics- Objekt abrufen , indem wir result.info () aufrufen . Aus dem Metrikobjekt können wir Einblicke in das zurückgegebene Ergebnis erhalten - zum Beispiel das Ergebnis und die Fehleranzahl:

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

Für das zurückgegebene Ergebnis können wir mit result.parseSuccess () überprüfen, ob die Abfrage syntaktisch korrekt ist und erfolgreich analysiert wurde. Wir können das Ergebnis.finalSuccess () verwenden, um festzustellen, ob die Ausführung der Abfrage erfolgreich war.

6. N1QL-Abfrageanweisungen

Schauen wir uns die verschiedenen N1QL Query-Anweisungen und ihre verschiedenen Ausführungsmöglichkeiten über das Java SDK an.

6.1. SELECT- Anweisung

Die SELECT- Anweisung in NIQL ähnelt einer Standard-SQL-Anweisung SELECT . Es besteht aus drei Teilen:

  • SELECT - Definiert die Projektion der zurückzugebenden Dokumente
  • FROM - beschreibt den Schlüsselbereich, aus dem die Dokumente abgerufen werden sollen. Schlüsselraum ist gleichbedeutend mit Tabellenname in SQL-Datenbanksystemen
  • WHERE - Gibt die zusätzlichen Filterkriterien an

Der Couchbase-Server wird mit einigen Beispiel-Buckets (Datenbanken) geliefert. Wenn sie bei der Ersteinrichtung nicht geladen wurden, verfügt der Abschnitt Einstellungen der Webkonsole über eine eigene Registerkarte zum Einrichten.

Wir werden den Reiseproben- Eimer verwenden. Der Reisebeispiel- Eimer enthält Daten für Fluggesellschaften, Sehenswürdigkeiten, Flughäfen, Hotels und Routen. Das Datenmodell finden Sie hier.

Lassen Sie uns 100 Fluglinienaufzeichnungen aus den Reisebeispieldaten auswählen:

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

Die N1QL-Abfrage sieht, wie oben zu sehen, SQL sehr ähnlich. Beachten Sie, dass der Schlüsselraumname in backtick (`) eingegeben werden muss, da er einen Bindestrich enthält.

N1qlQueryResult ist nur ein Wrapper um die von der Datenbank zurückgegebenen JSON-Rohdaten. Es erweitert Iterable und kann wiederholt werden.

Durch Aufrufen von result1.allRows () werden alle Zeilen in einem List- Objekt zurückgegeben. Dies ist nützlich, um Ergebnisse mit der Stream- API zu verarbeiten und / oder über den Index auf jedes Ergebnis zuzugreifen:

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

Wir haben die erste Zeile der zurückgegebenen Ergebnisse erhalten und verwenden row.value () , um ein JsonObject abzurufen, das die Zeile einem Schlüssel-Wert-Paar zuordnet und der Schlüssel dem Spaltennamen entspricht.

So we got the value of column, name, for the first row using the get(). It's as easy as that.

So far we have been using simple N1QL query. Let's look at the parameterized statement in N1QL.

In this query, we're going to use the wildcard (*) symbol for selecting all the fields in the travel-sample records where type is an airport.

The type will be passed to the statement – as a parameter. Then we process the returned result:

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

We created a JsonObject to hold the parameters as a key-value pair. The value of the key ‘type', in the pVal object, will be used to replace the $type placeholder in the query string.

N1qlQuery.parameterized() accepts a query string that contains one or more placeholders and a JsonObject as demonstrated above.

In the previous sample query above, we only select a column – name. This makes it easy to map the returned result into a JsonObject.

But now that we use the wildcard (*) in the select statement, it is not that simple. The returned result is a raw JSON string:

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

So what we need is a way to map each row to a structure that allows us to access the data by specifying the column name.

Therefore, let's create a method that will accept N1qlQueryResult and then map every row in the result to a JsonNode object.

We choose JsonNode because it can handle a broad range of JSON data structures and we can easily navigate it:

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

We processed each row in the result using the Stream API. We mapped each row to a JsonNode object and then return the result as a List of JsonNodes.

Now we can use the method to process the returned result from the last query:

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

From the example JSON output shown previously, every row has a key the correlates to the keyspace name specified in the SELECT query – which is travel-sample in this case.

So we got the first row in the result, which is a JsonNode. Then we traverse the node to get to the airportname key, that is then printed as a text.

The example raw JSON output shared earlier provides more clarity as per the structure of the returned result.

6.2. SELECT Statement Using N1QL DSL

Other than using raw string literals for building queries we can also use N1QL DSL which comes with the Java SDK we are using.

For example, the above string query can be formulated with the DSL thus:

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

The DSL is fluent and can be interpreted easily. The data selection classes and methods are in com.couchbase.client.java.query.Select class.

Expression methods like i(), eq(), x(), s() are in com.couchbase.client.java.query.dsl.Expression class. Read more about the DSL here.

N1QL select statements can also have OFFSET, GROUP BY and ORDER BY clauses. The syntax is pretty much like that of standard SQL, and its reference can be found here.

The WHERE clause of N1QL can take Logical Operators AND, OR, and NOT in its definitions. In addition to this, N1QL has provision for comparison operators like >, ==, !=, IS NULL and others.

There are also other operators that make accessing stored documents easy – the string operators can be used to concatenate fields to form a single string, and the nested operators can be used to slice arrays and cherry pick fields or element.

Let's see these in action.

This query selects the city column, concatenate the airportname and faa columns as portname_faa from the travel-sample bucket where the country column ends with ‘States'‘, and the latitude of the airport is greater than or equal to 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));

We can do the same thing using 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)); //...

Let's look at other statements in N1QL. We'll be building on the knowledge we've acquired in this section.

6.3. INSERT Statement

The syntax for the insert statement in N1QL is:

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

Where travel-sample is the keyspace name, unique_key is the required non-duplicate key for the value object that follows it.

The last segment is the RETURNING statement that specifies what gets returned.

In this case, the id of the inserted document is returned as docid. The wildcard (*) signifies that other attributes of the added document should be returned as well – separately from docid. See the sample result below.

Executing the following statement in the Query tab of Couchbase Web Console will insert a new record into the travel-sample bucket:

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

Let's do the same thing from a Java app. First, we can use a raw query like this:

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

This will return the id of the inserted document as docid separately and the complete document body separately:

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

However, since we're using the Java SDK, we can do it the object way by creating a JsonDocument that is then inserted into the bucket via the Bucket API:

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.

As it is now, using insert() will throw an exception if that same unique id already exists.

The insert(), however, if successful, will return a JsonDocument that contains the unique id and entries of the inserted data.

The syntax for bulk insert using N1QL is:

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

We can perform bulk operations with the Java SDK using Reactive Java that underlines the SDK. Let's add ten documents into a bucket using batch process:

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

First, we generate ten documents and put them into a List; then we used RxJava to perform the bulk operation.

Finally, we print out the result of each insert – which has been accumulated to form a List.

The reference for performing bulk operations in the Java SDK can be found here. Also, the reference for insert statement can be found here.

6.4. UPDATE Statement

N1QL also has UPDATE statement. It can update documents identified by their unique keys. We can use the update statement to either SET (update) values of an attribute or UNSET (remove) an attribute altogether.

Let's update one of the documents we recently inserted into the travel-sample bucket:

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

In the above query, we updated the name attribute of a cust_1 entry in the bucket to Sample Airline Updated, and we instruct the query to return the updated name.

As stated earlier, we can also achieve the same thing by constructing a JsonDocument with the same id and use the upsert() of Bucket API to update the document:

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

In this next query, let's use the UNSET command to remove the name attribute and return the affected document:

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

The returned JSON string is:

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

Take note of the missing name attribute – it has been removed from the document object. N1QL update syntax reference can be found here.

So we have a look at inserting new documents and updating documents. Now let's look at the final piece of the CRUD acronym – DELETE.

6.5. DELETE Statement

Let's use the DELETE query to delete some of the documents we have created earlier. We'll use the unique id to identify the document with the USE KEYS keyword:

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

N1QL DELETE statement also takes a WHERE clause. So we can use conditions to select the records to be deleted:

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

We can also use the remove() from the bucket API directly:

bucket.remove("cust_2");

Much simpler right? Yes, but now we also know how to do it using N1QL. The reference doc for DELETE syntax can be found here.

7. N1QL Functions and Sub-Queries

N1QL did not just resemble SQL regarding syntax alone; it goes all the way to some functionalities. In SQL, we've some functions like COUNT() that can be used within the query string.

N1QL, in the same fashion, has its functions that can be used in the query string.

For example, this query will return the total number of landmark records that are in the travel-sample bucket:

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

In previous examples above, we've used the META function in UPDATE statement to return the id of updated document.

There are string method that can trim trailing white spaces, make lower and upper case letters and even check if a string contains a token. Let's use some of these functions in a query:

Let's use some of these functions in a query:

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

The query above inserts a new entry into the travel-sample bucket. It uses the UUID() function to generate a unique random id which was converted to lower case using the LOWER() function.

The NOW_MILLIS() method was used to set the current time, in milliseconds, as the value of the created_at attribute. The complete reference of N1QL functions can be found here.

Sub-queries come in handy at times, and N1QL has provision for them. Still using the travel-sample bucket, let's select the destination airport of all routes for a particular airline – and get the country they are located in:

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

The sub-query in the above query is enclosed within parentheses and returns the destinationairport attribute, of all routes associated with airline_10, as a collection.

The destinationairport attributes correlate to the faa attribute on airport documents in the travel-sample bucket. The WITHIN keyword is part of collection operators in N1QL.

Now, that we've got the country of destination airport of all routes for airline_10. Let's do something interesting by looking for hotels within that country:

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

Die vorherige Abfrage wurde als Unterabfrage in der WHERE- Einschränkung der äußersten Abfrage verwendet. Beachten Sie, dass das Schlüsselwort DISTINCT - es funktioniert genauso wie in SQL - nicht doppelte Daten zurückgibt.

Alle Abfragebeispiele hier können mit dem SDK ausgeführt werden, wie weiter oben in diesem Artikel gezeigt.

8. Fazit

N1QL führt das Abfragen der dokumentbasierten Datenbank wie Couchbase auf eine andere Ebene. Dies vereinfacht nicht nur diesen Prozess, sondern erleichtert auch den Wechsel von einem relationalen Datenbanksystem erheblich.

Wir haben uns die N1QL-Abfrage in diesem Artikel angesehen. Die Hauptdokumentation finden Sie hier. Und hier erfahren Sie mehr über Spring Data Couchbase.

Wie immer ist der vollständige Quellcode auf Github verfügbar.