Запрос Couchbase с N1QL

Опрос Couchbase с N1QL

1. обзор

В этой статье мы рассмотрим запрос к серверу Couchbase с помощьюN1QL. В упрощенном виде это SQL для баз данных NoSQL - с целью облегчить переход от баз данных SQL / Relational к системе баз данных NoSQL.

Есть несколько способов взаимодействия с сервером Couchbase; здесь мы будем использовать Java SDK для взаимодействия с базой данных, что типично для приложений Java.

Дальнейшее чтение:

Введение в Spring Data Couchbase

Быстрое и практическое использование Spring Data Couchbase для взаимодействия с сервером Couchbase DB.

Read more

Асинхронные пакетные операции в Couchbase

Узнайте, как выполнять эффективные пакетные операции в Couchbase с помощью асинхронного API-интерфейса Couchbase Java.

Read more

Введение в Couchbase SDK для Java

Краткое и практическое введение в использование Java Couchbase SDK.

Read more

2. Maven Зависимости

Мы предполагаем, что локальный сервер Couchbase уже настроен; если это не так, этотguide может помочь вам начать работу.

Давайте теперь добавим зависимость для Couchbase Java SDK вpom.xml:


    com.couchbase.client
    java-client
    2.5.0

Последнюю версию Couchbase Java SDK можно найти наMaven Central.

Мы также будем использовать библиотеку Джексона для сопоставления результатов, возвращаемых по запросам; давайте также добавим его зависимость кpom.xml:


    com.fasterxml.jackson.core
    jackson-databind
    2.9.1

Последнюю версию библиотеки Джексона можно найти наMaven Central.

3. Подключение к серверу Couchbase

Теперь, когда проект настроен с правильными зависимостями, давайте подключимся к Couchbase Server из приложения Java.

Во-первых, нам нужно запустить Couchbase Server - если он еще не запущен.

Руководство по запуску и остановке сервера Couchbase можно найти вhere.

Давайте подключимся к CouchbaseBucket:

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

Мы подключились к CouchbaseCluster, а затем получили объектBucket.

Имя сегмента в кластере Couchbase -test, и его можно создать с помощью веб-консоли Couchbase. Когда мы завершим все операции с базой данных, мы можем закрыть конкретную открытую корзину.

С другой стороны, мы можем отключиться от кластера, что в итоге закроет все сегменты:

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 мы создали документ JSON, используяJsonDocument.create(),, который мы вставим в корзину. Обратите внимание, что мы генерируем случайныйid, используя классjava.util.UUID.

Вставленный документ можно увидеть в веб-консоли Couchbase вhttp://localhost:8091 или вызвавbucket.get() с егоid:

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

5. Базовый запрос N1QLSELECT

N1QL - это расширенный набор SQL, и его синтаксис, естественно, выглядит аналогично.

Например, N1QL для выбора всех документов вtest bucket:

SELECT * FROM test

Давайте выполним этот запрос в приложении:

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

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

Сначала мы создаем первичный индекс, используяcreateN1qlPrimaryIndex(), он будет проигнорирован, если он был создан ранее; его создание является обязательным перед выполнением любого запроса.

Затем мы используемbucket.query() для выполнения запроса N1QL.

N1qlQueryResult - это объектIterable<N1qlQueryRow>, поэтому мы можем распечатать каждую строку, используяforEach():

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

Из возвращенногоresult мы можем получить объектN1qlMetrics, вызвавresult.info(). Из объекта метрики мы можем получить представление о возвращаемом результате - например, результат и количество ошибок:

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

На возвращенномresult мы можем использоватьresult.parseSuccess(), чтобы проверить, является ли запрос синтаксически правильным и успешно проанализирован. Мы можем использоватьresult.finalSuccess(), чтобы определить, было ли выполнение запроса успешным.

6. Операторы запроса N1QL

Давайте посмотрим на различные операторы запросов N1QL и различные способы их выполнения с помощью Java SDK.

6.1. SELECT Заявление

ОператорSELECT в NIQL аналогичен стандартному SQLSELECT. Он состоит из трех частей:

  • SELECT определяет проекцию возвращаемых документов

  • FROM описывает пространство ключей для выборки документов; пространство ключей является синонимом имени таблицы в системах баз данных SQL.

  • WHERE указывает дополнительные критерии фильтрации

