Docs 菜单
Docs 主页
/ / /
Kotlin 协程
/

聚合表达式操作

在此页面上

  • Overview
  • 如何使用运算
  • 构造器方法
  • 操作
  • 算术运算
  • 数组运算
  • 布尔运算
  • 比较运算
  • 条件操作
  • 便捷操作
  • 转换操作
  • 日期操作符
  • 文档操作
  • 映射操作
  • 字符串操作
  • 类型检查操作

在本指南中,您可了解如何使用 MongoDB Kotlin 驱动程序来构建可在聚合管道中使用的表达式。您可以使用可发现且类型安全的 Java 方法而不是 BSON 文档来执行表达式操作。由于这些方法遵循连贯接口模式,因此可将聚合操作链接在一起来创建更紧凑且可更自然阅读的代码。

本指南中的操作使用 com.mongodb.client.model.mql 包。这些方法提供了使用 Query API 的惯用方式,而 Query API 正是驱动程序与 MongoDB 部署交互的机制。 要了解有关 Query API的更多信息,请参阅MongoDB Server手册文档。

本指南中的示例假定您的代码中包含以下导入语句:

import com.mongodb.client.model.Aggregates
import com.mongodb.client.model.Accumulators
import com.mongodb.client.model.Projections
import com.mongodb.client.model.Filters
import com.mongodb.client.model.mql.MqlValues

要访问表达式中的文档字段,您需要引用聚合管道正在处理的当前文档。使用 current() 方法引用本文档。要访问字段值,您必须使用适当类型的方法,如 getString()getDate()。为字段指定类型时,您要确保驱动程序只提供与该类型兼容的方法。以下代码显示了如何引用名为 name 的字符串字段:

current().getString("name")

要在某一操作中指定值,请将其传递给 of() 构造函数方法以将其转换为有效类型。以下代码展示了如何引用 1.0 值:

of(1.0)

要创建操作,请将方法链接到您的字段或值引用。您可通过链接其他方法来构建更复杂的操作。

以下示例创建一个操作,用于查找新墨西哥州至少看过一次医生的患者。该操作执行以下任务:

  • 使用 gt() 方法检查 visitDates 数组的大小是否大于 0

  • 使用 eq() 方法检查 state 字段值是否为“新墨西哥州”。

and() 方法将这些操作联系起来,因此管道阶段仅匹配满足这两个条件的文档。

current()
.getArray("visitDates")
.size()
.gt(of(0))
.and(current()
.getString("state")
.eq(of("New Mexico")))

group() 等聚合阶段直接接受操作,而其他阶段则希望您首先将操作包含在 computed()expr() 等方法中。这些方法采用 TExpression 类型的值,支持您在某些聚合中使用表达式。

要完成聚合管道阶段,请将表达式包含在聚合生成器方法中。以下列表举例说明了如何将表达式包含在常用聚合生成器方法中:

  • match(expr(<expression>))

  • project(fields(computed("<field name>", <expression>)))

  • group(<expression>)

要学习;了解有关这些方法的详情,请参阅聚合指南。

示例使用 listOf() 方法创建聚合阶段列表。该列表将传递给 MongoCollectionaggregate() 方法。

您可以使用这些构造方法来定义 Kotlin 聚合表达式所使用的值。

方法
说明
引用聚合管道正在处理的当前文档。
将聚合管道正在处理的当前文档引用为映射值。
返回与所提供基元相对应的 MqlValue 类型。
返回与所提供基元数组相对应的 MqlValue 类型数组。
返回条目值。
返回一个空的地图值。
返回 Query API 中存在的空值。

重要

当您向这些方法之一提供值时,驱动程序会按字面意思处理。例如,of("$x") 表示字符串值 "$x",而不是名为 x 的字段。

有关使用这些方法的示例,请参阅操作中的任何部分。

以下部分提供了驱动程序中可用聚合表达式操作的相关信息和示例。这些操作会按用途和功能进行分类。

每个部分均有一个表,其中描述了驱动程序中提供的聚合方法以及查询 API 中相应的表达式运算符。方法名称会链接到 API 文档,而聚合管道运算符名称则会链接到服务器手册文档中的描述和示例。虽然每个方法其实等效于相应的查询 API 表达式,但它们在预期参数与实现方面可能会有所不同。

