mongoTemplate 有多表关联吗?
是的,MongoTemplate 支持多表关联! 主要通过 $lookup 聚合阶段来实现类似 SQL JOIN 的功能。让我详细解释:
1. MongoDB 的多表关联机制
1.1 $lookup 聚合操作
JAVASCRIPT
// MongoDB 原生语法
db.orders.aggregate([
{
$lookup: {
from: "customers", // 要连接的表
localField: "customerId", // 本地字段
foreignField: "_id", // 外部表字段
as: "customerInfo" // 输出字段名
}
}
])
1.2 在 Spring 中的对应
JAVA
// Spring Data MongoDB 中的 $lookup
Aggregation.lookup("customers", "customerId", "_id", "customerInfo")
2. 完整的多表关联示例
2.1 数据模型定义
JAVA
// 主表:订单
@Document(collection = "orders")
public class Order {
@Id
private String orderId;
private String customerId; // 关联字段
private BigDecimal amount;
private Date orderDate;
private List<OrderItem> items;
// 嵌入关联结果
private List<Customer> customerInfo; // 关联结果
// getter/setter
}
// 关联表:客户
@Document(collection = "customers")
public class Customer {
@Id
private String customerId;
private String name;
private String email;
private String phone;
// getter/setter
}
// 订单项
public class OrderItem {
private String productId;
private Integer quantity;
private BigDecimal price;
// getter/setter
}
2.2 基本关联查询(一对一)
JAVA
public List<Order> getOrdersWithCustomerInfo() {
Aggregation aggregation = Aggregation.newAggregation(
// 1. 连接客户表
Aggregation.lookup(
"customers", // 要关联的集合名
"customerId", // 本地字段(orders 表的字段)
"_id", // 外部字段(customers 表的字段)
"customerInfo" // 输出字段名
),
// 2. 展开 customerInfo 数组(因为 $lookup 返回的是数组)
Aggregation.unwind("customerInfo", true), // preserveNullAndEmptyArrays = true
// 3. 可选:过滤条件
Aggregation.match(
Criteria.where("amount").gt(1000)
.and("orderDate").gte(LocalDate.now().minusMonths(1))
),
// 4. 可选:排序
Aggregation.sort(Sort.by("orderDate").descending()),
// 5. 可选:投影字段
Aggregation.project()
.and("orderId").as("orderId")
.and("amount").as("amount")
.and("orderDate").as("orderDate")
.and("customerInfo.name").as("customerName")
.and("customerInfo.email").as("customerEmail")
.andExclude("_id")
);
return mongoTemplate.aggregate(
aggregation,
"orders",
Order.class
).getMappedResults();
}
3. 多种关联模式
3.1 一对一关联
JAVA
// 一个订单对应一个客户
public List<Order> getOneToOneOrders() {
return mongoTemplate.aggregate(
Aggregation.newAggregation(
Aggregation.lookup("customers", "customerId", "_id", "customer"),
Aggregation.unwind("customer", true), // 展开为一对一
Aggregation.match(Criteria.where("customer.status").is("ACTIVE"))
),
"orders",
Order.class
).getMappedResults();
}
3.2 一对多关联
JAVA
// 一个订单有多个商品,关联商品详情
public List<OrderWithProducts> getOrdersWithProducts() {
Aggregation aggregation = Aggregation.newAggregation(
// 1. 展开订单项数组
Aggregation.unwind("items"),
// 2. 关联商品表
Aggregation.lookup("products", "items.productId", "_id", "productDetails"),
// 3. 展开商品详情数组
Aggregation.unwind("productDetails", true),
// 4. 重新分组
Aggregation.group("orderId")
.first("customerId").as("customerId")
.first("orderDate").as("orderDate")
.push(
new Document("productName", "$productDetails.name")
.append("price", "$productDetails.price")
.append("quantity", "$items.quantity")
).as("products")
);
return mongoTemplate.aggregate(
aggregation,
"orders",
OrderWithProducts.class
).getMappedResults();
}
3.3 多对多关联
JAVA
// 订单-产品多对多(通过订单项关联)
public List<OrderProductDetail> getOrderProductDetails() {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.unwind("items"),
Aggregation.lookup("products", "items.productId", "_id", "productInfo"),
Aggregation.unwind("productInfo"),
// 关联产品分类
Aggregation.lookup("categories", "productInfo.categoryId", "_id", "categoryInfo"),
Aggregation.unwind("categoryInfo", true),
// 关联供应商
Aggregation.lookup("suppliers", "productInfo.supplierId", "_id", "supplierInfo"),
Aggregation.unwind("supplierInfo", true),
Aggregation.project()
.and("orderId").as("orderId")
.and("items.quantity").as("quantity")
.and("productInfo.name").as("productName")
.and("categoryInfo.name").as("categoryName")
.and("supplierInfo.name").as("supplierName")
);
return mongoTemplate.aggregate(
aggregation,
"orders",
OrderProductDetail.class
).getMappedResults();
}
4. 复杂关联场景
4.1 多层嵌套关联
JAVA
// 订单 -> 客户 -> 地址 -> 城市
public List<OrderWithDetails> getOrderWithFullDetails() {
Aggregation aggregation = Aggregation.newAggregation(
// 第一层:关联客户
Aggregation.lookup("customers", "customerId", "_id", "customerInfo"),
Aggregation.unwind("customerInfo", true),
// 第二层:通过客户关联地址
Aggregation.lookup("addresses", "customerInfo.addressId", "_id", "addressInfo"),
Aggregation.unwind("addressInfo", true),
// 第三层:通过地址关联城市
Aggregation.lookup("cities", "addressInfo.cityId", "_id", "cityInfo"),
Aggregation.unwind("cityInfo", true),
// 第四层:通过城市关联省份
Aggregation.lookup("provinces", "cityInfo.provinceId", "_id", "provinceInfo"),
Aggregation.unwind("provinceInfo", true),
Aggregation.project()
.and("orderId").as("orderId")
.and("customerInfo.name").as("customerName")
.and("addressInfo.street").as("street")
.and("cityInfo.name").as("cityName")
.and("provinceInfo.name").as("provinceName")
);
return mongoTemplate.aggregate(
aggregation,
"orders",
OrderWithDetails.class
).getMappedResults();
}
4.2 双向关联
JAVA
// 双向关联:既查订单的客户,也查客户的订单
public List<CustomerWithOrders> getCustomersWithRecentOrders() {
Aggregation aggregation = Aggregation.newAggregation(
// 从客户表开始
Aggregation.lookup("orders", "_id", "customerId", "customerOrders"),
// 过滤最近30天有订单的客户
Aggregation.match(
Criteria.where("customerOrders.orderDate")
.gte(LocalDate.now().minusDays(30))
),
// 过滤客户的订单(只保留最近30天的)
Aggregation.addFields()
.addFieldWithValue("recentOrders",
AggregationSpELExpression.expressionOf(
"filter(customerOrders, 'order', order.orderDate >= ?0)",
LocalDate.now().minusDays(30)
)
).build(),
// 计算订单统计
Aggregation.project()
.and("name").as("customerName")
.and("recentOrders").as("orders")
.and(ArrayOperators.Size.lengthOfArray("recentOrders")).as("orderCount")
.and").as("totalAmount")
);
return mongoTemplate.aggregate(
aggregation,
"customers",
CustomerWithOrders.class
).getMappedResults();
}
5. 关联条件过滤
5.1 带条件的关联
JAVA
// MongoDB 4.0+ 支持带条件的 $lookup
public List<Order> getOrdersWithActiveCustomers() {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.lookup()
.from("customers")
.localField("customerId")
.foreignField("_id")
.pipeline( // 在关联管道中添加条件
Aggregation.newAggregation(
Aggregation.match(
Criteria.where("status").is("ACTIVE")
.and("createdDate").gte(LocalDate.now().minusYears(1))
)
).getPipeline()
)
.as("activeCustomer"),
Aggregation.unwind("activeCustomer", true),
Aggregation.match(Criteria.where("activeCustomer").exists(true))
);
return mongoTemplate.aggregate(
aggregation,
"orders",
Order.class
).getMappedResults();
}
5.2 关联后过滤
JAVA
// 先关联,再过滤关联结果
public List<Order> getOrdersFromVipCustomers() {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.lookup("customers", "customerId", "_id", "customerInfo"),
// 过滤关联结果
Aggregation.addFields()
.addField("vipCustomer")
.withValue(
ConditionalOperators.filter("customerInfo")
.as("cust")
.by(
ComparisonOperators.Eq.valueOf("cust.level")
.equalToValue("VIP")
)
).build(),
// 只保留有VIP客户的订单
Aggregation.match(
Criteria.where("vipCustomer").ne(Collections.emptyList())
),
Aggregation.unwind("vipCustomer")
);
return mongoTemplate.aggregate(
aggregation,
"orders",
Order.class
).getMappedResults();
}
6. 关联性能优化
6.1 索引优化
JAVA
// 确保关联字段有索引
@Document(collection = "orders")
@CompoundIndex(name = "customer_order_idx", def = "{'customerId': 1, 'orderDate': -1}")
public class Order {
// ...
}
@Document(collection = "customers")
@Indexed(name = "customer_id_idx")
public class Customer {
@Id
private String id;
// ...
}
// 查询时使用索引
public List<Order> getOrdersWithIndexedLookup() {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.match(
Criteria.where("orderDate").gte(LocalDate.now().minusMonths(1))
),
Aggregation.lookup("customers", "customerId", "_id", "customerInfo"),
Aggregation.unwind("customerInfo", true)
);
return mongoTemplate.aggregate(
aggregation,
"orders",
Order.class
).getMappedResults();
}
6.2 分页关联
JAVA
// 关联查询分页
public Page<OrderWithCustomer> getOrdersWithCustomerPage(int page, int size) {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.lookup("customers", "customerId", "_id", "customerInfo"),
Aggregation.unwind("customerInfo", true),
Aggregation.sort(Sort.by("orderDate").descending()),
Aggregation.skip((long) page * size),
Aggregation.limit(size)
);
List<OrderWithCustomer> content = mongoTemplate.aggregate(
aggregation,
"orders",
OrderWithCustomer.class
).getMappedResults();
// 获取总数
long total = mongoTemplate.count(new Query(), "orders");
return new PageImpl<>(content, PageRequest.of(page, size), total);
}
7. 关联结果处理
7.1 自定义结果映射
JAVA
// 使用自定义DTO接收关联结果
public class OrderWithCustomerDTO {
private String orderId;
private BigDecimal amount;
private String customerName;
private String customerEmail;
// 构造函数用于聚合结果映射
public OrderWithCustomerDTO(String orderId, BigDecimal amount,
String customerName, String customerEmail) {
this.orderId = orderId;
this.amount = amount;
this.customerName = customerName;
this.customerEmail = customerEmail;
}
// getter/setter
}
// 关联查询返回DTO
public List<OrderWithCustomerDTO> getOrderCustomerDTOs() {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.lookup("customers", "customerId", "_id", "customerInfo"),
Aggregation.unwind("customerInfo"),
Aggregation.project()
.and("orderId").as("orderId")
.and("amount").as("amount")
.and("customerInfo.name").as("customerName")
.and("customerInfo.email").as("customerEmail")
.andExclude("_id")
);
return mongoTemplate.aggregate(
aggregation,
"orders",
OrderWithCustomerDTO.class
).getMappedResults();
}
7.2 嵌套结果
JAVA
// 关联结果保持嵌套结构
public class OrderWithNestedCustomer {
private String orderId;
private Customer customer; // 嵌套对象
// getter/setter
}
public List<OrderWithNestedCustomer> getOrdersWithNestedCustomer() {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.lookup("customers", "customerId", "_id", "customers"),
// 将 customers 数组转换为对象
Aggregation.addFields()
.addField("customer")
.withValue(ArrayOperators.ArrayElemAt.arrayOf("customers").elementAt(0))
.build(),
Aggregation.project()
.and("orderId").as("orderId")
.and("customer").as("customer")
.andExclude("_id")
);
return mongoTemplate.aggregate(
aggregation,
"orders",
OrderWithNestedCustomer.class
).getMappedResults();
}
8. 高级关联特性
8.1 $graphLookup 递归关联
JAVA
// 递归查询组织结构(查找所有下级)
public List<Employee> getEmployeeHierarchy(String managerId) {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.match(Criteria.where("_id").is(managerId)),
Aggregation.graphLookup("employees")
.startWith("$_id")
.connectFrom("_id")
.connectTo("managerId")
.as("subordinates")
.maxDepth(10) // 最大递归深度
.depthField("level")
);
return mongoTemplate.aggregate(
aggregation,
"employees",
Employee.class
).getMappedResults();
}
8.2 $unionWith 联合查询
JAVA
// 合并多个集合的查询结果
public List<Product> searchAllProducts(String keyword) {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.unionWith("electronics")
.pipeline(
Aggregation.match(
Criteria.where("name").regex(keyword, "i")
)
),
Aggregation.unionWith("clothing")
.pipeline(
Aggregation.match(
Criteria.where("name").regex(keyword, "i")
)
),
Aggregation.unionWith("books")
.pipeline(
Aggregation.match(
Criteria.where("title").regex(keyword, "i")
)
),
Aggregation.sort(Sort.by("price").ascending())
);
return mongoTemplate.aggregate(
aggregation,
"products",
Product.class
).getMappedResults();
}
9. 关联查询性能对比
9.1 应用程序级关联 vs 数据库级关联
JAVA
// 方式1:应用程序级关联(多次查询)
public List<Order> getOrdersWithCustomersAppLevel() {
// 1. 查询订单
List<Order> orders = mongoTemplate.find(
Query.query(Criteria.where("status").is("ACTIVE")),
Order.class
);
// 2. 收集客户ID
Set<String> customerIds = orders.stream()
.map(Order::getCustomerId)
.collect(Collectors.toSet());
// 3. 批量查询客户
List<Customer> customers = mongoTemplate.find(
Query.query(Criteria.where("_id").in(customerIds)),
Customer.class
);
// 4. 手动组装
Map<String, Customer> customerMap = customers.stream()
.collect(Collectors.toMap(Customer::getId, c -> c));
orders.forEach(order ->
order.setCustomerInfo(Collections.singletonList(
customerMap.get(order.getCustomerId())
))
);
return orders;
}
// 方式2:数据库级关联(推荐,一次查询)
public List<Order> getOrdersWithCustomersDbLevel() {
return mongoTemplate.aggregate(
Aggregation.newAggregation(
Aggregation.match(Criteria.where("status").is("ACTIVE")),
Aggregation.lookup("customers", "customerId", "_id", "customerInfo"),
Aggregation.unwind("customerInfo", true)
),
"orders",
Order.class
).getMappedResults();
}
10. 实战示例:电商订单系统
JAVA
@Service
public class OrderService {
@Autowired
private MongoTemplate mongoTemplate;
public OrderDetail getOrderDetail(String orderId) {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.match(Criteria.where("orderId").is(orderId)),
// 关联客户信息
Aggregation.lookup("customers", "customerId", "_id", "customerInfo"),
Aggregation.unwind("customerInfo", true),
// 展开订单项
Aggregation.unwind("items"),
// 关联商品信息
Aggregation.lookup("products", "items.productId", "_id", "productInfo"),
Aggregation.unwind("productInfo", true),
// 关联商品分类
Aggregation.lookup("categories", "productInfo.categoryId", "_id", "categoryInfo"),
Aggregation.unwind("categoryInfo", true),
// 重新分组
Aggregation.group("orderId")
.first("customerInfo").as("customer")
.first("orderDate").as("orderDate")
.first("shippingAddress").as("shippingAddress")
.first("paymentMethod").as("paymentMethod")
.sum("items.quantity").as("totalQuantity")
.sum(
ArithmeticOperators.Multiply
.valueOf("items.price")
.multiplyBy("items.quantity")
).as("totalAmount")
.push(
new Document("productName", "$productInfo.name")
.append("category", "$categoryInfo.name")
.append("price", "$productInfo.price")
.append("quantity", "$items.quantity")
.append("subtotal",
ArithmeticOperators.Multiply
.valueOf("$productInfo.price")
.multiplyBy("$items.quantity")
)
).as("orderItems"),
// 关联物流信息
Aggregation.lookup("logistics", "orderId", "orderId", "logisticsInfo"),
Aggregation.unwind("logisticsInfo", true),
// 最终投影
Aggregation.project()
.and("orderId").as("orderId")
.and("customer.name").as("customerName")
.and("customer.phone").as("customerPhone")
.and("orderDate").as("orderDate")
.and("totalQuantity").as("totalQuantity")
.and("totalAmount").as("totalAmount")
.and("orderItems").as("items")
.and("logisticsInfo.trackingNumber").as("trackingNumber")
.and("logisticsInfo.status").as("deliveryStatus")
);
return mongoTemplate.aggregate(
aggregation,
"orders",
OrderDetail.class
).getUniqueMappedResult();
}
}
11. 注意事项和限制
11.1 MongoDB 关联的限制
JAVA
// 1. 只能关联同一数据库的集合
// 2. 关联字段必须有索引才能高效
// 3. 大数据集关联可能性能较差
// 4. 不支持全外连接,只支持左外连接
// 性能优化建议
Aggregation aggregation = Aggregation.newAggregation(
// 1. 先过滤,减少数据量
Aggregation.match(Criteria.where("status").is("ACTIVE")),
// 2. 再关联
Aggregation.lookup("customers", "customerId", "_id", "customerInfo"),
// 3. 使用 allowDiskUse 处理大数据
Aggregation.unwind("customerInfo")
).withOptions(
AggregationOptions.builder()
.allowDiskUse(true)
.build()
);
总结
MongoTemplate 支持多表关联,主要通过:
$lookup聚合阶段:实现左外连接$graphLookup:递归关联查询$unionWith:合并多个集合- 多种关联模式:一对一、一对多、多对多
- 关联条件过滤:支持带条件的关联
- 性能优化:索引、分页、批处理
虽然 MongoDB 是文档数据库,不推荐过度关联,但 $lookup 提供了必要的关联能力,适合适度使用的场景。
最佳实践:
- 优先考虑嵌入式文档设计
- 关联查询确保字段有索引
- 大数据集使用分页
- 考虑应用程序级关联的替代方案