Lookup & Joins
Join collections using MongoDB's $lookup stage - similar to SQL joins.
Simple Lookup (Equality Match)
Join two collections on matching field values:
data class OrderWithCustomer(
val id: String,
val total: Double,
val customerInfo: List<Customer>
)
val results = konduct.collection<Order>()
.lookup<Customer> {
from(Customer::class)
localField(Order::customerId)
foreignField(Customer::id)
into("customerInfo")
}
.into<OrderWithCustomer>()
.toList()
Result structure:
OrderWithCustomer(
id = "order123",
total = 150.0,
customerInfo = [
Customer(id = "c1", name = "John Doe", email = "john@example.com")
]
)
Unwinding Results
Convert array results to individual documents:
val results = konduct.collection<Order>()
.lookup<Customer> {
from(Customer::class)
localField(Order::customerId)
foreignField(Customer::id)
into("customer")
}
.unwind("customer") // Flatten array to object
.into<Document>()
.toList()
Before unwind:
After unwind:
Preserve Null and Empty Arrays
Keep documents even when lookup finds no matches:
konduct.collection<Order>()
.lookup<Customer> {
from(Customer::class)
localField(Order::customerId)
foreignField(Customer::id)
into("customer")
}
.unwind("customer", preserveNullAndEmptyArrays = true)
.into<Document>()
.toList()
Pipeline-Based Lookup
For complex joins with filtering and transformations:
konduct.collection<Order>()
.lookup<OrderItem> {
from(OrderItem::class)
let {
"orderId" to Order::id
}
pipeline {
match<OrderItem> {
expr {
OrderItem::orderId eq variable("orderId")
}
}
match<OrderItem> {
OrderItem::quantity gte 5
}
sort {
"quantity" from -1
}
limit(10)
}
into("items")
}
.into<OrderWithItems>()
.toList()
Available Pipeline Operations
Inside pipeline { }:
match { }- Filter joined documentssort { }- Order resultslimit(n)- Limit resultsskip(n)- Skip documentsproject { }- Select/transform fields
Using Variables
Access parent document fields in lookup pipeline:
konduct.collection<Product>()
.lookup<Review> {
from(Review::class)
let {
"productId" to Product::id
"minRating" to Product::minRating
}
pipeline {
match<Review> {
expr {
and(
Review::productId eq variable("productId"),
Review::rating gte variable("minRating")
)
}
}
}
into("goodReviews")
}
.toList()
Lookup and Merge
Flatten joined fields directly into parent document:
data class OrderWithCustomerInfo(
val id: String,
val total: Double,
val customerName: String,
val customerEmail: String,
val customerTier: String
)
val results = konduct.collection<Order>()
.lookupAndMerge(Customer::class) {
from()
on(Order::customerId, Customer::id)
merge(Customer::name, Customer::email, Customer::tier)
}
.into<OrderWithCustomerInfo>()
.toList()
Result (fields merged at top level):
OrderWithCustomerInfo(
id = "order123",
total = 150.0,
customerName = "John Doe", // From Customer
customerEmail = "john@example.com", // From Customer
customerTier = "gold" // From Customer
)
Multiple Lookups
Chain multiple lookups:
konduct.collection<Order>()
.lookup<Customer> {
from(Customer::class)
localField(Order::customerId)
foreignField(Customer::id)
into("customer")
}
.lookup<Product> {
from(Product::class)
localField(Order::productId)
foreignField(Product::id)
into("product")
}
.lookup<Warehouse> {
from(Warehouse::class)
localField(Order::warehouseId)
foreignField(Warehouse::id)
into("warehouse")
}
.into<Document>()
.toList()
Real-World Examples
Order with Customer Details
@Service
class OrderService(mongoTemplate: MongoTemplate) {
private val konduct = Konduct(mongoTemplate)
fun getOrderWithCustomer(orderId: String): OrderDetails? {
return konduct.collection<Order>()
.match { Order::id eq orderId }
.lookup<Customer> {
from(Customer::class)
localField(Order::customerId)
foreignField(Customer::id)
into("customer")
}
.unwind("customer")
.lookup<OrderItem> {
from(OrderItem::class)
let {
"orderId" to Order::id
}
pipeline {
match<OrderItem> {
expr {
OrderItem::orderId eq variable("orderId")
}
}
}
into("items")
}
.into<OrderDetails>()
.firstOrNull()
}
}
Product with Recent Reviews
fun getProductWithReviews(productId: String, limit: Int = 5): ProductWithReviews? {
return konduct.collection<Product>()
.match { Product::id eq productId }
.lookup<Review> {
from(Review::class)
let {
"productId" to Product::id
}
pipeline {
match<Review> {
expr {
Review::productId eq variable("productId")
}
}
sort {
"createdAt" from -1
}
limit(limit)
}
into("recentReviews")
}
.into<ProductWithReviews>()
.firstOrNull()
}
Customer Purchase History
data class CustomerWithOrders(
val id: String,
val name: String,
val email: String,
val orders: List<Order>
)
fun getCustomerPurchaseHistory(customerId: String): CustomerWithOrders? {
return konduct.collection<Customer>()
.match { Customer::id eq customerId }
.lookup<Order> {
from(Order::class)
let {
"customerId" to Customer::id
}
pipeline {
match<Order> {
expr {
Order::customerId eq variable("customerId")
}
}
sort {
"orderDate" from -1
}
limit(20)
}
into("orders")
}
.into<CustomerWithOrders>()
.firstOrNull()
}
Type Conversion with into()
Convert pipeline result type at any point:
// Convert to Document
.into<Document>()
// Convert to custom type
.into<OrderWithCustomer>()
// Use with KClass
.into(OrderWithCustomer::class)
Performance Tips
-
Index join fields:
-
Limit joined results:
-
Filter early:
-
Use lookupAndMerge for flat structures:
Common Patterns
Left Outer Join
konduct.collection<Order>()
.lookup<Customer> {
from(Customer::class)
localField(Order::customerId)
foreignField(Customer::id)
into("customer")
}
.unwind("customer", preserveNullAndEmptyArrays = true)
.toList()
Join with Aggregation
konduct.collection<Product>()
.lookup<Sale> {
from(Sale::class)
let {
"productId" to Product::id
}
pipeline {
match<Sale> {
expr {
Sale::productId eq variable("productId")
}
}
group {
by { }
accumulate {
"totalSales" sum Sale::amount
"salesCount" count Unit
}
}
}
into("salesStats")
}
.toList()
Self-Referencing Lookup
// Find employees and their managers
konduct.collection<Employee>()
.lookup<Employee> {
from(Employee::class)
localField(Employee::managerId)
foreignField(Employee::id)
into("manager")
}
.unwind("manager", preserveNullAndEmptyArrays = true)
.toList()
See Also
- Match & Filter - Filter in lookup pipelines
- Grouping - Aggregate joined data
- Expressions - Use expr for variables