注意

驱动程序生成的 Query API 表达式可能与每个示例提供的 Query API 表达式有所不同。不过,这两种表达式将产生相同的聚合结果。

重要

该驱动程序不为 Query API 中的所有聚合管道操作符提供方法。如果需要在聚合中使用不受支持的操作,则必须使用 BSON Document 类型定义整个表达式。要了解有关 Document 类型的详情,请参阅文档。

您可以使用本部分所述方法对类型为 MqlIntegerMqlNumber 的值执行算术操作。

假设您有特定年份的天气数据,其中包括每天的降水量测量值(以英寸为单位)。您想查找每个月的平均降水量(以毫米为单位)。

multiply() 操作符将 precipitation 字段乘以 25.4,将值转换为毫米。avg() 累加器方法将平均值作为 avgPrecipMM 字段返回。group() 方法按每个文档 date 字段所提供的月份对值进行分组。

以下代码展示此聚合的管道:

val month = current().getDate("date").month(of("UTC"))
val precip = current().getInteger("precipitation")
listOf(
Aggregates.group(
month,
Accumulators.avg("avgPrecipMM", precip.multiply(25.4))
))

以下代码在 Query API 中提供等效的聚合管道:

[ { $group: {
_id: { $month: "$date" },
avgPrecipMM: {
$avg: { $multiply: ["$precipitation", 25.4] } }
} } ]

您可以使用本节中描述的方法对类型为 MqlArray 的值执行数组操作。

假设您有一个电影集合,其中每个集合都包含近期放映时间的嵌套文档数组。每个嵌套文档都包含一个数组,表示电影院的座位总数,其中第一个数组条目是高级座位数,第二个条目是普通座位数。每个嵌套文档还包含已购电影票的数量。此集合中的文档可能如下所示:

{
"_id": ...,
"movie": "Hamlet",
"showtimes": [
{
"date": "May 14, 2023, 12:00 PM",
"seats": [ 20, 80 ],
"ticketsBought": 100
},
{
"date": "May 20, 2023, 08:00 PM",
"seats": [ 10, 40 ],
"ticketsBought": 34
}]
}

filter() 方法仅显示与提供的谓词匹配的结果。在这种情况下,谓词使用 sum() 计算座位总数,并将该值与 ticketsBoughtlt() 的数量进行比较。project() 方法会将这些筛选后的结果存储为一个新的 availableShowtimes 数组。

提示

如果需要将数组的值用作特定类型,则您必须指定以 getArray() 方法检索的数组的类型。

在本示例中,我们指定 seats 数组包含类型为 MqlDocument 的值,以便从每个数组条目提取嵌套字段。

以下代码展示此聚合的管道:

val showtimes = current().getArray<MqlDocument>("showtimes")
listOf(
Aggregates.project(
Projections.fields(
Projections.computed("availableShowtimes", showtimes
.filter { showtime ->
val seats = showtime.getArray<MqlInteger>("seats")
val totalSeats = seats.sum { n -> n }
val ticketsBought = showtime.getInteger("ticketsBought")
val isAvailable = ticketsBought.lt(totalSeats)
isAvailable
})
)))

注意

为了提高可读性,上例为 totalSeatsisAvailable 变量分配了中间值。即使这些变量没有获得中间值,代码仍然会产生相同的结果。

以下代码在 Query API 中提供等效的聚合管道:

[ { $project: {
availableShowtimes: {
$filter: {
input: "$showtimes",
as: "showtime",
cond: { $lt: [ "$$showtime.ticketsBought", { $sum: "$$showtime.seats" } ] }
} }
} } ]

您可以使用本节中描述的方法对类型为 MqlBoolean 的值执行布尔运算。

方法
聚合管道操作符

假设您想将极低或极高的天气温度读数(华氏度)归类为极端温度。

or() 操作符通过比较 temperature 字段与 lt()gt() 的预定义值,检查温度是否极端。project() 方法将此结果记录在 extremeTemp 字段中。

