Рубрики
Без рубрики

Запрос Couchbase с помощью N1QL

Узнайте о запросе сервера Couchbase с помощью N1QL – эквивалента SQL для баз данных NoSQL.

Автор оригинала: baeldung.

1. Обзор

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

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

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

Введение в базу данных Spring Data Couchbase

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

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

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

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

Теперь давайте добавим зависимость для 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 из приложения Java.

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

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

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

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

Что мы сделали, так это подключились к Couchbase Cluster , а затем получили объект 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);

Во-первых, мы создали JSON personObj и предоставили некоторые исходные данные. Ключи можно рассматривать как столбцы в системе реляционных баз данных.

Из объекта person мы создали документ JSON с помощью JsonDocument.create(), который мы вставим в корзину. Обратите внимание, что мы генерируем случайный id с помощью java.util.UUID класс.

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

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

5. Базовый запрос ВЫБОРА N1QL

N1QL-это надмножество SQL, и его синтаксис, естественно, выглядит аналогично.

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

SELECT * FROM test

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

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

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

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

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

Результат запроса N1ql является итерируемым объектом<Строка запроса N1ql> , и поэтому мы можем распечатать каждую строку с помощью forEach() :

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

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

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

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

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

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

6.1. Инструкция SELECT

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

  • SELECT определяет проекцию возвращаемых документов
  • FROM описывает пространство ключей для извлечения документов; пространство ключей является синонимом имени таблицы в системах баз данных SQL
  • ГДЕ указывает дополнительные критерии фильтрации

Сервер Couchbase поставляется с некоторыми образцами ведер (баз данных). Если они не были загружены во время первоначальной настройки, в разделе Настройки веб-консоли есть специальная вкладка для их настройки.

Мы будем использовать travel-sample bucket. Ведро travel-sample содержит данные по авиакомпаниям, ориентирам, аэропортам, отелям и маршрутам. Модель данных можно найти здесь .

Давайте выберем 100 записей авиакомпаний из данных выборки путешествий:

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

