聚合表达式操作
概述
在本指南中,您可以学习如何使用MongoDB Java驱动程序来构造用于聚合管道的表达式。您可以使用可发现、类型安全的Java方法来执行表达式操作,而不是使用BSON文档。因为这些方法遵循流畅接口模式,所以您可以链接聚合操作,创建更紧凑且更易于阅读的代码。
本指南中的操作使用来自com.mongodb.client.model.mql 包的方法。这些方法提供了一种使用查询API的惯用方式,这是驱动程序与MongoDB部署交互的机制。要了解有关查询API的更多信息,请参阅服务器手册文档。
如何使用操作
本指南中的示例假设您在代码中包含以下静态导入:
import static com.mongodb.client.model.Aggregates.*; import static com.mongodb.client.model.Accumulators.* import static com.mongodb.client.model.Projections.*; import static com.mongodb.client.model.Filters.*; import static com.mongodb.client.model.mql.MqlValues.*; import static java.util.Arrays.asList;
要在表达式中访问文档字段,您必须引用由聚合管道正在处理的当前文档。使用以下方法: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>)
有关这些方法的更多信息,请参阅聚合构建器.
示例使用 asList()
方法创建聚合阶段的列表。该列表传递给 MongoCollection
的 aggregate()
方法。
构造函数方法
您可以使用这些构造函数方法来定义用于 Java 聚合表达式中使用的值。
方法 | 描述 |
---|---|
引用当前正在由聚合管道处理的文档。 | |
将当前正在由聚合管道处理的文档作为映射值引用。 | |
返回一个与提供的原始数据类型相对应的 MqlValue 类型。 | |
返回一个与提供的原始数据数组相对应的 MqlValue 类型数组。 | |
返回一个条目值。 | |
返回一个空映射值。 | |
返回查询 API 中存在的空值。 |
重要
当你向这些方法之一提供值时,驱动器将按字面意思处理它。例如,of("$x")
表示字符串值 "$x"
,而不是名为 x
的字段。
请参阅 操作 中的任何部分,了解这些方法的示例。
操作
以下部分提供有关驱动程序中可用的聚合表达式操作的信息和示例。操作根据目的和功能进行分类。
每个部分都有一个表格,描述了驱动程序中可用的聚合方法以及查询 API 中的相应表达式运算符。方法名称链接到 API 文档,聚合管道运算符名称链接到服务器手册文档中的描述和示例。虽然每个 Java 方法在效果上相当于相应的查询 API 表达式,但它们可能在预期参数和实现上有所不同。
注意
驱动程序生成一个可能与每个示例中提供的查询 API 表达式不同的查询 API 表达式。但是,这两个表达式将产生相同的聚合结果。
重要
驱动程序不提供查询 API 中所有聚合管道运算符的方法。如果你必须在聚合中使用不受支持的运算,你必须使用 BSON Document
类型定义整个表达式。有关 Document
类型的更多信息,请参阅 文档。
算术运算
您可以使用本节中描述的方法在类型为 MqlInteger
或 MqlNumber
的值上执行算术运算。
Java 方法 | 聚合管道运算符 |
---|---|
假设您有一年特定日期的天气数据,包括每天的降水量(以英寸为单位)。您想找到每个月的平均降水量(以毫米为单位)。
multiply() 运算符将 precipitation 字段乘以 25.4,以将值转换为毫米。avg() 累加器方法返回平均值为 avgPrecipMM 字段。group() 方法根据每个文档中的 date 字段中的月份对值进行分组。
以下代码显示了此聚合的管道
var month = current().getDate("date").month(of("UTC")); var precip = current().getInteger("precipitation"); asList(group( month, avg("avgPrecipMM", precip.multiply(25.4)) ));
以下代码提供了在查询 API 中的等效聚合管道
[ { $group: { _id: { $month: "$date" }, avgPrecipMM: { $avg: { $multiply: ["$precipitation", 25.4] } } } } ]
数组运算
您可以使用本节中描述的方法在类型为 MqlArray
的值上执行数组运算。
Java 方法 | 聚合管道运算符 |
---|---|
假设您有一个电影集合,其中每个电影都包含一个表示即将上映时间的嵌套文档数组。每个嵌套文档包含一个数组,表示剧院中的总座位数,其中第一个数组元素是高级座位的数量,第二个元素是普通座位的数量。每个嵌套文档还包含该放映时间的已售票数。该集合中的文档可能如下所示
{ "_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
的值,以便我们可以从每个数组条目中提取嵌套字段。
以下代码显示了此聚合的管道
var showtimes = current().<MqlDocument>getArray("showtimes"); asList(project(fields( computed("availableShowtimes", showtimes .filter(showtime -> { var seats = showtime.<MqlInteger>getArray("seats"); var totalSeats = seats.sum(n -> n); var ticketsBought = showtime.getInteger("ticketsBought"); var isAvailable = ticketsBought.lt(totalSeats); return isAvailable; })) )));
注意
为了提高可读性,前面的示例将中间值分配给 totalSeats
和 isAvailable
变量。如果您不将这些中间值拉入变量,代码仍然会产生等效的结果。
以下代码提供了在查询 API 中的等效聚合管道
[ { $project: { availableShowtimes: { $filter: { input: "$showtimes", as: "showtime", cond: { $lt: [ "$$showtime.ticketsBought", { $sum: "$$showtime.seats" } ] } } } } } ]
布尔运算
您可以使用本节中描述的方法在 MqlBoolean
类型的值上执行布尔运算。
假设您想将非常低或非常高的气温(华氏度)读数归类为极端。
or()
操作符通过使用 lt()
和 gt()
将温度字段与预定义的值进行比较,来检查温度是否极端。 project()
方法将此结果记录在 extremeTemp
字段中。
以下代码显示了此聚合的管道
var temperature = current().getInteger("temperature"); asList(project(fields( 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()
方法。
Java 方法 | 聚合管道运算符 |
---|---|
以下示例显示了一个匹配所有 location
字段值为 "California"
的文档的管道。
var location = current().getString("location"); asList(match(expr(location.eq(of("California")))));
以下代码提供了在查询 API 中的等效聚合管道
[ { $match: { location: { $eq: "California" } } } ]
条件操作
您可以使用本节中描述的方法执行条件操作。
Java 方法 | 聚合管道运算符 |
---|---|
假设您有一个包含客户及其会员信息的集合。最初,客户要么是会员,要么不是。随着时间的推移,引入了会员等级,并使用了相同的字段。在此字段中存储的信息可以是几种不同类型之一,您想创建一个标准化的值来表示他们的会员等级。
switchOn()
方法按顺序检查每个条款。如果值与条款指示的类型匹配,则该条款确定与会员级别对应的字符串值。如果原始值是一个字符串,它表示会员级别并使用该值。如果数据类型是布尔值,它返回 Gold
或 Guest
作为会员级别。如果数据类型是数组,它返回数组中与最新会员级别匹配的最新的字符串。如果 member
字段是未知类型,则 switchOn()
方法提供一个默认值 Guest
。
以下代码显示了此聚合的管道
var member = current().getField("member"); asList(project(fields( computed("membershipLevel", member.switchOn(field -> field .isString(s -> s) .isBoolean(b -> b.cond(of("Gold"), of("Guest"))) .<MqlString>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
类型的值。
为了提高可读性和代码重用,您可以将冗余代码移动到静态方法中。但是,在 Java 中无法直接链式调用静态方法。passTo()
方法允许您将值链式连接到自定义静态方法。
Java 方法 | 聚合管道运算符 |
---|---|
无对应操作符 |
假设您想确定一个班级相对于某些基准的表现。您想找到每个班级的平均最终成绩,并将其与基准值进行比较。
以下自定义方法 gradeAverage()
接收一个文档数组和这些文档中共享的整数字段名称。它计算该字段在提供数组中的所有文档中的平均值,并确定该字段在提供数组中的所有元素中的平均值。evaluate()
方法比较提供的值与两个提供的范围限制,并根据值的比较生成响应字符串
public static MqlNumber gradeAverage(MqlArray<MqlDocument> students, String fieldName) { var sum = students.sum(student -> student.getInteger(fieldName)); var avg = sum.divide(students.size()); return avg; } public static MqlString evaluate(MqlNumber grade, MqlNumber cutoff1, MqlNumber cutoff2) { var 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
字段。
以下代码显示了此聚合的管道
var students = current().<MqlDocument>getArray("students"); asList(project(fields( computed("evaluation", students .passArrayTo(students -> gradeAverage(students, "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
类型之间进行转换。
Java 方法 | 聚合管道运算符 |
---|---|
无对应操作符 | |
无对应操作符 | |
假设您有一个包含学生毕业年份的数据集,这些年份存储为字符串。您想计算他们的五周年纪念年份,并将此值存储在新字段中。
parseInteger()
方法将 graduationYear
转换为整数,以便 add()
可以计算纪念年份。使用 addFields()
方法将此结果存储为新的 reunionYear
字段。
以下代码显示了此聚合的管道
var graduationYear = current().getString("graduationYear"); asList(addFields( new Field("reunionYear", graduationYear .parseInteger() .add(5)) ));
以下代码提供了在查询 API 中的等效聚合管道
[ { $addFields: { reunionYear: { $add: [ { $toInt: "$graduationYear" }, 5 ] } } } ]
日期操作
您可以使用本节中描述的方法在 MqlDate
类型的值上执行日期操作。
Java 方法 | 聚合管道运算符 |
---|---|
假设您有关于包裹投递的数据,并且想要匹配在 "America/New_York"
时区中的任何星期一发生的投递。
如果 deliveryDate
字段包含任何表示有效日期的字符串值,例如 "2018-01-15T16:00:00Z"
或 Jan 15, 2018, 12:00 PM EST
,您可以使用 parseDate()
方法将这些字符串转换为日期类型。
《dayOfWeek()` 方法用于确定当前是周几,并根据 "America/New_York"
参数将结果转换为以星期一为基准的数字。 eq()
方法将此值与 2
进行比较,根据提供的时间区域参数,2 对应于星期一。
以下代码显示了此聚合的管道
var deliveryDate = current().getString("deliveryDate"); asList(match(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
类型的值上执行文档操作。
Java 方法 | 聚合管道运算符 |
---|---|
无对应操作符 | |
假设您有一个包含地址作为 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"
。
以下代码显示了此聚合的管道
var address = current().getDocument("mailing.address"); asList(match(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)映射到值,则应将数据表示为映射。
Java 方法 | 聚合管道运算符 |
---|---|
无对应操作符 | |
无对应操作符 | |
无对应操作符 | |
无对应操作符 | |
无对应操作符 | |
无对应操作符 | |
无对应操作符 | |
无对应操作符 | |
无对应操作符 |
假设您有一组库存数据,其中每个文档代表您负责供应的单独项目。每个文档包含一个字段,该字段是映射,其中包含您的所有仓库及其当前库存中的项目副本数量。您想要确定所有仓库中物品的总副本数。该集合中的文档可能如下所示
{ "_id": ..., "item": "notebook" "warehouses": [ { "Atlanta", 50 }, { "Chicago", 0 }, { "Portland", 120 }, { "Dallas", 6 } ] }
entries()
方法返回 warehouses
字段中的映射条目作为数组。根据使用 getValue()
方法检索到的数组中的值,sum()
方法计算物品的总值。此示例使用 project()
方法将结果存储为新的 totalInventory
字段。
以下代码显示了此聚合的管道
var warehouses = current().getMap("warehouses"); asList(project(fields( computed("totalInventory", warehouses .entries() .sum(v -> v.getValue())) )));
以下代码提供了在查询 API 中的等效聚合管道
[ { $project: { totalInventory: { $sum: { $getField: { $objectToArray: "$warehouses" }, } } } } ]
字符串操作
您可以使用本节中描述的方法在 MqlString
类型的值上执行字符串操作。
Java 方法 | 聚合管道运算符 |
---|---|
假设您想要从员工的姓氏和员工ID生成员工的用户名。
append()
方法将 firstName
和 lastName
字段组合成一个单个用户名,而 toLower()
方法将整个用户名转换为小写。此示例使用 project()
方法将结果存储为新的 username
字段。
以下代码显示了此聚合的管道
var lastName = current().getString("lastName"); var employeeID = current().getString("employeeID"); asList(project(fields( computed("username", lastName .append(employeeID) .toLower()) )));
以下代码提供了在查询 API 中的等效聚合管道
[ { $project: { username: { $toLower: { $concat: ["$lastName", "$employeeID"] } } } } ]
类型检查操作
您可以使用本节中描述的方法在类型为 MqlValue
的值上执行类型检查操作。
这些方法不返回布尔值。相反,您提供一个与该方法指定的类型匹配的默认值。如果检查的值与该方法类型匹配,则返回检查的值。否则,返回提供的默认值。如果您想根据数据类型编程分支逻辑,请参阅 switchOn()
。
Java 方法 | 聚合管道运算符 |
---|---|
无对应操作符 | |
无对应操作符 | |
无对应操作符 | |
无对应操作符 | |
无对应操作符 | |
无对应操作符 | |
无对应操作符 | |
无对应操作符 |
假设您有一个评分数据的集合。早期版本的评论架构允许用户提交没有星级评分的负面评论。您希望将这些没有星级评分的负面评论转换为至少1星。
isNumberOr()
方法返回 rating
的值,或者如果 rating
不是一个数字或为 null,则返回 1
的值。project()
方法将此值作为新的 numericalRating
字段返回。
以下代码显示了此聚合的管道
var rating = current().getField("rating"); asList(project(fields( computed("numericalRating", rating .isNumberOr(of(1))) )));
以下代码提供了在查询 API 中的等效聚合管道
[ { $project: { numericalRating: { $cond: { if: { $isNumber: "$rating" }, then: "$rating", else: 1 } } } } ]