聚合表达式操作
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()
方法创建聚合阶段列表。该列表将传递给 MongoCollection
的 aggregate()
方法。
构造器方法
您可以使用这些构造方法来定义 Kotlin 聚合表达式所使用的值。
方法 | 说明 |
---|---|
引用聚合管道正在处理的当前文档。 | |
将聚合管道正在处理的当前文档引用为映射值。 | |
返回与所提供基元相对应的 MqlValue 类型。 | |
返回与所提供基元数组相对应的 MqlValue 类型数组。 | |
返回条目值。 | |
返回一个空的地图值。 | |
返回 Query API 中存在的空值。 |
重要
当您向这些方法之一提供值时,驱动程序会按字面意思处理。例如,of("$x")
表示字符串值 "$x"
,而不是名为 x
的字段。
有关使用这些方法的示例,请参阅操作中的任何部分。
操作
以下部分提供了驱动程序中可用聚合表达式操作的相关信息和示例。这些操作会按用途和功能进行分类。
每个部分均有一个表,其中描述了驱动程序中提供的聚合方法以及查询 API 中相应的表达式运算符。方法名称会链接到 API 文档,而聚合管道运算符名称则会链接到服务器手册文档中的描述和示例。虽然每个方法其实等效于相应的查询 API 表达式,但它们在预期参数与实现方面可能会有所不同。
注意
驱动程序生成的 Query API 表达式可能与每个示例提供的 Query API 表达式有所不同。不过,这两种表达式将产生相同的聚合结果。
重要
该驱动程序不为 Query API 中的所有聚合管道操作符提供方法。如果需要在聚合中使用不受支持的操作,则必须使用 BSON Document
类型定义整个表达式。要了解有关 Document
类型的详情,请参阅文档。
算术运算
您可以使用本部分所述方法对类型为 MqlInteger
或 MqlNumber
的值执行算术操作。
方法 | 聚合管道操作符 |
---|---|
假设您有特定年份的天气数据,其中包括每天的降水量测量值(以英寸为单位)。您想查找每个月的平均降水量(以毫米为单位)。
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()
计算座位总数,并将该值与 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
变量分配了中间值。即使这些变量没有获得中间值,代码仍然会产生相同的结果。
以下代码在 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()
方法会按顺序检查每个子句。如果该值与子句所表示的类型匹配,该子句则会确定与该成员资格级别相对应的字符串值。如果原始值为字符串,则表示成员资格级别,并会使用该值。如果数据类型为布尔值,则会为成员资格级别返回 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")}}) )))
以下代码在 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" ] }}}]
映射操作
您可以使用本节中描述的方法对类型为 MqlMap
或 MqlEntry
的值执行映射操作。
提示
如果数据将日期或列项 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()
方法将 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()) )))
以下代码在 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 } } } } ]