Перейти к основному содержимому

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

· 15 мин. чтения

1. Обзор

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

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

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

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

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

<dependency>
<groupId>com.couchbase.client</groupId>
<artifactId>java-client</artifactId>
<version>2.5.0</version>
</dependency>

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

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

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.0</version>
</dependency>

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

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

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

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

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

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

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

Что мы сделали, так это подключились к кластеру Couchbase, а затем получили объект Bucket .

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

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

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

4. Вставка документов

Couchbase — это система баз данных, ориентированная на документы. Давайте добавим новый документ в тестовую корзину:

JsonObject personObj = JsonObject.create()
.put("name", "John")
.put("email", "john@doe.com")
.put("interests", JsonArray.from("Java", "Nigerian Jollof"));

String id = UUID.randomUUID().toString();
JsonDocument doc = JsonDocument.create(id, personObj);
bucket.insert(doc);

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

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

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

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

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

N1QL — это надмножество SQL, и его синтаксис, естественно, похож.

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

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

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

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

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

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

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

6.1. Оператор ВЫБЕРИТЕ

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

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

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

Мы будем использовать ведро с образцами для путешествий . Сегмент Travel-sample содержит данные об авиакомпаниях, достопримечательностях, аэропортах, отелях и маршрутах. Модель данных можно найти здесь .

Давайте выберем 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 , который сопоставляет строку с парой ключ-значение, а ключ соответствует имени столбца.

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

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

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

N1qlQuery.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"
}
},

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

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

Мы выбираем JsonNode, потому что он может обрабатывать широкий спектр структур данных JSON, и мы можем легко перемещаться по нему:

public static List<JsonNode> 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<JsonNode> list = extractJsonResult(r2);
System.out.println(
list.get(0).get("travel-sample").get("airportname").asText());

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

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

Пример необработанных выходных данных 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 . Подробнее о DSL здесь .

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

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

Есть и другие операторы, упрощающие доступ к хранимым документам: строковые операторы можно использовать для объединения полей в одну строку, а вложенные операторы можно использовать для нарезки массивов и выбора полей или элементов.

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

Этот запрос выбирает столбец города , объединяет столбцы airportname и faa как portname_faa из сегмента выборки путешествий, где столбец страны заканчивается на «Штаты» , а широта аэропорта больше или равна 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<JsonNode> 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. ВСТАВИТЬ Заявление

Синтаксис оператора вставки в N1QL:

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

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

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

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

Выполнение следующего оператора на вкладке «Запрос» веб-консоли Couchbase добавит новую запись в корзину с образцами путешествий :

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

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

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

Однако, поскольку мы используем Java SDK, мы можем сделать это объектным путем, создав JsonDocument , который затем вставляется в корзину через 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() в случае успеха вернет 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<JsonDocument> 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<JsonDocument> r5 = Observable
.from(documents)
.flatMap(doc -> bucket.async().insert(doc))
.toList()
.last()
.toBlocking()
.single();

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

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

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

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

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

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() Bucket API для обновления документа:

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

В следующем запросе воспользуемся командой UNSET , чтобы удалить атрибут имени и вернуть затронутый документ:

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.remove("cust_2");

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

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

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

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

Например, этот запрос вернет общее количество записей ориентиров, которые находятся в сегменте выборки путешествий :

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

В предыдущих примерах выше мы использовали функцию META в операторе UPDATE для возврата идентификатора обновленного документа.

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

Давайте используем некоторые из этих функций в запросе:

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

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 всех маршрутов, связанных с авиакомпанией_10 . ``

Атрибуты destinationairport соотносятся с атрибутом faa в документах аэропорта в сегменте Travel-Sample . Ключевое слово WITHIN является частью операторов коллекции в N1QL.

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