1. 概述

本文将介绍如何使用 N1QL 查询 Couchbase 服务器。简单来说,N1QL 就是 NoSQL 数据库的 SQL——它的目标是让开发者从 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.7.2</version>
</dependency>

最新版本可在 Maven Central 查找。

我们还需要 Jackson 库来映射查询结果,同样添加到 pom.xml

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

最新版本可在 Maven Central 查找。

3. 连接到 Couchbase 服务器

项目配置好依赖后,现在从 Java 应用连接 Couchbase 服务器。

首先确保 Couchbase 服务器正在运行(启动和停止指南见这里)。

连接到 Couchbase Bucket:

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

我们连接到 Couchbase 集群(Cluster),然后获取 Bucket 对象。这里的 bucket 名为 test,可通过 Couchbase Web Console 创建。完成所有数据库操作后,可以关闭已打开的 bucket:

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

⚠️ 断开集群连接会自动关闭所有 bucket。

4. 插入文档

Couchbase 是文档型数据库系统。让我们向 test bucket 插入一个新文档:

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

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

首先创建 JSON 对象 personObj 并填充初始数据(键可视为关系型数据库中的列)。然后通过 JsonDocument.create() 创建 JSON 文档,使用 java.util.UUID 生成随机 ID,最后插入 bucket。

插入的文档可通过 Couchbase Web Console(http://localhost:8091)查看,或通过 ID 调用 bucket.get() 获取:

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

5. 基础 N1QL SELECT 查询

N1QL 是 SQL 的超集,语法自然相似。例如,查询 test bucket 中所有文档的 N1QL 语句:

SELECT * FROM test

在应用中执行此查询:

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

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

首先通过 createN1qlPrimaryIndex() 创建主索引(已创建则忽略),执行查询前必须创建主索引。然后使用 bucket.query() 执行 N1QL 查询。

N1qlQueryResultIterable<N1qlQueryRow> 对象,可通过 forEach() 遍历每行:

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

从返回的 result 中,调用 result.info() 可获取 N1qlMetrics 对象,从中获取结果统计信息(如结果数和错误数):

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 语句

N1QL 的 SELECT 语句与标准 SQL 类似,包含三部分:

  • SELECT:定义返回文档的投影
  • FROM:指定从哪个 keyspace 获取文档(keyspace 类似 SQL 中的表名)
  • WHERE:设置额外过滤条件

Couchbase 服务器自带示例 bucket(如未在初始设置时加载,可在 Web Console 的 Settings 选项卡中设置)。我们将使用 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));

⚠️ keyspace 名称包含连字符时需用反引号(`)包裹。

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(将行映射为键值对),键对应列名。因此通过 get("name") 即可获取首行的 name 列值。

接下来看参数化查询(避免 SQL 注入)。以下查询使用通配符(*)选择 travel-sampletypeairport 的所有字段,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 占位符会被 pValtype 键的值替换。N1qlQuery.parameterized() 接受含占位符的查询字符串和 JsonObject

前例只选择单列(name),映射结果到 JsonObject 很简单。但使用通配符(*)时,返回的是原始 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,返回 List<JsonNode>。现在处理上例的查询结果:

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

根据前面的 JSON 示例,每行都有一个键对应 SELECT 查询中指定的 keyspace 名称(此处为 travel-sample)。获取首行(JsonNode)后,遍历到 airportname 键并输出为文本。

6.2. 使用 N1QL DSL 的 SELECT 语句

除原始字符串构建查询外,还可使用 Java SDK 自带的 N1QL 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 中。

N1QL SELECT 语句还支持 OFFSETGROUP BYORDER BY 子句,语法与标准 SQL 类似(参考文档)。

WHERE 子句支持逻辑运算符 ANDORNOT,以及比较运算符(如 >, ==, !=, IS NULL 等,见文档)。此外还有简化文档访问的运算符:

看一个综合示例:查询 travel-samplecountry'States' 结尾、纬度 >= 70 的机场,返回 city 列,并将 airportnamefaa 列连接为 portname_faa

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

6.3. INSERT 语句

N1QL 的 INSERT 语法:

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

travel-sample 是 keyspace 名称,unique_key 是值对象的唯一键(不可重复)。RETURNING 指定返回内容:此处返回插入文档的 id(别名为 docid)和完整文档(通配符 *)。

在 Couchbase Web Console 的 Query 标签页执行以下语句插入记录:

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() 时若 ID 已存在会抛出异常。改用 upsert() 可更新已存在文档(ID 为 cust1295)。

批量插入的 N1QL 语法:

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

使用 Java SDK 的 RxJava 实现批量插入(示例插入 10 条):

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

先生成 10 个文档存入 List,再用 RxJava 执行批量操作,最后打印所有插入结果(累积为 List)。

6.4. UPDATE 语句

N1QL 的 UPDATE 语句可更新文档(通过唯一键标识)。使用 SET 更新属性值,或用 UNSET 删除属性。

更新 travel-samplecust_1 文档的 name 属性:

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

使用 USE KEYS 指定文档键,更新 name 并返回更新后的值。同样可通过 Java SDK 实现:

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

使用 UNSET 删除 cust_2 文档的 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 中 name 属性已被删除:

{  
  "travel-sample":{  
    "id":2,
    "type":"airline"
  }
}

6.5. DELETE 语句

使用 DELETE 删除之前创建的文档。通过 USE KEYS 指定文档键:

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

DELETE 也支持 WHERE 子句条件删除:

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

✅ 更简单的方式是直接调用 bucket API 的 remove()

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() 函数返回更新文档的 id。其他常用函数:

  • 字符串函数:修剪空白、大小写转换、检查子串等
  • 日期函数:如 NOW_MILLIS() 返回当前时间戳

示例插入记录时使用函数:

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, *

使用 UUID() 生成随机 ID 并转为小写,NOW_MILLIS() 设置 created_at 为当前时间戳(毫秒)。完整函数列表见文档

子查询在 N1QL 中同样支持。以下查询查找 airline_10 航空所有航线的目的地机场及其所在国家:

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 所有航线的 destinationairport 集合。WITHIN 是 N1QL 的集合运算符,用于检查 faa 是否在子查询结果中。

进一步查找这些国家中的酒店:

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 中一致)。

8. 总结

N1QL 将 Couchbase 这类文档数据库的查询能力提升到新高度。它不仅简化了查询过程,也让从关系型数据库迁移更加容易。

本文介绍了 N1QL 查询基础,完整文档见官方参考。关于 Spring Data Couchbase 可参考这里

本文的完整源代码可在 GitHub 获取。


原始标题:Querying Couchbase with N1QL

« 上一篇: Apache Cayenne ORM 介绍
» 下一篇: RxJava 调度器详解