聚合表达式操作
Overview
在本指南中,您可了解如何使用 MongoDB Java 驱动程序来构建可在聚合管道中使用的表达式。您可以使用可发现且类型安全的 Java 方法而不是 BSON 文档来执行表达式操作。由于这些方法遵循连贯接口模式,因此可将聚合操作链接在一起来创建更紧凑且可更自然阅读的代码。
本指南中的操作使用 com.mongodb.client.model.mql 包。这些方法提供了使用 Query API 的惯用方式,而 Query API 正是驱动程序与 MongoDB 部署交互的机制。 要了解有关 Query API的更多信息,请参阅MongoDB Server手册文档。
如何使用运算
本指南中的示例假定您在代码中包含以下静态导入:
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 类型数组。 | |
返回条目值。 | |
返回一个空的地图值。 | |
返回 Query API 中存在的空值。 |
重要
当您向这些方法之一提供值时,驱动程序会按字面意思处理。例如,of("$x")
表示字符串值 "$x"
,而不是名为 x
的字段。
有关使用这些方法的示例,请参阅操作中的任何部分。
操作
以下部分提供了驱动程序中可用聚合表达式操作的相关信息和示例。这些操作会按用途和功能进行分类。
每个部分都有一个表,其中描述了驱动程序中可用的聚合方法以及查询 API 中相应的表达式操作符。 方法名称链接到 API 文档,聚合管道操作符名称链接到服务器手册文档中的描述和示例。 虽然每个 Java 方法实际上等效于相应的查询 API 表达式,但它们在预期参数和实现方面可能有所不同。
注意
驱动程序生成的 Query API 表达式可能与每个示例中提供的 Query API 表达式不同。 但是,这两个表达式将生成相同的聚合结果。
重要
The driver does not provide methods for all aggregation pipeline operators in the Query 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)) ));
以下代码在 Query 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
变量分配了中间值。即使这些变量没有获得中间值,代码仍然会产生相同的结果。
以下代码在 Query API 中提供等效的聚合管道:
[ { $project: { availableShowtimes: { $filter: { input: "$showtimes", as: "showtime", cond: { $lt: [ "$$showtime.ticketsBought", { $sum: "$$showtime.seats" } ] } } } } } ]
布尔运算
您可以使用本节中描述的方法对类型为 MqlBoolean
的值执行布尔运算。
假设您想将极低或极高的天气温度读数(华氏度)归类为极端温度。
or()
操作符通过比较 temperature
字段与 lt()
和 gt()
的预定义值,检查温度是否极端。project()
方法将此结果记录在 extremeTemp
字段中。
以下代码展示此聚合的管道:
var temperature = current().getInteger("temperature"); asList(project(fields( 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()
方法用于更复杂的比较操作,例如对值类型执行模式匹配或对该值执行其他任意检查。
Java 方法 | 聚合管道操作符 |
---|---|
以下示例展示与 location
字段的值为 "California"
的所有文档相匹配的管道:
var location = current().getString("location"); asList(match(expr(location.eq(of("California")))));
以下代码在 Query 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")))) )));
以下代码在 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
的值。
为了提高可读性并允许代码重用,您可以将冗余代码移至静态方法中。但是,在 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)))) )));
以下代码在 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
类型之间进行转换。
Java 方法 | 聚合管道操作符 |
---|---|
没有相应的操作符 | |
没有相应的操作符 | |
假设您有一个包含毕业年份的学生数据集合,并且这些数据以字符串形式存储。您想要计算他们五年重聚的年份并将该值存储在新字段中。
parseInteger()
方法将 graduationYear
转换为整数,这样 add()
就可以计算出重聚年份。addFields()
方法将此结果存储为新的 reunionYear
字段。
以下代码展示此聚合的管道:
var graduationYear = current().getString("graduationYear"); asList(addFields( new Field("reunionYear", graduationYear .parseInteger() .add(5)) ));
以下代码在 Query 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
进行比较,后者根据所提供的时区参数对应于星期一。
以下代码展示此聚合的管道:
var deliveryDate = current().getString("deliveryDate"); asList(match(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
的值执行文档操作。
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")) )));
以下代码在 Query 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
字段中的地图条目。sum()
方法会根据使用 getValue()
方法所检索到的数组中的值来计算各商品的总价值。此示例使用 project()
方法将结果存储为新的 totalInventory
字段。
以下代码展示此聚合的管道:
var warehouses = current().getMap("warehouses"); asList(project(fields( computed("totalInventory", warehouses .entries() .sum(v -> v.getValue())) )));
以下代码在 Query 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()) )));
以下代码在 Query API 中提供等效的聚合管道:
[ { $project: { username: { $toLower: { $concat: ["$lastName", "$employeeID"] } } } } ]
类型检查操作
您可以使用本节中描述的方法对类型为 MqlValue
的值执行类型检查操作。
这些方法不返回布尔值。相反,您提供与该方法指定的类型匹配的默认值。如果校验值与方法类型匹配,则返回校验值。否则,将返回所提供的默认值。如果您要根据数据类型对分支逻辑进行编程,请参阅 switchOn()
。
Java 方法 | 聚合管道操作符 |
---|---|
没有相应的操作符 | |
没有相应的操作符 | |
没有相应的操作符 | |
没有相应的操作符 | |
没有相应的操作符 | |
没有相应的操作符 | |
没有相应的操作符 | |
没有相应的操作符 |
假设您有评级数据的集合。早期版本的评论模式允许用户提交没有星级的负面评论。您希望将这些没有星级的负面评论转换为最低值 1 星。
isNumberOr()
方法返回 rating
的值,如果 rating
不是数字或为空,则返回 1
的值。project()
方法将此值作为新的 numericalRating
字段返回。
以下代码展示此聚合的管道:
var rating = current().getField("rating"); asList(project(fields( computed("numericalRating", rating .isNumberOr(of(1))) )));
以下代码在 Query API 中提供等效的聚合管道:
[ { $project: { numericalRating: { $cond: { if: { $isNumber: "$rating" }, then: "$rating", else: 1 } } } } ]