1. 概述

本教程将介绍几种基础的MapReduce视图,并演示如何使用Couchbase Java SDK进行查询。

2. Maven依赖

在Maven项目中使用Couchbase,需在pom.xml中添加SDK依赖:

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

最新版本可在Maven Central获取。

3. MapReduce视图

Couchbase中,MapReduce视图是一种索引类型,用于查询数据桶。它通过JavaScript的map函数和可选的reduce函数定义。

3.1. map函数

map函数对每个文档执行一次。视图创建时,会对桶中所有文档执行一次map函数,结果存储在桶中。

视图创建后,仅对新插入或更新的文档执行map函数以增量更新视图。

由于map函数结果存储在数据桶中,视图查询延迟极低。

示例:创建一个索引,匹配所有type为*"StudentGrade"且包含name*字段的文档:

function (doc, meta) {
    if(doc.type == "StudentGrade" && doc.name) {    
        emit(doc.name, null);
    }
}

关键点

  • emit函数指定索引键(第一个参数)和关联值(第二个参数)
  • 此例中仅将name存入索引键,值设为null

当三个文档的name为*"John Doe"时,索引键"John Doe"*会关联这三个文档。

3.2. reduce函数

reduce函数用于对map函数结果执行聚合计算。Couchbase管理界面提供内置函数:

  • _count:计数
  • _sum:求和
  • _stats:统计

也可自定义reduce函数实现复杂聚合,后续将演示内置函数用法。

4. 视图与查询操作

4.1. 视图组织

视图按桶分组存储在设计文档中。理论上单个设计文档可包含无限视图,但建议:

  • ⚠️ 性能优化:每个设计文档不超过10个视图

新创建的视图默认为开发视图,测试满意后需发布生产视图

4.2. 构建查询

通过设计文档名和视图名创建ViewQuery对象:

ViewQuery query = ViewQuery.from("design-document-name", "view-name");

查询开发视图需添加development()方法:

ViewQuery query 
  = ViewQuery.from("design-doc-name", "view-name").development();

4.3. 执行查询

使用ViewQuery对象获取ViewResult

ViewResult result = bucket.query(query);

4.4. 处理结果

遍历ViewResult获取文档ID和内容:

for(ViewRow row : result.allRows()) {
    JsonDocument doc = row.document();
    String id = doc.id();
    String json = doc.content().toString();
}

5. 示例应用

后续将基于以下格式的学生成绩文档编写视图和查询(成绩范围0-100):

{ 
    "type": "StudentGrade",
    "name": "John Doe",
    "course": "History",
    "hours": 3,
    "grade": 95
}

文档存储在*"baeldung-tutorial"桶,视图存于"studentGrades"*设计文档。连接桶的代码:

Bucket bucket = CouchbaseCluster.create("127.0.0.1")
  .openBucket("baeldung-tutorial");

6. 精确匹配查询

6.1. 单键匹配

查找特定课程的成绩,创建*"findByCourse"*视图:

function (doc, meta) {
    if(doc.type == "StudentGrade" && doc.course && doc.grade) {
        emit(doc.course, null);
    }
}

查询历史课程成绩:

ViewQuery query 
  = ViewQuery.from("studentGrades", "findByCourse").key("History");

6.2. 多键匹配

查找数学和科学课程成绩,使用keys()方法:

ViewQuery query = ViewQuery
  .from("studentGrades", "findByCourse")
  .keys(JsonArray.from("Math", "Science"));

7. 范围查询

7.1. 单字段范围查询

创建*"findByGrade"*视图:

function (doc, meta) {
    if(doc.type == "StudentGrade" && doc.grade) {
        emit(doc.grade, null);
    }
}

查询B级成绩(80-89分):

ViewQuery query = ViewQuery.from("studentGrades", "findByGrade")
  .startKey(80)
  .endKey(89)
  .inclusiveEnd(true);

注意

  • 范围查询的startKey始终包含
  • 若成绩均为整数,以下查询等效:
    ViewQuery query = ViewQuery.from("studentGrades", "findByGrade")
      .startKey(80)
      .endKey(90)
      .inclusiveEnd(false);
    

查询A级成绩(≥90分):

ViewQuery query = ViewQuery
  .from("studentGrades", "findByGrade")
  .startKey(90);

查询不及格成绩(<60分):

ViewQuery query = ViewQuery
  .from("studentGrades", "findByGrade")
  .endKey(60)
  .inclusiveEnd(false);

7.2. 多字段范围查询

创建*"findByCourseAndGrade"*视图,按课程和成绩排序:

function (doc, meta) {
    if(doc.type == "StudentGrade" && doc.course && doc.grade) {
        emit([doc.course, doc.grade], null);
    }
}

索引键排序示例:

["History", 80]
["History", 90]
["Math", 82]
["Math", 97]

