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 .