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 collectionremove_from_customer_domains(customer_obj_or_id)- Remove this domain from customer's domains collectionin_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 directlyCustomer.active_customers- Returns the active customers sorted set
Manual management (rarely needed):
Customer.add_to_all_customers(customer)- Manually add customer to trackingCustomer.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 collectionteam.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 directlyCustomer.username_lookup- Returns the username hash indexCustomer.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 usernameCustomer.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 indexcustomer.remove_from_class_email_lookup- Manually remove from email indexcustomer.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 customercustomer.find_by_subdomain(subdomain)- Find domain ID by subdomain within this customercustomer.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 namescustomer.find_all_by_subdomain(subdomains)- Find multiple domain IDs by subdomainscustomer.find_all_by_port(ports)- Find multiple domain IDs by ports
Direct index access:
customer.domain_index- Access the name index hash directlycustomer.subdomain_index- Access the subdomain index hash directlycustomer.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 indexdomain.remove_from_customer_domain_index(customer)- Remove from customer's domain name indexdomain.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.}"
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().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
- Relationships Feature Guide - Conceptual introduction to relationships
- Technical Reference - Advanced implementation patterns
- Feature System Guide - Understanding Familia's feature architecture