Запрос N1QL, как видно выше, очень похож на SQL. Обратите внимание, что имя пространства клавиш должно быть помещено в backtick ( ` ), поскольку оно содержит дефис.

Результат запроса N1ql это просто оболочка вокруг необработанных данных JSON, возвращенных из базы данных. Он расширяется Итерируемый и может быть закольцован.

Вызов результат 1.all Rows() вернет все строки в List<Строка запроса N1ql> объект. Это полезно для обработки результатов с помощью 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 – который сопоставляет строку с парой ключ-значение, а ключ соответствует имени столбца.

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

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

В этом запросе мы будем использовать подстановочный знак (*) для выбора всех полей в записях 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 для хранения параметров в виде пары ключ-значение. Значение ключа ‘ type’, в объекте pVal , будет использоваться для замены заполнителя $type в строке запроса .

N1ql Query.parameterized() принимает строку запроса, содержащую один или несколько заполнителей и JSONObject , как показано выше.

В предыдущем примере запроса выше мы выбираем только столбец – имя. Это позволяет легко отобразить возвращаемый результат в 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"
    }
  },

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

Поэтому давайте создадим метод, который примет результат запроса N1ql , а затем сопоставим каждую строку в результате с объектом 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 , а затем вернули результат в виде Списка узлов Json.

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

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

Из примера вывода JSON, показанного ранее, каждая строка имеет ключ, коррелирующий с именем пространства ключей, указанным в запросе SELECT , который в данном случае является travel – sample .

Таким образом, мы получили первую строку в результате, которая является JsonNode . Затем мы пересекаем узел, чтобы добраться до ключа airport name , который затем печатается в виде текста.

Пример необработанного вывода JSON, опубликованного ранее, обеспечивает большую ясность в соответствии со структурой возвращаемого результата.

6.2. Инструкция SELECT С использованием DSL N1QL

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

Методы выражения, такие как i(), eq(), x(), s() находятся в находятся в класс. Подробнее о DSL здесь .

Операторы N1QL select также могут иметь предложения OFFSET , GROUP BY и ORDER BY . Синтаксис в значительной степени похож на стандартный SQL, и его ссылку можно найти здесь .

Предложение WHERE N1QL может принимать логические операторы И , ИЛИ и НЕ в своих определениях. В дополнение к этому, в N1QL предусмотрены операторы сравнения, такие как >, ==, !=, ИМЕЕТ ЗНАЧЕНИЕ NULL и другие .

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

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

Этот запрос выбирает столбец city , объединяет столбцы airportname и faa как portname_faa из travel-sample bucket, где столбец country заканчивается на ‘States’ ‘, а широта аэропорта больше или равна 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 -необходимый не дублирующий ключ для объекта value, который следует за ним.

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

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

Выполнение следующей инструкции на вкладке Запрос веб-консоли Couchbase вставит новую запись в travel-sample bucket:

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, мы можем сделать это объектным способом, создав документ Json , который затем вставляется в корзину через Bucket API:

JsonObject ob = JsonObject.create()
  .put("id", "1293")
  .put("name", "Sample Airline")
  .put("type", "airline");
bucket.insert(JsonDocument.create("cust1295", ob));

Вместо использования insert() мы можем использовать upsert () , который обновит документ, если существует существующий документ с тем же уникальным идентификатором cust1295 .

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

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

Синтаксис для массовой вставки с использованием 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, используя реактивную 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);

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

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

Ссылку на выполнение массовых операций в Java SDK можно найти здесь . Кроме того, ссылку на инструкцию insert можно найти здесь .

6.4. Заявление об ОБНОВЛЕНИИ

В N1QL также есть оператор UPDATE . Он может обновлять документы, идентифицированные по их уникальным ключам. Мы можем использовать оператор update либо для УСТАНОВКИ (обновления) значений атрибута, либо для СНЯТИЯ (удаления) атрибута в целом.

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

В приведенном выше запросе мы обновили атрибут name записи cust_1 в корзине в Образец обновленной авиакомпании, и мы поручаем запросу вернуть обновленное имя.

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

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 можно найти здесь.

Итак, мы рассмотрим вставку новых документов и обновление документов. Теперь давайте посмотрим на последнюю часть аббревиатуры CRUD – DELETE .

6.5. УДАЛИТЬ заявление

Давайте используем запрос DELETE для удаления некоторых документов, которые мы создали ранее. Мы будем использовать уникальный идентификатор для идентификации документа с помощью ключевого слова USE KEYS :

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

Оператор N1QL DELETE также принимает предложение WHERE . Таким образом, мы можем использовать условия для выбора записей, подлежащих удалению:

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

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

bucket.remove("cust_2");

Гораздо проще, верно? Да, но теперь мы также знаем, как это сделать с помощью N1QL. Справочный документ для синтаксиса DELETE можно найти здесь .

7. Функции N1QL и подзапросы

N1QL не просто напоминал SQL в отношении одного только синтаксиса; он полностью соответствует некоторым функциональным возможностям. В SQL у нас есть некоторые функции, такие как COUNT () , которые можно использовать в строке запроса.

N1QL, таким же образом, имеет свои функции, которые могут быть использованы в строке запроса.

Например, этот запрос вернет общее количество записей ориентиров, которые находятся в travel-sample bucket:

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 можно найти здесь .

Подзапросы иногда пригодятся, и у N1QL есть для них резерв. Все еще используя travel-sample bucket, давайте выберем аэропорт назначения всех маршрутов для конкретной авиакомпании – и получим страну, в которой они находятся:

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 , в виде коллекции.

Атрибуты аэропорт назначения коррелируют с атрибутом faa в документах airport в корзине travel-sample . Ключевое слово ВНУТРИ является частью операторов collection в 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 в этой статье; основную документацию можно найти здесь . И вы можете узнать о Spring Data Couchbase здесь .

Как всегда, полный исходный код доступен на Github .