Relationship Methods Reference

This guide provides detailed documentation for all methods automatically generated by Familia's relationships feature. Each relationship declaration creates a comprehensive set of methods for managing object associations, indexing, and tracking.

Method Generation Overview

The relationships feature uses consistent naming patterns to generate methods based on your declarations. Understanding these patterns helps you predict and use the available methods effectively.

Participation Methods (participates_in)

Basic Participation Declaration

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

class Customer < Familia::Horreum
  feature :relationships
  set :domains  # Must define the collection
end

Generated Instance Methods

On Domain instances:

  • add_to_customer_domains(customer_obj_or_id) - Add this domain to customer's domains collection
  • remove_from_customer_domains(customer_obj_or_id) - Remove this domain from customer's domains collection
  • in_customer_domains?(customer_obj_or_id) - Check if this domain is in customer's domains collection
domain = Domain.new(name: "example.com")
customer = Customer.new(name: "Acme Corp")

# Explicit method calls
domain.add_to_customer_domains(customer)
domain.in_customer_domains?(customer)        # => true
domain.remove_from_customer_domains(customer)
domain.in_customer_domains?(customer)        # => false

Collection Operator Support

Ruby-like syntax with automatic bidirectional updates:

customer.domains << domain                    # Equivalent to domain.add_to_customer_domains(customer)
customer.domains.delete(domain.identifier)   # Removes relationship bidirectionally

Method Naming Pattern

{action}_{to|from}_{lowercase_class_name}_{collection_name} or in_{lowercase_class_name}_{collection_name}?

Class-Level Tracking Methods (class_participates_in)

Declaration

class Customer < Familia::Horreum
  feature :relationships
  class_participates_in :all_customers, score: :created_at
  class_participates_in :active_customers, score: ->(customer) {
    customer.status == 'active' ? customer.last_activity : 0
  }
end

Generated Class Methods

Collection access:

  • Customer.all_customers - Returns the sorted set collection directly
  • Customer.active_customers - Returns the active customers sorted set

Manual management (rarely needed):

  • Customer.add_to_all_customers(customer) - Manually add customer to tracking
  • Customer.remove_from_all_customers(customer) - Manually remove customer from tracking

Collection Operations

# Query operations (delegated to underlying sorted set)
Customer.all_customers.size                              # Count of tracked customers
Customer.all_customers.range(0, 9)                      # First 10 customers (by score)
Customer.all_customers.range_by_score(min_score, max_score)  # Customers in score range

# Score-based queries
recent_customers = Customer.all_customers.range_by_score(
  (Time.now - 1.week).to_i, '+inf'
)

# Get customer with scores
customers_with_scores = Customer.all_customers.range(0, -1, with_scores: true)
# => [["customer_id_1", 1634567890], ["customer_id_2", 1634567920], ...]

Automatic Behavior

  • Objects are automatically added to class-level tracking collections when saved
  • Objects are automatically removed when destroyed
  • Scores are automatically calculated using the provided field or lambda
  • No manual method calls required for basic lifecycle tracking

Scored Participation in Parent Collections

class User < Familia::Horreum
  feature :relationships
  participates_in Team, :active_users, score: :last_seen
  participates_in Team, :top_performers, score: ->(user) { user.performance_score }
end

class Team < Familia::Horreum
  feature :relationships
  sorted_set :active_users, :top_performers
end

Generated methods on Team instances:

  • team.active_users - Access the scored collection
  • team.top_performers - Access the performance-scored collection

Usage:

team = Team.new(name: "Development")
user = User.new(name: "Alice", last_seen: Time.now.to_i)

# User automatically added with score when relationship established
team.active_users << user

# Query by score ranges
recently_active = team.active_users.range_by_score(
  (Time.now - 1.hour).to_i, '+inf'
)

Indexing Methods (indexed_by)

The indexed_by declaration creates Valkey/Redis hash-based indexes for O(1) field lookups with automatic management.

Class-Level Indexing (class_indexed_by)

class Customer < Familia::Horreum
  feature :relationships
  field :email, :username, :api_key

  class_indexed_by :email, :email_lookup
  class_indexed_by :username, :username_lookup
  class_indexed_by :api_key, :api_key_lookup
end

Generated Class Methods

Index access:

  • Customer.email_lookup - Returns the hash index directly
  • Customer.username_lookup - Returns the username hash index
  • Customer.api_key_lookup - Returns the API key hash index

Convenience lookup methods:

  • Customer.find_by_email(email) - Find customer ID by email (O(1) lookup)
  • Customer.find_by_username(username) - Find customer ID by username
  • Customer.find_by_api_key(api_key) - Find customer ID by API key

Generated Instance Methods (rarely used manually)

