Skip to main content
ChameleonDB supports eager loading of related entities using the Include() method. This prevents N+1 query problems and efficiently loads related data.

Eager vs Lazy Loading

Eager Loading

Load relations alongside the main query using Include()

Lazy Loading

Without Include(), relations are not fetched automatically
Important: Without .Include(), relations are not loaded. You must explicitly request them.

Include (Eager Loading)

Load related entities alongside the main query:
users, err := db.Users().
    Include("orders").
    Execute()
Design choice: ChameleonDB uses separate queries for eager loading (not JOINs) to avoid row duplication and keep results clean.

Why Separate Queries?

ChameleonDB uses separate queries instead of JOINs for eager loading because:
  1. No row duplication - JOINs duplicate parent rows for each child
  2. Cleaner results - Each entity appears exactly once
  3. Better performance - For one-to-many relations, separate queries can be faster
  4. Simpler result mapping - No need to deduplicate or group rows

Nested Include

Load relations multiple levels deep:
users, err := db.Users().
    Include("orders").
    Include("orders.items").
    Execute()
You can chain as many nested includes as needed. Each level triggers a separate query.

IdentityMap Deduplication

ChameleonDB automatically deduplicates objects in memory using an IdentityMap:
// If User has 100 posts, User object is duplicated 100 times in memory
// Memory: ~100x duplication

How IdentityMap Works

  1. When loading related data, ChameleonDB tracks entities by their primary key
  2. If the same entity appears multiple times, only one instance is kept in memory
  3. All references point to the same object
  4. This dramatically reduces memory usage for large result sets
Performance: IdentityMap is enabled by default and requires no configuration. It’s especially beneficial when loading many-to-one or many-to-many relationships.

Query with Relations

Example using the Go SDK:
// Query users with their posts
result, err := eng.Query("User").
    Select("id", "name", "email").
    Include("posts").
    Execute(ctx)

if err != nil {
    log.Fatal(err)
}

for _, user := range result.Rows {
    fmt.Printf("User: %s\n", user["name"])
    
    if posts, ok := result.Relations["posts"]; ok {
        fmt.Printf("  Posts: %d\n", len(posts))
        for _, post := range posts {
            fmt.Printf("  - %s\n", post["title"])
        }
    }
}
Filter the main entity based on a condition on a related entity. This is different from filtering the included results:
users, err := db.Users().
    Filter("orders.total", "gt", 100).
    Execute()
When filtering on a relation, ChameleonDB uses a JOIN automatically. DISTINCT is added to avoid duplicates when a user has multiple matching orders.

Filter + Include Combined

You can filter on a relation and also include it. The filter affects which users are returned; the include loads all their orders (not just matching ones):
users, err := db.Users().
    Filter("orders.total", "gt", 100).
    Include("orders").
    Execute()
Key distinction:
  • The filter determines which users have at least one order > 100
  • The include loads all orders for those users (including orders ≤ 100)

N+1 Query Problem

Without eager loading, you risk the N+1 query problem:
// ❌ Bad: N+1 queries (1 for users + N for each user's orders)
users := db.Users().Execute()
for _, user := range users {
    orders := db.Orders().Filter("user_id", "eq", user.ID).Execute()
    // Separate query for EACH user!
}

// ✅ Good: 2 queries total (1 for users + 1 for all orders)
users := db.Users().Include("orders").Execute()
When you include relations, they’re available in the result:
result, err := db.Query("User").
    Include("orders").
    Include("orders.items").
    Execute(ctx)

// Access main entities
for _, user := range result.Rows {
    fmt.Printf("User: %s\n", user["name"])
}

// Access related data
if orders, ok := result.Relations["orders"]; ok {
    for _, order := range orders {
        fmt.Printf("Order: %v\n", order["total"])
    }
}

if items, ok := result.Relations["orders.items"]; ok {
    for _, item := range items {
        fmt.Printf("Item: %v\n", item["quantity"])
    }
}

What’s Next?