N1QLによるCouchbaseのクエリ

N1QLを使用したCouchbaseのクエリ

1. 概要

この記事では、N1QLを使用してCouchbaseServerにクエリを実行する方法について説明します。 簡単に言えば、これはNoSQLデータベース用のSQLです。これは、SQL /リレーショナルデータベースからNoSQLデータベースシステムへの移行を容易にすることを目的としています。

CouchbaseServerと対話する方法はいくつかあります。ここでは、Java SDKを使用してデータベースを操作します。これは、Javaアプリケーションで一般的です。

参考文献:

Spring Data Couchbaseの紹介

Spring Data Couchbaseを使用してCouchbase DBサーバーとやり取りするための迅速かつ実用的な方法。

Couchbaseでの非同期バッチ操作

非同期Couchbase Java APIを使用して、Couchbaseで効率的なバッチ操作を実行する方法を学びます。

Java用Couchbase SDKの概要

Java Couchbase SDKを使用するための迅速かつ実用的な入門書。

2. Mavenの依存関係

ローカルのCouchbaseServerがすでにセットアップされていることを前提としています。そうでない場合は、このguideを使用して開始できます。

次に、Couchbase Java SDKの依存関係をpom.xmlに追加しましょう。


    com.couchbase.client
    java-client
    2.5.0

Couchbase Java SDKの最新バージョンは、Maven Centralにあります。

また、Jacksonライブラリを使用して、クエリから返された結果をマッピングします。その依存関係をpom.xmlにも追加しましょう。


    com.fasterxml.jackson.core
    jackson-databind
    2.9.1

Jacksonライブラリの最新バージョンはMaven Centralにあります。

3. CouchbaseServerへの接続

プロジェクトが適切な依存関係でセットアップされたので、JavaアプリケーションからCouchbaseServerに接続しましょう。

まず、Couchbase Serverを起動する必要があります(まだ実行されていない場合)。

Couchbase Serverの起動と停止のガイドは、hereにあります。

CouchbaseBucketに接続しましょう:

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

私たちが行ったのは、CouchbaseClusterに接続してから、Bucketオブジェクトを取得することでした。

Couchbaseクラスター内のバケットの名前はtestであり、CouchbaseWebコンソールを使用して作成できます。 すべてのデータベース操作が完了したら、開いた特定のバケットを閉じることができます。

一方、クラスターから切断できます。これにより、最終的にすべてのバケットが閉じられます。

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

4. ドキュメントを挿入する

Couchbaseはドキュメント指向のデータベースシステムです。 testバケットに新しいドキュメントを追加しましょう。

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

まず、JSONpersonObjを作成し、いくつかの初期データを提供しました。 キーは、リレーショナルデータベースシステムの列と見なすことができます。

personオブジェクトから、バケットに挿入するJsonDocument.create(),を使用してJSONドキュメントを作成しました。 java.util.UUIDクラスを使用してランダムなidを生成することに注意してください。

挿入されたドキュメントは、Couchbase Webコンソールのhttp://localhost:8091で、またはidを使用してbucket.get()を呼び出すことで確認できます。

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

5. 基本的なN1QLSELECTクエリ

N1QLはSQLのスーパーセットであり、その構文は当然似ています。

たとえば、test bucket内のすべてのドキュメントを選択するためのN1QLは次のとおりです。

SELECT * FROM test

アプリケーションでこのクエリを実行してみましょう。

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

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

まず、createN1qlPrimaryIndex()を使用してプライマリインデックスを作成します。以前に作成されている場合は無視されます。クエリを実行する前に、作成する必要があります。

次に、bucket.query()を使用してN1QLクエリを実行します。

N1qlQueryResultIterable<N1qlQueryRow>オブジェクトであるため、forEach()を使用してすべての行を出力できます。

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

返されたresultから、result.info()を呼び出すことでN1qlMetricsオブジェクトを取得できます。 メトリックスオブジェクトから、返された結果についての洞察を得ることができます-例えば、結果とエラーカウント:

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