Index management:

  • customer.add_to_class_email_lookup - Manually add to email index
  • customer.remove_from_class_email_lookup - Manually remove from email index
  • customer.update_class_email_lookup(old_email) - Update index when email changes

Usage Examples

# Automatic indexing on save
customer = Customer.new(
  email: "alice@example.com",
  username: "alice123",
  api_key: "ak_abcd1234"
)
customer.save  # Automatically added to all indexes

# O(1) lookups
customer_id = Customer.find_by_email("alice@example.com")
customer = Customer.load(customer_id) if customer_id

# Direct index access
all_emails = Customer.email_lookup.to_h
# => {"alice@example.com" => "customer_1", "bob@example.com" => "customer_2"}

# Index operations
Customer.email_lookup.get("alice@example.com")  # => "customer_1"
Customer.email_lookup.set("new@example.com", "customer_3")

Automatic Behavior

  • Objects are automatically indexed when saved
  • Indexes are automatically updated when indexed fields change
  • Objects are automatically removed from indexes when destroyed
  • No manual index management required for standard lifecycle operations

Redis key pattern: {class_name.downcase}:{index_name}

Relationship-Scoped Indexing (indexed_by with target:)

class Customer < Familia::Horreum
  feature :relationships
  field :name
  set :domains
end

class Domain < Familia::Horreum
  feature :relationships
  field :name, :subdomain, :port
  participates_in Customer, :domains

  # Index domains by name within each customer (domains can have same name across customers)
  indexed_by :name, :domain_index, target: Customer
  indexed_by :subdomain, :subdomain_index, target: Customer
  indexed_by :port, :port_index, target: Customer
end

Generated Methods on Target Class (Customer)

Single lookups:

  • customer.find_by_name(domain_name) - Find domain ID by name within this customer
  • customer.find_by_subdomain(subdomain) - Find domain ID by subdomain within this customer
  • customer.find_by_port(port) - Find domain ID by port within this customer

Multiple lookups:

  • customer.find_all_by_name(domain_names) - Find multiple domain IDs by names
  • customer.find_all_by_subdomain(subdomains) - Find multiple domain IDs by subdomains
  • customer.find_all_by_port(ports) - Find multiple domain IDs by ports

Direct index access:

  • customer.domain_index - Access the name index hash directly
  • customer.subdomain_index - Access the subdomain index hash directly
  • customer.port_index - Access the port index hash directly

Usage Examples

customer = Customer.new(name: "Acme Corp")
domain1 = Domain.new(name: "example.com", subdomain: "www", port: 443)
domain2 = Domain.new(name: "api.example.com", subdomain: "api", port: 443)

# Establish relationships (automatic indexing)
customer.domains << domain1
customer.domains << domain2

# O(1) lookups within customer scope
domain_id = customer.find_by_name("example.com")
api_domain_id = customer.find_by_subdomain("api")

# Multiple lookups
ssl_domains = customer.find_all_by_port([443, 8443])

# Direct index access
all_domain_names = customer.domain_index.to_h
# => {"example.com" => "domain_1", "api.example.com" => "domain_2"}

# Check if customer has domain with specific name
has_domain = customer.domain_index.get("example.com").present?

Generated Methods on Indexed Class (Domain)

Index management (automatic):

  • domain.add_to_customer_domain_index(customer) - Add to customer's domain name index
  • domain.remove_from_customer_domain_index(customer) - Remove from customer's domain name index
  • domain.update_customer_domain_index(customer, old_name) - Update index when name changes

Automatic Behavior

  • Domain is automatically indexed when added to customer's domains collection
  • Index is automatically updated when domain's indexed fields change
  • Domain is automatically removed from index when relationship is removed
  • All index management happens transparently during relationship operations

Redis key pattern: {target_class.downcase}:{target_id}:{index_name}

When to Use Each Indexing Context

Class-level indexing (class_indexed_by):

  • Use for system-wide unique field lookups
  • Examples: email addresses, usernames, API keys, social security numbers
  • Best when field values should be unique across all instances

Relationship-scoped indexing (indexed_by with target:):

  • Use for context-specific field lookups
  • Examples: domain names per customer, project names per team, usernames per organization
  • Best when field values are unique within a specific parent context but may duplicate across different parents

Complete API Reference

Method Naming Conventions

Participation methods:

  • {add_to|remove_from}_{target_class_downcase}_{collection_name}(target)
  • in_{target_class_downcase}_{collection_name}?(target)

Class-level tracking:

  • {add_to|remove_from}_{collection_name}(object) (class methods)
  • {ClassName}.{collection_name} (collection accessor)

Class-level indexing:

  • {add_to|remove_from}_class_{index_name} (instance methods)
  • {ClassName}.{index_name} (index accessor)
  • {ClassName}.find_by_{field_name}(value) (convenience lookup)

