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 ListextractJsonResult(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.
Теперь мы можем использовать этот метод для обработки возвращенного результата последнего запроса:
Listlist = 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)); Listlist3 = 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. Давайте добавим десять документов в корзину с помощью пакетного процесса:
Listdocuments = 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 .