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 Symbol
  • NoMethodError: 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
  1. Adopt bulk loading for performance-critical paths
  2. Use reverse collection methods to simplify relationship queries
  3. Consider check_exists: false for guaranteed-existing keys
  4. Update external_identifier formats if custom prefixes are needed

See Also