Relationships Feature Guide

The Relationships feature provides automatic bidirectional associations between Familia objects, eliminating manual foreign key management while enabling efficient queries through Redis-native data structures.

[!TIP] Enable with feature :relationships and define associations using participates_in for automatic method generation.

Quick Start

class Customer < Familia::Horreum
  feature :relationships
  # Collection 'domains' created automatically
end

class Domain < Familia::Horreum
  feature :relationships
  participates_in Customer, :domains
end

# Automatic bidirectional relationship management
customer.add_domains_instance(domain)      # Add relationship
domain.in_customer_domains?(customer)      # => true
domain.customer_instances                  # => [customer]

Core Capabilities

Participation - Bidirectional Associations

Create semantic relationships between objects with automatic reverse tracking:

class User < Familia::Horreum
  feature :relationships
  participates_in Team, :members, score: :joined_at
  participates_in Team, :admins
end

# Generated methods on Team (target)
team.add_members_instance(user)           # Add single member
team.add_members([user1, user2])          # Bulk add
team.members.range(0, 9)                  # First 10 members

# Generated methods on User (participant)
user.add_to_team_members(team)            # Add self to team
user.in_team_admins?(team)                # Check membership
user.team_instances                       # All teams (members + admins)

Indexing - Fast Attribute Lookups

Enable O(1) field-based queries with automatic index management:

class User < Familia::Horreum
  feature :relationships
  field :email, :username

  # Global unique indexes (auto-managed on save/destroy)
  unique_index :email, :email_lookup
  unique_index :username, :username_lookup
end

User.find_by_email("alice@example.com")   # O(1) lookup
User.find_by_username("alice")            # O(1) lookup

# Scoped indexing (manual management required)
class Employee < Familia::Horreum
  feature :relationships
  unique_index :badge_number, :badge_index, within: Company
  multi_index :department, :dept_index, within: Company
end

employee.add_to_company_badge_index(company)
company.find_by_badge_number("12345")     # Scoped lookup
company.find_all_by_department("engineering")  # Multi-value

Scoring - Semantic Ordering

Use scores for temporal tracking, priority systems, or custom ordering:

class Task < Familia::Horreum
  feature :relationships
  field :priority, :created_at

  # Field-based scoring
  participates_in Project, :tasks, score: :priority

  # Lambda-based scoring
  participates_in Sprint, :tasks, score: -> {
    priority * 100 + (Familia.now - created_at) / 3600
  }
end

project.tasks.range(0, 4, order: 'DESC')  # Top 5 by priority
sprint.tasks.range_by_score(500, '+inf')  # High priority tasks

Generated Method Reference

When Domain declares participates_in Customer, :domains

Class Method Purpose
Customer domains Access collection
add_domains_instance(domain) Add single item
add_domains([domains]) Bulk add
remove_domains_instance(domain) Remove item
Domain add_to_customer_domains(customer) Add to collection
remove_from_customer_domains(customer) Remove from collection
in_customer_domains?(customer) Check membership
score_in_customer_domains(customer) Get score (sorted_set)
customer_instances Load all customers
customer_ids Get customer IDs
customer? Has any customers?
customer_count Count relationships

Common Patterns

Multiple Collections

class User < Familia::Horreum
  feature :relationships
  participates_in Project, :contributors
  participates_in Project, :reviewers
  participates_in Organization, :employees, as: :employers
end

# Separate methods per collection
user.add_to_project_contributors(project)
user.add_to_project_reviewers(project)

# Custom reverse method names
user.employers_instances  # Instead of organization_instances

Class-Level Tracking

class Customer < Familia::Horreum
  feature :relationships
  class_participates_in :all_customers, score: :created_at
  class_participates_in :premium_customers,
    score: ->(c) { c.tier == 'premium' ? c.last_activity : 0 }
end

Customer.all_customers.size               # Total count
Customer.premium_customers.range(0, 9)    # Top 10 premium

Performance Optimization

# Bulk operations
team.add_members([user1, user2, user3])

# Pagination
team.members.range(0, 19)                 # First 20
team.members.range(20, 39)                # Next 20

# Direct ID access (no object loading)
team.members.to_a                         # Just IDs
team.member_instances                     # Load objects

Serialization of Collection Members

Relationship collections (participation sorted sets, index hash keys, instance-scoped sets) store object identifiers as raw strings. When adding objects to these collections, serialize_value extracts the .identifier from Familia objects and stores it without JSON encoding. This ensures consistent membership checks regardless of whether code passes an object reference or a string identifier. See Collection Member Serialization for the authoritative explanation of why reference collections use raw identifiers while value fields use JSON.

Best Practices

  1. Use bulk methods for multiple additions: add_domains([d1, d2, d3])
  2. Paginate large collections: range(0, 19) instead of loading all
  3. Leverage reverse methods: domain.customer_instances for efficient loading
  4. Clean up on destroy: Call cleanup_relationships before deletion
  5. Validate before adding: Check capacity/eligibility in overridden methods

See Also