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 查询。
N1qlQueryResult
是 Iterable<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-sample
中 type
为 airport
的所有字段,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
。
前例只选择单列(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 语句还支持 OFFSET
、GROUP BY
和 ORDER BY
子句,语法与标准 SQL 类似(参考文档)。
WHERE
子句支持逻辑运算符 AND
、OR
、NOT
,以及比较运算符(如 >
, ==
, !=
, IS NULL
等,见文档)。此外还有简化文档访问的运算符:
看一个综合示例:查询 travel-sample
中 country
以 'States'
结尾、纬度 >= 70
的机场,返回 city
列,并将 airportname
和 faa
列连接为 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-sample
中 cust_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 获取。