以下代码展示此聚合的管道:

val temperature = current().getInteger("temperature")
listOf(
Aggregates.project(
Projections.fields(
Projections.computed("extremeTemp", temperature
.lt(of(10))
.or(temperature.gt(of(95))))
)))

以下代码在 Query API 中提供等效的聚合管道:

[ { $project: {
extremeTemp: { $or: [ { $lt: ["$temperature", 10] },
{ $gt: ["$temperature", 95] } ] }
} } ]

您可以使用本节中描述的方法对类型为 MqlValue 的值执行比较操作。

提示

cond() 方法类似于 Java 中的三元运算符,而您应将其用于基于布尔值的简单分支。您应将 switchOn() 方法用于更复杂的比较操作,例如对值类型执行模式匹配或对该值执行其他任意检查。

以下示例展示与 location 字段的值为 "California" 的所有文档相匹配的管道:

val location = current().getString("location")
listOf(
Aggregates.match(
Filters.expr(location.eq(of("California")))
))

以下代码在 Query API 中提供等效的聚合管道:

[ { $match: { location: { $eq: "California" } } } ]

您可以使用本部分所述方法执行条件操作。

假设您有一个客户集合,其中包含客户的会员信息。最初,客户要么是会员,要么不是。随着时间的推移,会员级别被引入并使用相同的字段。该字段所存储的信息可以有不同的类型,而您希望通过标准化的值来表示客户的会员级别。

switchOn() 方法会按顺序检查每个子句。如果该值与子句所表示的类型匹配,该子句则会确定与该成员资格级别相对应的字符串值。如果原始值为字符串,则表示成员资格级别,并会使用该值。如果数据类型为布尔值,则会为成员资格级别返回 GoldGuest。如果数据类型为数组,则会返回数组中与最新成员资格级别相匹配的最新字符串。如果 member 字段的类型未知,switchOn() 方法则会提供默认值 Guest

以下代码展示此聚合的管道:

val member = current().getField("member")
listOf(
Aggregates.project(
Projections.fields(
Projections.computed("membershipLevel",
member.switchOn{field -> field
.isString{s-> s}
.isBoolean{b -> b.cond(of("Gold"), of("Guest"))}
.isArray { a -> a.last()}
.defaults{ d -> of("Guest")}})
)))

以下代码在 Query API 中提供等效的聚合管道:

[ { $project: {
membershipLevel: {
$switch: {
branches: [
{ case: { $eq: [ { $type: "$member" }, "string" ] }, then: "$member" },
{ case: { $eq: [ { $type: "$member" }, "bool" ] }, then: { $cond: {
if: "$member",
then: "Gold",
else: "Guest" } } },
{ case: { $eq: [ { $type: "$member" }, "array" ] }, then: { $last: "$member" } }
],
default: "Guest" } }
} } ]

您可以使用本节中描述的方法将自定义函数应用于类型为 MqlValue 的值。

为了提高可读性并允许重复使用代码,可以将冗余代码移至静态方法中。但是,在 Kotlin 中无法直接链接静态方法。passTo() 方法允许您将值链接到自定义静态方法中。

假设您需要根据一些基准,确定某个班级的表现。您想要知道每个班级的平均期末成绩,并与基准值进行比较。

以下自定义方法 gradeAverage() 接收文档数组以及这些文档之间共享的整数字段的名称。该方法计算所提供数组的所有文档中该字段的平均值,并确定所提供数组的所有元素中该字段的平均值。evaluate() 方法将提供的值与两个提供的范围限值进行比较,并根据比较结果生成响应字符串:

fun gradeAverage(students: MqlArray<MqlDocument>, fieldName: String): MqlNumber {
val sum = students.sum{ student -> student.getInteger(fieldName) }
val avg = sum.divide(students.size())
return avg
}
fun evaluate(grade: MqlNumber, cutoff1: MqlNumber, cutoff2: MqlNumber): MqlString {
val message = grade.switchOn{ on -> on
.lte(cutoff1) { g -> of("Needs improvement") }
.lte(cutoff2) { g -> of("Meets expectations") }
.defaults{g -> of("Exceeds expectations")}}
return message
}

提示

