聚合表达式操作
概述
在本指南中,您可以学习如何使用 Kotlin Sync 驱动来构建用于聚合管道的表达式。您可以使用可发现、类型安全的 Kotlin 方法执行表达式操作,而不是使用 BSON 文档。因为这些方法遵循流畅接口模式,您可以链接聚合操作以创建紧凑且易于阅读的代码。
本指南中的操作使用来自com.mongodb.client.model.mql 包的方法。这些方法提供了一种使用查询 API 的直观方式,这是驱动程序与 MongoDB 部署交互的机制。要了解更多关于查询 API 的信息,请参阅服务器手册文档。
如何使用操作
本指南中的示例假设您在代码中包含了以下导入
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
字段值是否为“New Mexico”
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>)
有关这些方法的更多信息,请参阅使用聚合转换您的数据 指南。
构造函数方法
您可以使用这些构造函数方法来定义用于 Kotlin 聚合表达式的值。
方法 | 描述 |
---|---|
引用正在由聚合管道处理的当前文档。 | |
引用正在由聚合管道处理的当前文档作为地图值。 | |
返回与提供的原始数据类型对应的 MqlValue 类型。 | |
返回与提供的原始数组类型对应的 MqlValue 类型的数组。 | |
返回条目值。 | |
返回空地图值。 | |
返回查询 API 中的 null 值。 |
重要
当您向这些方法之一提供值时,驱动程序将其视为字面值。例如,of("$x")
表示字符串值 "$x"
,而不是名为 x
的字段。
请参阅 操作 下的任何部分,以获取使用这些方法的示例。
操作
以下部分提供了有关驱动程序中可用的聚合表达式操作的信息和示例。操作根据目的和功能进行分类。
每个部分都有一个表格,描述了驱动程序中可用的聚合方法以及查询 API 中的相应表达式运算符。方法名称链接到 API 文档,聚合管道运算符名称链接到服务器手册文档中的描述和示例。虽然每个方法在效果上等效于相应的聚合运算符,但它们可能在预期参数和实现上有所不同。
每个部分的示例使用 listOf()
方法从聚合阶段创建管道。然后,每个示例将管道传递给 MongoCollection
的 aggregate()
方法。
注意
驱动程序生成的 Query API 表达式可能与每个示例中提供的 Query API 表达式不同。但是,这两个表达式将产生相同的聚合结果。
重要
驱动程序不提供 Query API 中所有聚合管道运算符的方法。要在聚合中使用不受支持的运算符,必须使用 BSON Document
类型定义整个表达式。
算术运算
您可以使用本节中描述的方法在 MqlInteger
或 MqlNumber
类型的值上执行算术运算。
方法 | 聚合管道运算符 |
---|---|
假设您有一年特定年份的天气数据,其中包括每天降雨量(以英寸为单位)。您想找到每个月的平均降雨量(以毫米为单位)。
multiply()
运算符将 precipitation
字段乘以 25.4
以将字段值转换为毫米。累加器方法 avg()
返回平均值为 avgPrecipMM
字段。方法 group()
根据每个文档的 date
字段中给出的月份对值进行分组。
以下代码显示了此聚合的管道
val month = current().getDate("date").month(of("UTC")) val precip = current().getInteger("precipitation") val results = collection.aggregate<Document>( 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()
来计算总座位数,并使用 lt()
方法将该值与 ticketsBought
相比较。使用 project()
方法将这些筛选结果存储为新字段 availableShowtimes
的数组。
提示
在您使用 getArray()
方法以任何特定类型处理值时,必须指定数组包含的值的类型。例如,您必须指定数组包含整数,以便在应用程序的其他部分对这些整数进行计算。
本节的示例指定了 seats
数组包含类型为 MqlDocument
的值,以便可以从每个数组条目中提取嵌套字段。
以下代码显示了此聚合的管道
val showtimes = current().getArray<MqlDocument>("showtimes") val results = collection.aggregate<Document>( 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 }) ) ) ) )
注意
为了提高可读性,前面的示例将中间值分配给变量 totalSeats
和 isAvailable
。如果您不将这些中间值分配给变量,代码仍会产生等效的结果。
以下代码提供了在 Query API 中等效的聚合管道
[ { $project: { availableShowtimes: { $filter: { input: "$showtimes", as: "showtime", cond: { $lt: [ "$$showtime.ticketsBought", { $sum: "$$showtime.seats" } ] } } } } } ]
布尔运算
您可以使用本节中描述的方法在类型为 MqlBoolean
的值上执行布尔运算。
假设您想将非常低或非常高的天气温度读数(华氏度)分类为极端。
or()
操作符通过使用 lt()
和 gt()
方法将 temperature
字段与预定义值进行比较来检查温度是否为极端。使用 project()
方法将此结果记录在 extremeTemp
字段中。
以下代码显示了此聚合的管道
val temperature = current().getInteger("temperature") val results = collection.aggregate<Document>( 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()
方法类似于 Kotlin 中的三元运算符,您可以使用它进行简单的基于布尔值的分支。对于更复杂的比较,例如在值类型上执行模式匹配或其他任意检查,请使用 switchOn()
方法。
方法 | 聚合管道运算符 |
---|---|
以下示例显示了一个管道,该管道匹配所有 location
字段值为 "California"
的文档。
val location = current().getString("location") val results = collection.aggregate<Document>( listOf( Aggregates.match( Filters.expr(location.eq(of("California"))) ) ) )
以下代码提供了在 Query API 中等效的聚合管道
[ { $match: { location: { $eq: "California" } } } ]
条件操作
您可以使用本节中描述的方法执行条件操作。
方法 | 聚合管道运算符 |
---|---|
假设您有一个包含客户及其会员信息的集合。最初,客户要么是会员,要么不是。随着时间的推移,引入了会员级别并使用相同的字段。存储在此字段中的信息可以是几种不同类型之一,并且您希望创建一个标准化的值来指示他们的会员级别。
switchOn()
方法按顺序检查每个子句。如果值与子句指示的类型匹配,则该子句确定与会员级别对应的字符串值。如果原始值是字符串,则它表示会员级别并使用该值。如果数据类型是布尔值,则返回 Gold
或 Guest
以表示会员级别。如果数据类型是数组,则返回与最新会员级别匹配的数组中最新的字符串。如果 member
字段是未知类型,则 switchOn()
方法提供一个默认值 Guest
。
以下代码显示了此聚合的管道
val member = current().getField("member") val results = collection.aggregate<Document>( 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") val results = collection.aggregate<Document>( 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 students = current().getArray<MqlDocument>("students") val results = collection.aggregate<Document>( listOf( Aggregates.project( Projections.fields( Projections.computed("evaluation", students .passArrayTo { s -> gradeAverage(s, "finalGrade") } .passNumberTo { grade -> evaluate(grade, of(70), of(85)) }) ) ) ) )
以下代码提供了在 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"
时区时,数字赋值使用 0
表示星期天。使用 eq()
方法将此值与 2
或星期一进行比较。
以下代码显示了此聚合的管道
val deliveryDate = current().getString("deliveryDate") val results = collection.aggregate<Document>( 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") val results = collection.aggregate<Document>( 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" ] }}}]
映射操作
您可以使用本节中描述的方法在类型为 MqlMap
或 MqlEntry
的值上执行映射操作。
方法 | 聚合管道运算符 |
---|---|
没有相应的运算符 | |
没有相应的运算符 | |
没有相应的运算符 | |
没有相应的运算符 | |
没有相应的运算符 | |
没有相应的运算符 | |
没有相应的运算符 | |
没有相应的运算符 | |
没有相应的运算符 |
假设您有一个库存数据集合,其中每个文档代表您负责供应的个别项目。每个文档都包含一个字段,该字段是您所有仓库及其库存中该项目的副本数量的映射。您想确定所有仓库中项目的总副本数。此集合中的文档可能如下所示
{ "_id": ..., "item": "notebook" "warehouses": [ { "Atlanta", 50 }, { "Chicago", 0 }, { "Portland", 120 }, { "Dallas", 6 } ] }
entries()
方法返回 warehouses
字段中的映射条目作为数组。根据使用 getValue()
方法检索到的数组中的值,sum()
方法计算项目的总价值。此示例使用 project()
方法将结果存储为新的 totalInventory
字段。
以下代码显示了此聚合的管道
val warehouses = current().getMap<MqlNumber>("warehouses") val results = collection.aggregate<Document>( 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()
方法将 lastName
和 employeeID
字段合并成一个单个用户名,而 toLower()
方法将整个用户名转换为小写。此示例使用 project()
方法将结果存储为新的 username
字段。
以下代码显示了此聚合的管道
val lastName = current().getString("lastName") val employeeID = current().getString("employeeID") val results = collection.aggregate<Document>( 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
不是数字或为 null,则返回 1
的值。然后 project()
方法将此值作为一个新的 numericalRating
字段返回。
以下代码显示了此聚合的管道
val rating = current().getField("rating") val results = collection.aggregate<Document>( listOf( Aggregates.project( Projections.fields( Projections.computed( "numericalRating", rating .isNumberOr(of(1)) ) ) ) ) )
以下代码提供了在 Query API 中等效的聚合管道
[ { $project: { numericalRating: { $cond: { if: { $isNumber: "$rating" }, then: "$rating", else: 1 } } } } ]