Relationship-scoped indexing:

  • {target_instance}.find_by_{field_name}(value) (single lookup)
  • {target_instance}.find_all_by_{field_name}(values) (multiple lookup)
  • {target_instance}.{index_name} (direct index access)

Key Benefits of the Relationships API

  • Automatic Management: Save operations update indexes and tracking automatically
  • Ruby-Idiomatic: Use << operator for natural collection syntax
  • Consistent Storage: All indexes stored at class level for architectural simplicity
  • Predictable Naming: Method names follow consistent patterns for easy discovery
  • O(1) Performance: Hash-based indexes provide constant-time lookups
  • Bidirectional Sync: Relationship changes automatically update both sides

Advanced Implementation Patterns

Error Handling and Edge Cases

Handling Missing Objects:

# Safe relationship operations
begin
  customer.domains << domain
rescue Familia::Problem => e
  Rails.logger.error "Failed to establish relationship: #{e.message}"
end

# Check if objects exist before relating
if domain.persisted? && customer.persisted?
  customer.domains << domain
end

# Batch operations with error handling
domain_ids.each do |id|
  next unless Domain.exists?(id)  # Custom existence check
  customer.domains.add(id)
end

Index Consistency Validation:

class Customer < Familia::Horreum
  feature :relationships

  def validate_email_index_consistency
    stored_id = self.class.email_lookup.get(email)
    return true if stored_id == identifier

    Rails.logger.warn "Email index inconsistency: #{email} -> #{stored_id} vs #{identifier}"
    # Repair the index
    self.class.email_lookup.set(email, identifier)
    false
  end

  after_save :validate_email_index_consistency
end

Performance Optimization Techniques

Lazy Loading Patterns:

class Customer < Familia::Horreum
  feature :relationships
  set :domains

  # Memoized relationship loading
  def domain_objects
    @domain_objects ||= begin
      domain_ids = domains.to_a
      Domain.multiget(*domain_ids).compact
    end
  end

  # Paginated relationship loading
  def recent_domains(limit = 10)
    recent_ids = domains.range(0, limit - 1)
    Domain.multiget(*recent_ids).compact
  end

  # Selective field loading
  def domain_names
    domains.to_a.map do |id|
      Domain.new(domain_id: id).name  # Load only name field
    end
  end
end

Bulk Operations with Transaction Safety:

class Team < Familia::Horreum
  feature :relationships
  set :members

  def bulk_add_members(user_ids)
    # Validate all IDs exist first
    valid_ids = user_ids.select { |id| User.exists?(id) }

    # Use Valkey/Redis pipeline for bulk operations
    Familia.redis.pipelined do |pipeline|
      valid_ids.each do |user_id|
        members.add(user_id)
        # Update reverse indexes in same pipeline
        user = User.new(user_id: user_id)
        user.add_to_team_members(self)
      end
    end

    valid_ids.size
  end

  def bulk_remove_members(user_ids)
    # Atomic bulk removal
    removed_count = 0
    user_ids.each do |user_id|
      if members.delete(user_id)
        removed_count += 1
        # Clean up reverse relationship
        user = User.new(user_id: user_id)
        user.remove_from_team_members(self)
      end
    end
    removed_count
  end
end

Custom Scoring and Complex Relationships

Dynamic Scoring with Context:

class Project < Familia::Horreum
  feature :relationships
  field :priority, :created_at, :deadline

  participates_in Team, :projects, score: :calculated_priority

  private

  def calculated_priority
    base_priority = priority.to_i
    urgency_multiplier = deadline_urgency_factor
    age_factor = project_age_factor

    (base_priority * urgency_multiplier * age_factor).to_i
  end

  def deadline_urgency_factor
    return 1.0 unless deadline

    days_until_deadline = (deadline.to_time - Time.now) / 1.day
    return 3.0 if days_until_deadline <= 1  # Critical
    return 2.0 if days_until_deadline <= 7  # High
    1.0  # Normal
  end

  def project_age_factor
    days_old = (Time.now - created_at.to_time) / 1.day
    [1.0 + (days_old / 30.0), 2.0].min  # Cap at 2x multiplier
  end
end

Multi-Level Relationships:

class Organization < Familia::Horreum
  feature :relationships
  set :departments

  # Find all users across all departments
  def all_users
    department_ids = departments.to_a
    user_ids = []

    department_ids.each do |dept_id|
      dept = Department.load(dept_id)
      user_ids.concat(dept.users.to_a) if dept
    end

    User.multiget(*user_ids.uniq).compact
  end

  # Hierarchical relationship queries
  def users_in_department(department_name)
    dept_id = find_by_name(department_name)
    return [] unless dept_id

    dept = Department.load(dept_id)
    return [] unless dept

    user_ids = dept.users.to_a
    User.multiget(*user_ids).compact
  end