passTo() 方法的优点之一是,您可以在其他聚合中重复使用这一自定义方法。您可以使用 gradeAverage() 方法,查找按入学年份或学区(而不仅仅是班级)等条件筛选的学生组的平均成绩。例如,您可以使用 evaluate() 方法来评估学生的个人表现或整个学校或学区的表现。

passArrayTo() 方法会获取所有学生的分数,并使用 gradeAverage() 方法计算平均分数。然后,passNumberTo() 方法使用 evaluate() 方法来确定班级的表现。此示例使用 project() 方法将结果存储为 evaluation 字段。

以下代码展示此聚合的管道:

val students = current().getArray<MqlDocument>("students")
listOf(
Aggregates.project(
Projections.fields(
Projections.computed("evaluation", students
.passArrayTo { s -> gradeAverage(s, "finalGrade") }
.passNumberTo { grade -> evaluate(grade, of(70), of(85)) })
)))

以下代码在 Query API 中提供等效的聚合管道:

[ { $project: {
evaluation: { $switch: {
branches: [
{ case: { $lte: [ { $avg: "$students.finalGrade" }, 70 ] },
then: "Needs improvement"
},
{ case: { $lte: [ { $avg: "$students.finalGrade" }, 85 ] },
then: "Meets expectations"
}
],
default: "Exceeds expectations" } }
} } ]

您可以使用本部分所介绍的方法执行转换操作,在某些 MqlValue 类型之间进行转换。

假设您有一个包含毕业年份的学生数据集合,并且这些数据以字符串形式存储。您想要计算他们五年重聚的年份并将该值存储在新字段中。

parseInteger() 方法将 graduationYear 转换为整数,这样 add() 就可以计算出重聚年份。addFields() 方法将此结果存储为新的 reunionYear 字段。

以下代码展示此聚合的管道:

val graduationYear = current().getString("graduationYear")
listOf(
Aggregates.addFields(
Field("reunionYear",
graduationYear
.parseInteger()
.add(5))
))

以下代码在 Query API 中提供等效的聚合管道:

[ { $addFields: {
reunionYear: {
$add: [ { $toInt: "$graduationYear" }, 5 ] }
} } ]

您可以使用本部分介绍的方法对 MqlDate 类型的值执行日期操作。

假设您有关于包裹递送的数据,需要匹配在 "America/New_York" 时区任何星期一发生的递送。

如果 deliveryDate 字段包含任何代表有效日期的字符串值,如 "2018-01-15T16:00:00Z""Jan 15, 2018, 12:00 PM EST",则您可以使用 parseDate() 方法将这些字符串转换为日期类型。

dayOfWeek() 方法确定日期是星期几,并根据 "America/New_York" 参数,将其转换为哪天是星期一的数字。eq() 方法会将该值与 2 进行比较,后者根据所提供的时区参数对应于星期一。

以下代码展示此聚合的管道:

val deliveryDate = current().getString("deliveryDate")
listOf(
Aggregates.match(
Filters.expr(deliveryDate
.parseDate()
.dayOfWeek(of("America/New_York"))
.eq(of(2))
)))

以下代码在 Query API 中提供等效的聚合管道:

[ { $match: {
$expr: {
$eq: [ {
$dayOfWeek: {
date: { $dateFromString: { dateString: "$deliveryDate" } },
timezone: "America/New_York" }},
2
] }
} } ]

您可以使用本节中描述的方法对类型为 MqlDocument 的值执行文档操作。

假设您有一个旧客户数据集合,其中包括地址作为 mailing.address 字段下的子文档。您想查找目前居住在华盛顿州的所有客户。此集合中的文档可能如下所示:

{
"_id": ...,
"customer.name": "Mary Kenneth Keller",
"mailing.address":
{
"street": "601 Mongo Drive",
"city": "Vasqueztown",
"state": "CO",
"zip": 27017
}
}

getDocument() 方法会将 mailing.address 字段作为文档进行检索,因此可使用 getString() 方法来检索嵌套的 state 字段。eq() 方法会检查 state 字段的值是否为 "WA"

以下代码展示此聚合的管道:

