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