Сервер Couchbase поставляется с некоторыми примерами (базами данных). Если они не были загружены во время начальной настройки, в разделе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. Обратите внимание, что имя пространства ключей должно быть помещено в backtick (`), потому что оно содержит дефис.

N1qlQueryResult - это просто оболочка для необработанных данных JSON, возвращаемых из базы данных. Он расширяетIterable<N1qlQueryRow> и может быть зациклен.

Вызовresult1.allRows() вернет все строки в объектеList<N1qlQueryRow>. Это полезно для обработки результатов с помощью APIStream и / или доступа к каждому результату через индекс:

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

Мы получили первую строку возвращенных результатов, и мы используемrow.value() для полученияJsonObject, который отображает строку в пару ключ-значение, а ключ соответствует имени столбца.

Итак, мы получили значение столбцаname, для первой строки, используяget(). Это так просто.

До сих пор мы использовали простой запрос N1QL. Давайте посмотрим на операторparameterized в N1QL.

В этом запросе мы будем использовать подстановочный знак (*) для выбора всех полей в записяхtravel-sample, гдеtype - этоairport.

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 для хранения параметров в виде пары ключ-значение. Значение ключа «type', в объектеpVal» будет использоваться для замены заполнителя$type в строкеquery.

N1qlQuery.parameterized() принимает строку запроса, содержащую один или несколько заполнителей и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());
}

Мы обработали каждую строку в результате с помощью APIStream. Мы сопоставили каждую строку с объектомJsonNode, а затем возвращаем результат какList изJsonNodes.

Теперь мы можем использовать метод для обработки возвращенного результата из последнего запроса:

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. ОператорSELECT с использованием N1QL DSL

Помимо использования необработанных строковых литералов для построения запросов, мы также можем использовать N1QL DSL, который поставляется с Java SDK, который мы используем.

Например, приведенный выше строковый запрос может быть сформулирован с помощью DSL следующим образом:

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

DSL свободно говорит и может быть легко интерпретирован. Классы и методы выбора данных находятся в классе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.

ПредложениеWHERE в N1QL может принимать в своих определениях логические операторыAND,OR иNOT. В дополнение к этому в N1QL есть операторы сравнения, такие как>, ==,! =,IS NULL иothers.

Существуют также другие операторы, которые упрощают доступ к сохраненным документам -string operators можно использовать для объединения полей в одну строку, аnested operators можно использовать для нарезки массивов и полей или элементов выбора вишни.

Давайте посмотрим на это в действии.

Этот запрос выбирает столбецcity, объединяет столбцыairportname иfaa какportname_faa из корзиныtravel-sample, где столбецcountry заканчивается на‘States' ', а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 INTO `travel-sample` ( KEY, VALUE )
VALUES("unique_key", { "id": "01", "type": "airline"})
RETURNING META().id as docid, *;

Гдеtravel-sample - это имя пространства ключей,unique_key - это обязательный неповторяющийся ключ для следующего за ним объекта значения.

Последний сегмент - это операторRETURNING, который указывает, что возвращается.

В этом случаеid вставленного документа возвращается какdocid. Подстановочный знак (*) означает, что другие атрибуты добавленного документа также должны быть возвращены - отдельно отdocid. См. образец результата ниже.

Выполнение следующего оператора на вкладке Query веб-консоли Couchbase вставит новую запись в корзину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);

Это вернетid вставленного документа какdocid отдельно и полное тело документа отдельно:

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

Однако, поскольку мы используем Java SDK, мы можем сделать это объектным способом, создавJsonDocument, который затем вставляется в корзину через 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.

Как и сейчас, использованиеinsert() вызовет исключение, если тот же уникальный идентификатор уже существует.

Однакоinsert() в случае успеха вернет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, *;

Мы можем выполнять массовые операции с Java SDK, используя Reactive Java, который подчеркивает SDK. Давайте добавим десять документов в корзину с помощью пакетной обработки:

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

Сначала мы генерируем десять документов и помещаем их вList;, затем мы использовали RxJava для выполнения массовой операции.

Наконец, мы распечатываем результат каждой вставки, который был накоплен для формированияList.

Ссылку на выполнение массовых операций в Java SDK можно найти вhere. Также можно найти ссылку на оператор вставкиhere.

6.4. UPDATE Заявление

N1QL также имеет операторUPDATE. Он может обновлять документы, идентифицированные по их уникальным ключам. Мы можем использовать оператор обновления либо для значенийSET (обновление) атрибута, либо дляUNSET (удаление) атрибута в целом.

Давайте обновим один из документов, которые мы недавно вставили в корзинуtravel-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);

В приведенном выше запросе мы обновили атрибутname записиcust_1 в корзине доSample Airline Updated, и проинструктировали запрос вернуть обновленное имя.

Как было сказано ранее, мы также можем достичь того же, построивJsonDocument с тем же идентификатором и используяupsert() APIBucket для обновления документа:

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, чтобы удалить некоторые из документов, которые мы создали ранее. Мы будем использовать уникальный идентификатор, чтобы идентифицировать документ с ключевым словом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));

Мы также можем напрямую использоватьremove() из bucket API:

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'

В предыдущих примерах выше мы использовали функциюMETA в оператореUPDATE, чтобы вернуть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().

Метод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")

Подзапрос в приведенном выше запросе заключен в круглые скобки и возвращает атрибутdestinationairport всех маршрутов, связанных сairline_10, в виде коллекции.

Атрибутыdestinationairport коррелируют с атрибутомfaa в документахairport в корзинеtravel-sample. Ключевое словоWITHIN является частьюcollection operators в N1QL.

Теперь, когда у нас есть аэропорт назначения для всех маршрутов для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.