val address = current().getDocument("mailing.address")
listOf(
Aggregates.match(
Filters.expr(address
.getString("state")
.eq(of("WA"))
)))

以下代码在 Query API 中提供等效的聚合管道:

[
{ $match: {
$expr: {
$eq: [{
$getField: {
input: { $getField: { input: "$$CURRENT", field: "mailing.address"}},
field: "state" }},
"WA" ]
}}}]

您可以使用本节中描述的方法对类型为 MqlMapMqlEntry 的值执行映射操作。

提示

如果数据将日期或列项 ID 等任意键映射到值,则您应将数据表示为映射。

方法
聚合管道操作符
没有相应的操作符
没有相应的操作符
没有相应的操作符
没有相应的操作符
没有相应的操作符
没有相应的操作符
没有相应的操作符
没有相应的操作符
没有相应的操作符

假设您有一个库存数据集合,其中每个文档都代表您负责供应的单个物品。每个文档都包含一个字段,该字段是所有仓库的映射,以及目前在该物品的库存方面有多少副本。您要确定所有仓库中物品的副本总数。此集合中的文档可能如下所示:

{
"_id": ...,
"item": "notebook"
"warehouses": [
{ "Atlanta", 50 },
{ "Chicago", 0 },
{ "Portland", 120 },
{ "Dallas", 6 }
]
}

entries() 方法会以数组的形式返回 warehouses 字段中的地图条目。sum() 方法会根据使用 getValue() 方法所检索到的数组中的值来计算各商品的总价值。此示例使用 project() 方法将结果存储为新的 totalInventory 字段。

以下代码展示此聚合的管道:

val warehouses = current().getMap<MqlNumber>("warehouses")
listOf(
Aggregates.project(
Projections.fields(
Projections.computed("totalInventory", warehouses
.entries()
.sum { v -> v.getValue() })
)))

以下代码在 Query API 中提供等效的聚合管道:

[ { $project: {
totalInventory: {
$sum: {
$getField: { $objectToArray: "$warehouses" },
} }
} } ]

您可以使用本节介绍的方法对 MqlString 类型的值执行字符串操作。

假设您需根据员工的姓氏和员工 ID 为公司员工生成小写用户名。

append() 方法将 lastNameemployeeID 字段合并为一个用户名,而 toLower() 方法可以使整个用户名小写。此示例使用 project() 方法将结果存储为新的 username 字段。

以下代码展示此聚合的管道:

val lastName = current().getString("lastName")
val employeeID = current().getString("employeeID")
listOf(
Aggregates.project(
Projections.fields(
Projections.computed("username", lastName
.append(employeeID)
.toLower())
)))

以下代码在 Query API 中提供等效的聚合管道:

[ { $project: {
username: {
$toLower: { $concat: ["$lastName", "$employeeID"] } }
} } ]

您可以使用本节中描述的方法对类型为 MqlValue 的值执行类型检查操作。

这些方法不返回布尔值。相反,您提供与该方法指定的类型匹配的默认值。如果校验值与方法类型匹配,则返回校验值。否则,将返回所提供的默认值。如果您要根据数据类型对分支逻辑进行编程,请参阅 switchOn()

方法
聚合管道操作符
没有相应的操作符
没有相应的操作符
没有相应的操作符
没有相应的操作符
没有相应的操作符
没有相应的操作符
没有相应的操作符
没有相应的操作符

假设您有评级数据的集合。早期版本的评论模式允许用户提交没有星级的负面评论。您希望将这些没有星级的负面评论转换为最低值 1 星。

isNumberOr() 方法返回 rating 的值,如果 rating 不是数字或为空,则返回 1 的值。project() 方法将此值作为新的 numericalRating 字段返回。

以下代码展示此聚合的管道:

val rating = current().getField("rating")
listOf(
Aggregates.project(
Projections.fields(
Projections.computed("numericalRating", rating
.isNumberOr(of(1)))
)))

以下代码在 Query API 中提供等效的聚合管道:

[ { $project: {
numericalRating: {
$cond: { if: { $isNumber: "$rating" },
then: "$rating",
else: 1
} }
} } ]

后退

聚合(Aggregation)