end

Advanced Indexing Patterns

Composite Index Keys:

class ApiKey < Familia::Horreum
  feature :relationships
  field :customer_id, :environment, :key_type, :key_hash

  # Composite index for environment + type lookups
  indexed_by :environment_and_type, :env_type_lookup, target: Customer

  private

  def environment_and_type
    "#{environment}:#{key_type}"  # e.g., "production:read_write"
  end
end

# Usage
customer.find_by_environment_and_type("production:read_only")
customer.find_all_by_environment_and_type(["staging:read_write", "production:read_write"])

Time-Based Index Partitioning:

class Event < Familia::Horreum
  feature :relationships
  field :event_type, :timestamp, :user_id

  participates_in User, :events, score: :timestamp
  indexed_by :daily_partition, :daily_events, target: User

  private

  def daily_partition
    Time.at(timestamp).strftime('%Y%m%d')  # e.g., "20241215"
  end
end

# Usage - find today's events for user
today = Time.now.strftime('%Y%m%d')
todays_event_ids = user.find_all_by_daily_partition([today])

Testing Relationship Methods

Unit Testing Patterns:

# test/models/relationship_test.rb
class RelationshipTest < Minitest::Test
  def setup
    @customer = Customer.create(email: "test@example.com", name: "Test Corp")
    @domain = Domain.create(name: "test.com", status: "active")
  end

  def test_bidirectional_relationship_establishment
    @customer.domains << @domain

    # Test both sides of relationship
    assert @customer.domains.member?(@domain.identifier)
    assert @domain.in_customer_domains?(@customer.identifier)
  end

  def test_relationship_removal
    @customer.domains << @domain
    @customer.domains.delete(@domain.identifier)

    # Verify complete cleanup
    refute @customer.domains.member?(@domain.identifier)
    refute @domain.in_customer_domains?(@customer.identifier)
  end

  def test_index_automatic_maintenance
    @customer.save  # Should trigger indexing

    found_id = Customer.find_by_email("test@example.com")
    assert_equal @customer.identifier, found_id
  end

  def test_scoped_index_lookup
    @customer.domains << @domain

    found_id = @customer.find_by_name("test.com")
    assert_equal @domain.identifier, found_id
  end

  def test_scored_relationship_ordering
    project = Project.create(name: "Test Project")
    task1 = Task.create(title: "Low priority", priority: 1)
    task2 = Task.create(title: "High priority", priority: 10)

    project.tasks << task1
    project.tasks << task2

    # Should be ordered by priority (high to low)
    task_ids = project.tasks.range(0, -1, order: 'DESC')
    assert_equal task2.identifier, task_ids.first
    assert_equal task1.identifier, task_ids.last
  end
end

Complete Production Example

# Real-world e-commerce system
class Customer < Familia::Horreum
  feature :relationships
  field :email, :username, :tier, :created_at, :last_activity

  # Global unique lookups
  class_indexed_by :email, :email_lookup
  class_indexed_by :username, :username_lookup

  # Tiered customer tracking
  class_participates_in :all_customers, score: :created_at
  class_participates_in :active_customers, score: :last_activity
  class_participates_in :premium_customers,
    score: ->(c) { c.tier == 'premium' ? c.last_activity : 0 }

  # Relationships
  set :orders, :addresses, :payment_methods

  def recent_orders(limit = 10)
    order_ids = orders.range(0, limit - 1)
    Order.multiget(*order_ids).compact
  end

  def total_spent
    recent_orders(100).sum(&:total_amount)
  end
end

class Order < Familia::Horreum
  feature :relationships
  field :total_amount, :status, :created_at, :order_number

  participates_in Customer, :orders, score: :created_at
  indexed_by :order_number, :order_lookup, target: Customer
  indexed_by :status, :status_lookup, target: Customer

  set :line_items
end

class Address < Familia::Horreum
  feature :relationships
  field :street, :city, :state, :zip_code, :address_type

  participates_in Customer, :addresses
  indexed_by :address_type, :address_type_lookup, target: Customer
end

# Usage in production
customer = Customer.create(
  email: "alice@example.com",
  username: "alice_smith",
  tier: "premium"
)

# Automatic indexing and tracking
Customer.find_by_email("alice@example.com")  # => customer.identifier
Customer.premium_customers.range(0, 9)       # Recent premium customers

# Complex relationship queries
order = Order.create(order_number: "ORD-12345", status: "shipped")
customer.orders << order

customer.find_by_order_number("ORD-12345")   # => order.identifier
shipped_orders = customer.find_all_by_status(["shipped", "delivered"])

See Also