聚合表达式操作
概述
在本指南中,您可以了解如何使用MongoDB Kotlin驱动程序构建聚合管道中使用的表达式。您可以使用可发现、类型安全的Java方法来执行表达式操作,而不是使用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()
方法来引用此文档。要访问字段的值,必须使用适当类型的方
current().getString("name")
要在操作中指定值,将其传递给 of()
构造方法以将其转换为有效类型。
of(1.0)
要创建一个操作,将方法链接到您的字段或值引用。您可以通过链接更多方法来构建更复杂的操作。
以下示例创建了一个操作,用于查找至少访问过医生办公室一次的新墨西哥州的病人。该操作执行以下操作
使用
gt()
方法检查visitDates
数组的长度是否大于使用
eq()
方法检查state
字段的值是否为“新墨西哥州”。
and()
方法将这些操作链接起来,以便管道阶段仅匹配满足两个条件的文档。
current() .getArray("visitDates") .size() .gt(of(0)) .and(current() .getString("state") .eq(of("New Mexico")))
虽然某些聚合阶段(如 group()
)可以直接接受操作,但其他阶段要求您首先在
要完成您的聚合管道阶段,请将表达式包含在聚合构建器方法中。以下列表提供了如何将表达式包含在常见的聚合构建器方法中的示例
match(expr(<expression>))
project(fields(computed("<field name>", <expression>)))
group(<expression>)
有关这些方法的更多信息,请参阅聚合指南.
这些示例使用 listOf()
方法创建一个聚合阶段的列表。该列表传递给
构造方法
您可以使用这些构造方法在 Kotlin 聚合表达式中定义值。
方法 | 描述 |
---|---|
引用当前由聚合管道处理的文档。 | |
将正在处理的文档引用为聚合管道中的映射值。 | |
返回与提供的原语相对应的 MqlValue 类型。 | |
返回与提供的原语数组相对应的 MqlValue 类型数组。 | |
返回条目值。 | |
返回空映射值。 | |
返回查询API中存在的null值。 |
重要
当你向这些方法之一提供值时,驱动器将其视为字面意义。例如,of("$x")
表示字符串值"$x"
,而不是名为x
的字段。
有关使用这些方法的示例,请参阅操作中的任何部分。
操作
以下各节提供有关驱动器中可用的聚合表达式操作的信息和示例。操作按用途和功能分类。
每个部分都包含一个表格,描述驱动器中可用的聚合方法以及查询API中的对应表达式运算符。方法名称链接到API文档,聚合管道运算符名称链接到服务器手册文档中的描述和示例。虽然每个方法在效果上等同于相应的查询API表达式,但它们可能在预期参数和实现上有所不同。
注意
驱动器生成一个可能与每个示例中提供的查询API表达式不同的Query API表达式。但是,这两个表达式将产生相同的聚合结果。
重要
驱动器不提供查询API中所有聚合管道运算符的方法。如果您需要在聚合中使用不受支持的运算,您必须使用BSON Document
类型定义整个表达式。有关Document
类型的更多信息,请参阅文档。
算术运算
您可以使用本节中描述的方法在类型为 MqlInteger
或 MqlNumber
的值上执行算术运算。
方法 | 聚合管道操作符 |
---|---|
假设您有一年的天气数据,其中包括每天的平均降水量(以英寸为单位)。您想找出每个月的平均降水量(以毫米为单位)。
multiply()
操作符将 precipitation
字段乘以 25.4 以将其值转换为毫米。累积方法 avg()
返回平均值,作为 avgPrecipMM
字段。方法 group()
将值按每个月的给定日期字段的月份分组。
以下代码显示了此聚合的管道
val month = current().getDate("date").month(of("UTC")) val precip = current().getInteger("precipitation") listOf( Aggregates.group( month, Accumulators.avg("avgPrecipMM", precip.multiply(25.4)) ))
以下代码提供了在查询 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()
来计算总座位数,并将该值与 ticketsBought
的 lt()
进行比较。使用 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 }) )))
注意
为了提高可读性,前面的示例将中间值分配给 totalSeats
和 isAvailable
变量。如果你不将这些中间值拉入变量中,代码仍然会产生等效的结果。
以下代码提供了在查询 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") listOf( Aggregates.project( Projections.fields( Projections.computed("extremeTemp", temperature .lt(of(10)) .or(temperature.gt(of(95)))) )))
以下代码提供了在查询 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"))) ))
以下代码提供了在查询 API 中等效的聚合管道
[ { $match: { location: { $eq: "California" } } } ]
条件操作
您可以使用本节中描述的方法执行条件操作。
方法 | 聚合管道操作符 |
---|---|
假设您有一个包含客户及其会员信息的集合。最初,客户要么是会员,要么不是。随着时间的推移,引入了会员等级,并使用了相同的字段。该字段存储的信息可以是几种不同类型之一,您想要创建一个标准化的值来指示他们的会员等级。
switchOn()
方法按顺序检查每个子句。如果值与子句中指示的类型匹配,则该子句确定与会员等级对应的字符串值。如果原始值是字符串,则它表示会员等级,并使用该值。如果数据类型是布尔型,则返回 Gold
或 Guest
以表示会员等级。如果数据类型是数组,则返回数组中最新的字符串,该字符串与最新的会员等级匹配。如果 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")}}) )))
以下代码提供了在查询 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)) }) )))
以下代码提供了在查询 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)) ))
以下代码提供了在查询 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)) )))
以下代码提供了在查询 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")) )))
以下代码提供了在查询 API 中等效的聚合管道
[ { $match: { $expr: { $eq: [{ $getField: { input: { $getField: { input: "$$CURRENT", field: "mailing.address"}}, field: "state" }}, "WA" ] }}}]
映射操作
您可以使用本节中描述的方法在类型为 MqlMap
或 MqlEntry
的值上执行映射操作。
提示
如果数据将任意键(如日期或项目ID)映射到值,则应将数据表示为映射。
方法 | 聚合管道操作符 |
---|---|
没有对应的操作符 | |
没有对应的操作符 | |
没有对应的操作符 | |
没有对应的操作符 | |
没有对应的操作符 | |
没有对应的操作符 | |
没有对应的操作符 | |
没有对应的操作符 | |
没有对应的操作符 |
假设您有一个库存数据集合,其中每个文档代表您负责供应的单独项目。每个文档包含一个字段,该字段是您所有仓库及其当前库存中该物品副本数量的映射。您想要确定您所有仓库中物品的副本总数。这个集合中的一个文档可能如下所示
{ "_id": ..., "item": "notebook" "warehouses": [ { "Atlanta", 50 }, { "Chicago", 0 }, { "Portland", 120 }, { "Dallas", 6 } ] }
entries()
方法返回 warehouses
字段中的映射条目作为数组。根据使用 getValue()
方法检索到的数组中的值,sum()
方法计算物品的总价值。此示例使用 project()
方法将结果存储为新的 totalInventory
字段。
以下代码显示了此聚合的管道
val warehouses = current().getMap<MqlNumber>("warehouses") listOf( Aggregates.project( Projections.fields( Projections.computed("totalInventory", warehouses .entries() .sum { v -> v.getValue() }) )))
以下代码提供了在查询 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") listOf( Aggregates.project( Projections.fields( Projections.computed("username", lastName .append(employeeID) .toLower()) )))
以下代码提供了在查询 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))) )))
以下代码提供了在查询 API 中等效的聚合管道
[ { $project: { numericalRating: { $cond: { if: { $isNumber: "$rating" }, then: "$rating", else: 1 } } } } ]