查询数学B级成绩(80-89分):

ViewQuery query = ViewQuery
  .from("studentGrades", "findByCourseAndGrade")
  .startKey(JsonArray.from("Math", 80))
  .endKey(JsonArray.from("Math", 89))
  .inclusiveEnd(true);

⚠️ 踩坑提示

  • 查询A级成绩需设置上限,否则会包含字典序大于"Math"的课程:
    ViewQuery query = ViewQuery
      .from("studentGrades", "findByCourseAndGrade")
      .startKey(JsonArray.from("Math", 90))
      .endKey(JsonArray.from("Math", 100));
    

查询数学不及格成绩需设置下限:

ViewQuery query = ViewQuery
  .from("studentGrades", "findByCourseAndGrade")
  .startKey(JsonArray.from("Math", 0))
  .endKey(JsonArray.from("Math", 60))
  .inclusiveEnd(false);

获取数学最高5个成绩(降序排序):

ViewQuery query = ViewQuery
  .from("studentGrades", "findByCourseAndGrade")
  .descending()
  .startKey(JsonArray.from("Math", 100))
  .endKey(JsonArray.from("Math", 0))
  .inclusiveEnd(true)
  .limit(5);

8. 聚合查询

8.1. 使用*count()*函数

创建*"countStudentsByCourse"*视图统计各课程学生数:

function (doc, meta) {
    if(doc.type == "StudentGrade" && doc.course && doc.name) {
        emit([doc.course, doc.name], null);
    }
}

配置使用内置_count函数。查询代码:

ViewQuery query = ViewQuery
  .from("studentGrades", "countStudentsByCourse")
  .reduce()
  .groupLevel(1);

处理聚合结果:

ViewResult result = bucket.query(query);
Map<String, Long> numStudentsByCourse = new HashMap<>();
for(ViewRow row : result.allRows()) {
    JsonArray keyArray = (JsonArray) row.key();
    String course = keyArray.getString(0);
    long count = Long.valueOf(row.value().toString());
    numStudentsByCourse.put(course, count);
}

8.2. 使用*sum()*函数

创建*"sumCreditsByStudent"*视图统计学生总学分:

function (doc, meta) {
    if(doc.type == "StudentGrade"
         && doc.name
         && doc.course
         && doc.hours) {
        emit([doc.name, doc.course], doc.hours);
    }
}

配置使用内置_sum函数。查询代码:

ViewQuery query = ViewQuery
  .from("studentGrades", "sumCreditsByStudent")
  .reduce()
  .groupLevel(1);

处理结果:

ViewResult result = bucket.query(query);
Map<String, Long> hoursByStudent = new HashMap<>();
for(ViewRow row : result.allRows()) {
    String name = (String) row.key();
    long sum = Long.valueOf(row.value().toString());
    hoursByStudent.put(name, sum);
}

8.3. 计算平均绩点

结合两个视图计算GPA(A=4, B=3, C=2, D=1):

  1. 使用已有sumCreditsByStudent视图
  2. 创建*"sumGradePointsByStudent"*视图:
function (doc, meta) {
    if(doc.type == "StudentGrade"
         && doc.name
         && doc.hours
         && doc.grade) {
        if(doc.grade >= 90) {
            emit(doc.name, 4*doc.hours);
        }
        else if(doc.grade >= 80) {
            emit(doc.name, 3*doc.hours);
        }
        else if(doc.grade >= 70) {
            emit(doc.name, 2*doc.hours);
        }
        else if(doc.grade >= 60) {
            emit(doc.name, doc.hours);
        }
        else {
            emit(doc.name, 0);
        }
    }
}

查询并处理结果:

ViewQuery query = ViewQuery.from(
  "studentGrades",
  "sumGradePointsByStudent")
  .reduce()
  .groupLevel(1);
ViewResult result = bucket.query(query);

Map<String, Long> gradePointsByStudent = new HashMap<>();
for(ViewRow row : result.allRows()) {
    String course = (String) row.key();
    long sum = Long.valueOf(row.value().toString());
    gradePointsByStudent.put(course, sum);
}

计算GPA:

Map<String, Float> result = new HashMap<>();
for(Entry<String, Long> creditHoursEntry : hoursByStudent.entrySet()) {
    String name = creditHoursEntry.getKey();
    long totalHours = creditHoursEntry.getValue();
    long totalGradePoints = gradePointsByStudent.get(name);
    result.put(name, ((float) totalGradePoints / totalHours));
}

9. 总结

本文演示了:

  • 编写基础MapReduce视图
  • 构建与执行视图查询
  • 处理查询结果

完整代码见GitHub项目。更多细节参考:


原始标题:Querying Couchbase with MapReduce Views

« 上一篇: Spring CORS配置详解
» 下一篇: Spark Java 框架入门