Migrating Guide: v2.0.0-pre22
This version introduces significant performance optimizations for Redis operations, completes the bidirectional relationships feature, and improves flexibility for external identifiers.
Major Features
Bidirectional Relationship Methods
What's New:
The participates_in declarations now generate reverse collection methods with the _instances suffix, providing symmetric access to relationships from both directions.
Generated Methods:
class User < Familia::Horreum
participates_in Team, :members
participates_in Organization, :employees
end
# New reverse collection methods:
user.team_instances # => [team1, team2]
user.team_ids # => ["team_123", "team_456"]
user.team? # => true/false
user.team_count # => 2
user.organization_instances # => [org1]
user.organization_ids # => ["org_789"]
Custom Names:
class User < Familia::Horreum
participates_in Organization, :contractors, as: :clients
end
user.clients_instances # Instead of organization_instances
user.clients_ids # Instead of organization_ids
Migration:
No changes required for existing code. The new methods are additive and don't affect existing participates_in functionality.
Pipelined Bulk Loading
What's New:
New load_multi methods provide up to 2× performance improvement for bulk object loading by using Redis pipelining.
Before (N×2 commands):
users = ids.map { |id| User.find_by_id(id) }
# For 14 objects: 28 Redis commands (14 EXISTS + 14 HGETALL)
After (1 round trip):
users = User.load_multi(ids)
# For 14 objects: 1 pipelined batch with 14 HGETALL commands
Additional Methods:
# Load by full dbkeys
users = User.load_multi_by_keys(['user:123:object', 'user:456:object'])
# Filter out nils for missing objects
existing_only = User.load_multi(ids).compact
Optional EXISTS Check Optimization
What's New:
The find_by_id and related methods now support skipping the EXISTS check for 50% reduction in Redis commands.
# Default behavior (unchanged, 2 commands)
user = User.find_by_id(123)
# Optimized mode (1 command)
user = User.find_by_id(123, check_exists: false)
When to Use:
- Performance-critical paths
- Bulk operations with known-to-exist keys
- High-throughput APIs
- Loading from sorted set results
Enhanced Features
Flexible External Identifier Format
What's New:
The external_identifier feature now supports custom format templates.
Examples:
# Default format (unchanged)
class User < Familia::Horreum
feature :external_identifier
end
user.extid # => "ext_abc123def456"
# Custom prefix
class Customer < Familia::Horreum
feature :external_identifier, format: 'cust_%{id}'
end
customer.extid # => "cust_abc123def456"
# Different separator
class APIKey < Familia::Horreum
feature :external_identifier, format: 'api-%{id}'
end
key.extid # => "api-abc123def456"
Atomic Index Rebuilding
What's New:
Auto-generated rebuild methods for all unique and multi indexes with zero downtime.
Examples:
# Class-level unique index
User.rebuild_email_lookup
# Instance-scoped unique index
company.rebuild_badge_index
# With progress tracking
User.rebuild_email_lookup(batch_size: 100) do |progress|
puts "#{progress[:completed]}/#{progress[:total]}"
end
When to Use:
- After data migrations or bulk imports
- Recovering from index corruption
- Adding indexes to existing data
Migration:
Run rebuild methods once after upgrade to ensure index consistency. No code changes required—methods are auto-generated from existing unique_index and multi_index declarations.
Bug Fixes
Symbol/String Target Classes in participates_in
What Was Fixed:
Fixed multiple bugs when using Symbol or String class names in participates_in:
class Domain < Familia::Horreum
# All forms now work correctly:
participates_in Customer, :domains # Class object
participates_in :Customer, :domains # Symbol (was broken)
participates_in 'Customer', :domains # String (was broken)
end
Errors Fixed:
NoMethodError: private method 'member_by_config_name'NoMethodError: undefined method 'familia_name' for SymbolNoMethodError: undefined method 'config_name' for Symbol- Confusing nil errors for unloaded classes
New Behavior:
When a target class can't be resolved, you now get a helpful error:
Target class 'Customer' could not be resolved.
Possible causes:
1. The class hasn't been loaded yet (load order issue)
2. The class name is misspelled
3. The class doesn't inherit from Familia::Horreum
Registered Familia classes: ["User", "Team", "Organization"]
Performance Recommendations
Use Bulk Loading for Collections
# ❌ Avoid N+1 queries
team.members.to_a.map { |id| User.find_by_id(id) }
# ✅ Use bulk loading
User.load_multi(team.members.to_a)
Skip EXISTS Checks When Safe
# When loading from sorted sets (keys guaranteed to exist)
task_ids = project.tasks.range(0, 9)
tasks = Task.load_multi(task_ids) # Or use check_exists: false
# For known-existing keys
user = User.find_by_id(session[:user_id], check_exists: false)
Leverage Reverse Collection Methods
# ❌ Manual parsing of participations
team_keys = user.participations.members.select { |k| k.start_with?("team:") }
team_ids = team_keys.map { |k| k.split(':')[1] }
teams = Team.load_multi(team_ids)
# ✅ Use generated methods
teams = user.team_instances
Backwards Compatibility
All changes in this version are backwards compatible:
- New methods are additive and don't affect existing APIs
- Default behaviors remain unchanged
- Symbol/String fixes don't require code changes
Recommended Actions
- Adopt bulk loading for performance-critical paths
- Use reverse collection methods to simplify relationship queries
- Consider check_exists: false for guaranteed-existing keys
- Update external_identifier formats if custom prefixes are needed