Relationers Service Kind

Overview

Relationers are entity-specific services responsible for loading and distributing related data into domain model collections. They handle the actual data fetching for relationships and provide efficient batch loading with proper data distribution across entity collections.

Architecture

Core Components

Base Relationer Library (src/lib/relationer/) - Relationer struct: Core relationship loading coordinator - RelationerMod: Configuration object with fetcher modifications - Entity interface: Common interface for entities with ID access

Entity-Specific Relationers (src/srv/relationers/) - Each domain model has its own dedicated relationer (e.g., GardenRelationer, UserRelationer) - All relationers follow the same interface pattern and method conventions - Integrated with corresponding Fetcher services for data retrieval

Key Structures

RelationerMod

Configuration object that controls relationship loading: - FetcherMod: Embedded fetcher modification for query customization - Passed through to underlying fetchers for filtering, ordering, etc.

Entity Interface

Common interface that all domain models implement:

type Entity interface {
    GetId() string
}

Standard Methods

Every entity relationer implements relationship-specific methods following these patterns:

Hydrate Methods

Hydrate{RelationshipName}(ctx, entities, mod) error
  • Load related data and distribute it to entity collections
  • Batch-optimized for loading relationships across multiple entities
  • Handle empty collections and missing relationships gracefully

Distribute Methods

Distribute{RelationshipName}(ctx, entities, relatedData)
  • Internal methods for distributing fetched data to appropriate entities
  • Match related entities by foreign key relationships
  • Append related data to entity relationship slices

Conversion Methods

convert{EntityName}ToEntities(entities) []relationer.Entity
  • Convert typed entity collections to generic Entity interface
  • Enable use of generic relationship loading utilities
  • Support polymorphic relationship handling

Usage Examples

Loading Single Relationship

// Load garden tasks for a collection of gardens
err := gardenRelationer.HydrateGardenTasks(ctx, gardens, nil)
// gardens[0].GardenTasks now contains related garden tasks

Loading with Fetcher Modifications

// Load plantations with specific filtering
mod := &relationer.RelationerMod{
    FetcherMod: plantationFetcher.Mod(),
}
mod.FetcherMod.ExactBooleanValueFilter("active", true)
err := gardenRelationer.HydratePlantations(ctx, gardens, mod)

Multiple Relationship Loading

// Load multiple relationships (typically done via Hydrators)
err = gardenRelationer.HydrateGardenTasks(ctx, gardens, nil)
if err != nil { return err }
err = gardenRelationer.HydratePlantations(ctx, gardens, nil)
if err != nil { return err }

Relationship Loading Process

1. ID Extraction

ids := this.Relationer.GetUniqueEntitiesIds(ctx, this.convertGardensToEntities(set))
if len(ids) == 0 {
    return nil  // No entities to process
}

2. Batch Data Fetching

setToMerge, err := this.GardenTaskFetcher.FindSetInGardenIds(ctx, ids, this.Relationer.GetFetcherMod(mod))
if err != nil {
    return err
}

3. Data Distribution

this.DistributeGardenTasks(ctx, set, setToMerge)

Data Distribution Logic

Relationers implement efficient N+1 query prevention through batch loading:

Foreign Key Matching

func (this *GardenRelationer) DistributeGardenTasks(ctx context.Context, set []*mdl.Garden, setToMerge []*mdl.GardenTask) {
    for _, item := range set {
        for _, itemToMerge := range setToMerge {
            if item.Id == itemToMerge.GardenId {
                item.GardenTasks = append(item.GardenTasks, itemToMerge)
            }
        }
    }
}

Unique ID Collection

The base Relationer provides utilities for collecting unique IDs: - Eliminates duplicate IDs to prevent redundant fetching - Handles empty/nil IDs gracefully - Returns deduplicated ID collections for batch queries

Integration with Framework

With Fetchers

Relationers delegate data retrieval to appropriate Fetchers:

setToMerge, err := this.PlantationFetcher.FindSetInGardenIds(ctx, ids, fetcherMod)

With Hydrators

Hydrators orchestrate multiple Relationers for complete entity hydration:

// In GardenHydrator.Many()
if mod.Contains("garden_tasks") {
    this.GardenRelationer.HydrateGardenTasks(ctx, entities, nil)
}

Performance Optimization

Batch Loading

  • Single query per relationship type regardless of entity count
  • Prevents N+1 query problems common in ORM systems
  • Efficiently handles large entity collections

Early Exit Conditions

  • Skip processing when no entities are provided
  • Return early when entity collections are empty
  • Avoid unnecessary database queries

Memory Efficiency

  • Stream processing of relationship data
  • In-place entity modification rather than copying
  • Efficient ID deduplication using maps

Error Handling

  • Propagate fetcher errors to calling code
  • Handle missing relationships gracefully (empty slices)
  • Maintain entity integrity when relationship loading fails
  • Support partial success scenarios

Relationship Types

Relationers handle various relationship patterns: - One-to-Many: Parent entity with multiple child entities - Many-to-One: Child entities referencing parent entity - Many-to-Many: Junction table relationships (through intermediate fetchers) - Nested Relationships: Multi-level relationship chains