返されたresultで、result.parseSuccess()を使用して、クエリが構文的に正しく、正常に解析されているかどうかを確認できます。 result.finalSuccess()を使用して、クエリの実行が成功したかどうかを判断できます。

6. N1QLクエリステートメント

さまざまなN1QLクエリステートメントと、JavaSDKを介してそれらを実行するさまざまな方法を見てみましょう。

6.1. SELECTステートメント

NIQLのSELECTステートメントは、標準のSQLSELECTとまったく同じです。 それは3つの部分から成ります:

  • SELECTは、返されるドキュメントの投影を定義します

  • FROMは、ドキュメントをフェッチするためのキースペースを記述します。キースペースは、SQLデータベースシステムのテーブル名と同義です。

  • WHEREは、追加のフィルタリング基準を指定します

Couchbase Serverには、いくつかのサンプルバケット(データベース)が付属しています。 初期設定時にロードされなかった場合、WebコンソールのSettingsセクションには、それらを設定するための専用のタブがあります。

travel-sampleバケットを使用します。 travel-sampleバケットには、航空会社、ランドマーク、空港、ホテル、およびルートのデータが含まれています。 データモデルはhereで見つけることができます。

旅行サンプルデータから100の航空会社レコードを選択しましょう。

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

N1QLクエリは、上記のように、SQLに非常によく似ています。 キースペース名にはハイフンが含まれているため、バックティック( `)に入れる必要があることに注意してください。

N1qlQueryResultは、データベースから返された生のJSONデータの単なるラッパーです。 Iterable<N1qlQueryRow>を拡張し、ループオーバーできます。

result1.allRows()を呼び出すと、List<N1qlQueryRow>オブジェクトのすべての行が返されます。 これは、Stream APIを使用して結果を処理したり、インデックスを介して各結果にアクセスしたりする場合に役立ちます。

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

返された結果の最初の行を取得し、row.value()を使用してJsonObjectを取得します。これは行をキーと値のペアにマップし、キーは列名に対応します。

したがって、get()を使用して、最初の行の列の値name,を取得しました。 それは簡単です。

これまで、単純なN1QLクエリを使用してきました。 N1QLのparameterizedステートメントを見てみましょう。

このクエリでは、ワイルドカード(*)記号を使用して、typeairportであるtravel-sampleレコードのすべてのフィールドを選択します。

typeは、パラメーターとしてステートメントに渡されます。 次に、返された結果を処理します。

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

パラメーターをキーと値のペアとして保持するJsonObjectを作成しました。 pValオブジェクトのキー ‘type',の値は、query文字列の$typeプレースホルダーを置き換えるために使用されます。

N1qlQuery.parameterized()は、上記のように1つ以上のプレースホルダーとJsonObjectを含むクエリ文字列を受け入れます。

上記の前のサンプルクエリでは、列のみを選択します–name.これにより、返された結果をJsonObjectに簡単にマッピングできます。

しかし、selectステートメントでワイルドカード(*)を使用するようになったため、それほど単純ではありません。 返される結果は生のJSON文字列です。

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

したがって、必要なのは、列名を指定してデータにアクセスできる構造に各行をマップする方法です。

したがって、N1qlQueryResultを受け入れるメソッドを作成してから、結果のすべての行をJsonNodeオブジェクトにマップしましょう。

JsonNodeを選択するのは、さまざまなJSONデータ構造を処理でき、簡単にナビゲートできるためです。

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

Stream APIを使用して、結果の各行を処理しました。 各行をJsonNodeオブジェクトにマップし、結果をJsonNodes.Listとして返します。

これでメソッドを使用して、最後のクエリから返された結果を処理できます。

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

前に示したJSON出力の例から、すべての行には、SELECTクエリで指定されたキースペース名(この場合はtravel-sample)に相関するキーがあります。

そのため、結果の最初の行であるJsonNodeを取得しました。 次に、ノードをトラバースしてairportnameキーに到達し、テキストとして出力されます。

前に共有した生のJSON出力の例では、返される結果の構造に従って、より明確になります。

6.2. N1QL DSLを使用したSELECTステートメント

クエリの作成に生の文字列リテラルを使用する以外に、使用しているJava SDKに付属のN1QL DSLを使用することもできます。

たとえば、上記の文字列クエリはDSLで定式化できます。

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

DSLは流fluentで、簡単に解釈できます。 データ選択クラスとメソッドはcom.couchbase.client.java.query.Selectクラスにあります。

i(), eq(), x(), s()のような式メソッドはcom.couchbase.client.java.query.dsl.Expressionクラスにあります。 DSLhereの詳細をご覧ください。

N1QL select statements can also have OFFSET, GROUP BY and ORDER BY clauses.構文は標準SQLの構文とほとんど同じであり、その参照はhereにあります。

N1QLのWHERE句は、その定義に論理演算子ANDOR、およびNOTを含めることができます。 これに加えて、N1QLには、>、==、!=、IS NULLothersなどの比較演算子が用意されています。

保存されたドキュメントへのアクセスを容易にする他の演算子もあります。string operatorsを使用してフィールドを連結し、単一の文字列を形成できます。nested operatorsを使用して、配列をスライスし、フィールドまたは要素をチェリーピックできます。

これらの動作を見てみましょう。

このクエリは、city列を選択し、country列が%で終わるtravel-sampleバケットからairportname列とfaa列をportname_faaとして連結します。 (t6)s '、および空港のlatitudeが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));

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

N1QLの他のステートメントを見てみましょう。 このセクションで習得した知識に基づいて構築します。

6.3. INSERTステートメント

N1QLのinsertステートメントの構文は次のとおりです。

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

travel-sampleはキースペース名ですが、unique_keyはそれに続く値オブジェクトに必要な重複しないキーです。

最後のセグメントは、返されるものを指定するRETURNINGステートメントです。

この場合、挿入されたドキュメントのiddocid.として返されます。ワイルドカード(*)は、docid.とは別に、追加されたドキュメントの他の属性も返される必要があることを示します。以下のサンプル結果。

Couchbase Web Consoleの[クエリ]タブで次のステートメントを実行すると、travel-sampleバケットに新しいレコードが挿入されます。

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

Javaアプリから同じことをしましょう。 まず、次のような生のクエリを使用できます。

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

これにより、挿入されたドキュメントのiddocidとして個別に返され、ドキュメント本文全体が個別に返されます。

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

ただし、Java SDKを使用しているため、Bucket APIを介してバケットに挿入されるJsonDocumentを作成することで、オブジェクトの方法でそれを行うことができます。

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

現在のように、同じ一意のIDがすでに存在する場合、insert()を使用すると例外がスローされます。

ただし、insert()は成功すると、挿入されたデータの一意のIDとエントリを含むJsonDocumentを返します。

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

SDKに下線を引くReactive Javaを使用して、Java SDKで一括操作を実行できます。 バッチプロセスを使用して、10個のドキュメントをバケットに追加しましょう。

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

まず、10個のドキュメントを生成し、それらをList;に入れてから、RxJavaを使用して一括操作を実行しました。

最後に、各挿入の結果を出力します–これはList.を形成するために蓄積されています

Java SDKで一括操作を実行するためのリファレンスは、hereにあります。 また、挿入ステートメントの参照はhereにあります。

6.4. UPDATEステートメント

N1QLにはUPDATEステートメントもあります。 一意のキーで識別されるドキュメントを更新できます。 updateステートメントを使用して、属性のSET(更新)値または属性全体のUNSET(削除)のいずれかを実行できます。

最近travel-sampleバケットに挿入したドキュメントの1つを更新しましょう。

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

上記のクエリでは、バケット内のcust_1エントリのname属性をSample Airline Updated,に更新し、更新された名前を返すようにクエリに指示します。

前述のように、同じIDでJsonDocumentを作成し、Bucket APIのupsert()を使用してドキュメントを更新することで、同じことを実現することもできます。

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

この次のクエリでは、UNSETコマンドを使用してname属性を削除し、影響を受けるドキュメントを返します。

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

返されるJSON文字列は次のとおりです。

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

欠落しているname属性に注意してください–ドキュメントオブジェクトから削除されました。 N1QL更新構文リファレンスはhere.にあります

そこで、新しいドキュメントの挿入とドキュメントの更新について見ていきます。 それでは、CRUDの頭字語の最後の部分であるDELETEを見てみましょう。

6.5. DELETEステートメント

DELETEクエリを使用して、以前に作成したドキュメントの一部を削除してみましょう。 一意のIDを使用して、USE KEYSキーワードでドキュメントを識別します。

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

N1QLDELETEステートメントもWHERE句を取ります。 したがって、条件を使用して、削除するレコードを選択できます。

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

バケットAPIのremove()を直接使用することもできます。

bucket.remove("cust_2");

はるかに簡単ですよね? はい、しかし今ではN1QLを使用してそれを行う方法もわかっています。 DELETE構文のリファレンスドキュメントはhereにあります。

7. N1QL関数とサブクエリ

N1QLは、構文だけではSQLに似ていませんでした。それはすべての機能にまで及びます。 SQLには、クエリ文字列内で使用できるCOUNT()のような関数がいくつかあります。

N1QLも同様に、クエリ文字列で使用できる機能を備えています。

たとえば、このクエリは、travel-sampleバケットにあるランドマークレコードの総数を返します。

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

上記の前の例では、UPDATEステートメントでMETA関数を使用して、更新されたドキュメントのidを返しました。

末尾の空白を削除し、小文字と大文字を作成し、さらに文字列にトークンが含まれているかどうかを確認できる文字列メソッドがあります。 クエリでこれらの関数のいくつかを使用してみましょう。

クエリでこれらの関数のいくつかを使用してみましょう。

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

上記のクエリは、travel-sampleバケットに新しいエントリを挿入します。 UUID()関数を使用して、LOWER()関数を使用して小文字に変換された一意のランダムIDを生成します。

NOW_MILLIS()メソッドを使用して、現在の時刻をミリ秒単位でcreated_at属性の値として設定しました。 N1QL関数の完全なリファレンスは、hereにあります。

サブクエリは時々役立つので、N1QLにはそれらが用意されています。 引き続きtravel-sampleバケットを使用して、特定の航空会社のすべてのルートの目的地の空港を選択し、それらが配置されている国を取得しましょう。

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

上記のクエリのサブクエリは括弧で囲まれ、airline_10に関連付けられているすべてのルートのdestinationairport属性をコレクションとして返します。

destinationairport属性は、travel-sampleバケット内のairportドキュメントのfaa属性に相関します。 WITHINキーワードは、N1QLのcollection operatorsの一部です。

これで、airline_10のすべてのルートの目的地の空港の国がわかりました。 その国のホテルを探して、何か面白いことをしましょう。

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

前のクエリは、最も外側のクエリのWHERE制約のサブクエリとして使用されました。 DISTINCTキーワードに注意してください-SQLと同じことをします-重複しないデータを返します。

この記事の前半で示したように、ここでのクエリの例はすべてSDKを使用して実行できます。

8. 結論

N1QLは、Couchbaseなどのドキュメントベースのデータベースを別のレベルに照会するプロセスを実行します。 このプロセスを簡素化するだけでなく、リレーショナルデータベースシステムからの切り替えも非常に簡単になります。

この記事ではN1QLクエリについて見てきました。主なドキュメントはhereにあります。 そして、Spring Data Couchbasehereについて学ぶことができます。

いつものように、完全なソースコードはover on Githubで利用できます。