familia 2.0.0.pre15 → 2.0.0.pre17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +2 -2
- data/.github/workflows/code-quality.yml +138 -0
- data/.github/workflows/code-smells.yml +85 -0
- data/.github/workflows/docs.yml +31 -8
- data/.gitignore +3 -1
- data/.pre-commit-config.yaml +7 -1
- data/.reek.yml +98 -0
- data/.rubocop.yml +54 -10
- data/.talismanrc +9 -0
- data/.yardopts +18 -13
- data/CHANGELOG.rst +86 -4
- data/CLAUDE.md +39 -1
- data/Gemfile +6 -5
- data/Gemfile.lock +99 -23
- data/LICENSE.txt +1 -1
- data/README.md +285 -85
- data/changelog.d/README.md +2 -2
- data/docs/archive/FAMILIA_RELATIONSHIPS.md +22 -22
- data/docs/archive/FAMILIA_TECHNICAL.md +42 -42
- data/docs/archive/FAMILIA_UPDATE.md +3 -3
- data/docs/archive/README.md +3 -2
- data/docs/{guides/API-Reference.md → archive/api-reference.md} +87 -101
- data/docs/conf.py +29 -0
- data/docs/guides/{Field-System-Guide.md → core-field-system.md} +9 -9
- data/docs/guides/feature-encrypted-fields.md +785 -0
- data/docs/guides/{Expiration-Feature-Guide.md → feature-expiration.md} +11 -2
- data/docs/guides/feature-external-identifiers.md +637 -0
- data/docs/guides/feature-object-identifiers.md +435 -0
- data/docs/guides/{Quantization-Feature-Guide.md → feature-quantization.md} +94 -29
- data/docs/guides/feature-relationships-methods.md +684 -0
- data/docs/guides/feature-relationships.md +200 -0
- data/docs/guides/{Features-System-Developer-Guide.md → feature-system-devs.md} +4 -4
- data/docs/guides/{Feature-System-Guide.md → feature-system.md} +5 -5
- data/docs/guides/{Transient-Fields-Guide.md → feature-transient-fields.md} +2 -2
- data/docs/guides/{Implementation-Guide.md → implementation.md} +3 -3
- data/docs/guides/index.md +176 -0
- data/docs/guides/{Security-Model.md → security-model.md} +1 -1
- data/docs/migrating/v2.0.0-pre.md +1 -1
- data/docs/migrating/v2.0.0-pre11.md +2 -2
- data/docs/migrating/v2.0.0-pre12.md +2 -2
- data/docs/migrating/v2.0.0-pre5.md +33 -12
- data/docs/migrating/v2.0.0-pre6.md +2 -2
- data/docs/migrating/v2.0.0-pre7.md +8 -8
- data/docs/overview.md +624 -20
- data/docs/reference/api-technical.md +1365 -0
- data/examples/autoloader/mega_customer/features/deprecated_fields.rb +7 -0
- data/examples/autoloader/mega_customer/safe_dump_fields.rb +1 -1
- data/examples/autoloader/mega_customer.rb +3 -1
- data/examples/encrypted_fields.rb +378 -0
- data/examples/json_usage_patterns.rb +144 -0
- data/examples/relationships.rb +13 -13
- data/examples/safe_dump.rb +7 -7
- data/examples/single_connection_transaction_confusions.rb +379 -0
- data/lib/familia/base.rb +51 -10
- data/lib/familia/connection/handlers.rb +223 -0
- data/lib/familia/connection/individual_command_proxy.rb +64 -0
- data/lib/familia/connection/middleware.rb +75 -0
- data/lib/familia/connection/operation_core.rb +93 -0
- data/lib/familia/connection/operations.rb +277 -0
- data/lib/familia/connection/pipeline_core.rb +87 -0
- data/lib/familia/connection/transaction_core.rb +100 -0
- data/lib/familia/connection.rb +60 -186
- data/lib/familia/data_type/class_methods.rb +63 -0
- data/lib/familia/data_type/commands.rb +53 -51
- data/lib/familia/data_type/connection.rb +83 -0
- data/lib/familia/data_type/serialization.rb +108 -107
- data/lib/familia/data_type/settings.rb +96 -0
- data/lib/familia/data_type/types/counter.rb +1 -1
- data/lib/familia/data_type/types/hashkey.rb +15 -11
- data/lib/familia/data_type/types/{list.rb → listkey.rb} +13 -5
- data/lib/familia/data_type/types/lock.rb +3 -2
- data/lib/familia/data_type/types/sorted_set.rb +128 -14
- data/lib/familia/data_type/types/{string.rb → stringkey.rb} +7 -9
- data/lib/familia/data_type/types/unsorted_set.rb +20 -27
- data/lib/familia/data_type.rb +12 -171
- data/lib/familia/distinguisher.rb +85 -0
- data/lib/familia/encryption/encrypted_data.rb +15 -24
- data/lib/familia/encryption/manager.rb +6 -4
- data/lib/familia/encryption/providers/aes_gcm_provider.rb +1 -1
- data/lib/familia/encryption/providers/secure_xchacha20_poly1305_provider.rb +7 -9
- data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +4 -5
- data/lib/familia/encryption/request_cache.rb +7 -7
- data/lib/familia/encryption.rb +2 -3
- data/lib/familia/errors.rb +9 -3
- data/lib/familia/features/autoloader.rb +30 -12
- data/lib/familia/features/encrypted_fields/concealed_string.rb +3 -4
- data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +13 -14
- data/lib/familia/features/encrypted_fields.rb +71 -66
- data/lib/familia/features/expiration/extensions.rb +1 -1
- data/lib/familia/features/expiration.rb +31 -26
- data/lib/familia/features/external_identifier.rb +57 -19
- data/lib/familia/features/object_identifier.rb +134 -25
- data/lib/familia/features/quantization.rb +16 -21
- data/lib/familia/features/relationships/README.md +97 -0
- data/lib/familia/features/relationships/collection_operations.rb +104 -0
- data/lib/familia/features/relationships/indexing/multi_index_generators.rb +202 -0
- data/lib/familia/features/relationships/indexing/unique_index_generators.rb +306 -0
- data/lib/familia/features/relationships/indexing.rb +182 -256
- data/lib/familia/features/relationships/indexing_relationship.rb +35 -0
- data/lib/familia/features/relationships/participation/participant_methods.rb +164 -0
- data/lib/familia/features/relationships/participation/target_methods.rb +225 -0
- data/lib/familia/features/relationships/participation.rb +656 -0
- data/lib/familia/features/relationships/participation_relationship.rb +31 -0
- data/lib/familia/features/relationships/score_encoding.rb +20 -20
- data/lib/familia/features/relationships.rb +65 -266
- data/lib/familia/features/safe_dump.rb +127 -130
- data/lib/familia/features/transient_fields/redacted_string.rb +6 -6
- data/lib/familia/features/transient_fields/transient_field_type.rb +5 -5
- data/lib/familia/features/transient_fields.rb +10 -7
- data/lib/familia/features.rb +10 -14
- data/lib/familia/field_type.rb +6 -4
- data/lib/familia/horreum/connection.rb +297 -0
- data/lib/familia/horreum/{core/database_commands.rb → database_commands.rb} +27 -17
- data/lib/familia/horreum/{subclass/definition.rb → definition.rb} +139 -74
- data/lib/familia/horreum/{subclass/management.rb → management.rb} +73 -27
- data/lib/familia/horreum/{core/serialization.rb → persistence.rb} +108 -185
- data/lib/familia/horreum/{subclass/related_fields_management.rb → related_fields.rb} +104 -23
- data/lib/familia/horreum/serialization.rb +172 -0
- data/lib/familia/horreum/{shared/settings.rb → settings.rb} +2 -1
- data/lib/familia/horreum/{core/utils.rb → utils.rb} +2 -1
- data/lib/familia/horreum.rb +222 -119
- data/lib/familia/json_serializer.rb +0 -1
- data/lib/familia/logging.rb +11 -114
- data/lib/familia/refinements/dear_json.rb +122 -0
- data/lib/familia/refinements/logger_trace.rb +20 -17
- data/lib/familia/refinements/stylize_words.rb +65 -0
- data/lib/familia/refinements/time_literals.rb +60 -52
- data/lib/familia/refinements.rb +2 -1
- data/lib/familia/secure_identifier.rb +60 -28
- data/lib/familia/settings.rb +83 -7
- data/lib/familia/utils.rb +5 -87
- data/lib/familia/verifiable_identifier.rb +4 -4
- data/lib/familia/version.rb +1 -1
- data/lib/familia.rb +72 -14
- data/lib/middleware/database_middleware.rb +56 -14
- data/lib/{familia/multi_result.rb → multi_result.rb} +23 -16
- data/try/configuration/scenarios_try.rb +2 -2
- data/try/connection/fiber_context_preservation_try.rb +250 -0
- data/try/connection/handler_constraints_try.rb +59 -0
- data/try/connection/operation_mode_guards_try.rb +208 -0
- data/try/connection/pipeline_fallback_integration_try.rb +128 -0
- data/try/connection/responsibility_chain_tracking_try.rb +72 -0
- data/try/connection/transaction_fallback_integration_try.rb +288 -0
- data/try/connection/transaction_mode_permissive_try.rb +153 -0
- data/try/connection/transaction_mode_strict_try.rb +98 -0
- data/try/connection/transaction_mode_warn_try.rb +131 -0
- data/try/connection/transaction_modes_try.rb +249 -0
- data/try/core/autoloader_try.rb +120 -2
- data/try/core/connection_try.rb +10 -10
- data/try/core/conventional_inheritance_try.rb +130 -0
- data/try/core/create_method_try.rb +15 -23
- data/try/core/database_consistency_try.rb +11 -10
- data/try/core/errors_try.rb +11 -14
- data/try/core/familia_extended_try.rb +2 -2
- data/try/core/familia_members_methods_try.rb +76 -0
- data/try/core/familia_try.rb +1 -1
- data/try/core/isolated_dbclient_try.rb +165 -0
- data/try/core/middleware_try.rb +16 -16
- data/try/core/persistence_operations_try.rb +4 -4
- data/try/core/pools_try.rb +42 -26
- data/try/core/secure_identifier_try.rb +28 -24
- data/try/core/time_utils_try.rb +10 -10
- data/try/core/tools_try.rb +3 -3
- data/try/core/utils_try.rb +2 -2
- data/try/data_types/boolean_try.rb +4 -4
- data/try/data_types/datatype_base_try.rb +0 -2
- data/try/data_types/list_try.rb +10 -10
- data/try/data_types/sorted_set_try.rb +5 -5
- data/try/data_types/sorted_set_zadd_options_try.rb +625 -0
- data/try/data_types/string_try.rb +12 -12
- data/try/data_types/unsortedset_try.rb +33 -0
- data/try/debugging/cache_behavior_tracer.rb +7 -7
- data/try/debugging/debug_aad_process.rb +1 -1
- data/try/debugging/debug_concealed_internal.rb +1 -1
- data/try/debugging/debug_cross_context.rb +1 -1
- data/try/debugging/debug_fresh_cross_context.rb +1 -1
- data/try/debugging/encryption_method_tracer.rb +10 -10
- data/try/edge_cases/hash_symbolization_try.rb +1 -1
- data/try/edge_cases/ttl_side_effects_try.rb +1 -1
- data/try/encryption/config_persistence_try.rb +2 -2
- data/try/encryption/encryption_core_try.rb +19 -19
- data/try/encryption/instance_variable_scope_try.rb +1 -1
- data/try/encryption/module_loading_try.rb +2 -2
- data/try/encryption/providers/aes_gcm_provider_try.rb +1 -1
- data/try/encryption/providers/xchacha20_poly1305_provider_try.rb +1 -1
- data/try/encryption/secure_memory_handling_try.rb +1 -1
- data/try/features/encrypted_fields/concealed_string_core_try.rb +11 -7
- data/try/features/encrypted_fields/encrypted_fields_core_try.rb +1 -1
- data/try/features/encrypted_fields/encrypted_fields_integration_try.rb +3 -3
- data/try/features/encrypted_fields/encrypted_fields_no_cache_security_try.rb +10 -10
- data/try/features/encrypted_fields/encrypted_fields_security_try.rb +14 -14
- data/try/features/encrypted_fields/error_conditions_try.rb +7 -7
- data/try/features/encrypted_fields/fresh_key_try.rb +1 -1
- data/try/features/encrypted_fields/nonce_uniqueness_try.rb +1 -1
- data/try/features/encrypted_fields/secure_by_default_behavior_try.rb +7 -7
- data/try/features/encrypted_fields/universal_serialization_safety_try.rb +13 -20
- data/try/features/external_identifier/external_identifier_try.rb +1 -1
- data/try/features/feature_dependencies_try.rb +3 -3
- data/try/features/field_groups_try.rb +244 -0
- data/try/features/object_identifier/object_identifier_integration_try.rb +28 -34
- data/try/features/object_identifier/object_identifier_try.rb +10 -0
- data/try/features/quantization/quantization_try.rb +1 -1
- data/try/features/relationships/indexing_commands_verification_try.rb +136 -0
- data/try/features/relationships/indexing_try.rb +443 -0
- data/try/features/relationships/participation_commands_verification_spec.rb +102 -0
- data/try/features/relationships/participation_commands_verification_try.rb +105 -0
- data/try/features/relationships/participation_performance_improvements_try.rb +124 -0
- data/try/features/relationships/participation_reverse_index_try.rb +196 -0
- data/try/features/relationships/relationships_api_changes_try.rb +72 -71
- data/try/features/relationships/relationships_edge_cases_try.rb +15 -18
- data/try/features/relationships/relationships_performance_minimal_try.rb +2 -2
- data/try/features/relationships/relationships_performance_simple_try.rb +8 -8
- data/try/features/relationships/relationships_performance_try.rb +20 -20
- data/try/features/relationships/relationships_try.rb +27 -38
- data/try/features/safe_dump/safe_dump_advanced_try.rb +2 -2
- data/try/features/transient_fields/refresh_reset_try.rb +3 -1
- data/try/features/transient_fields/simple_refresh_test.rb +1 -1
- data/try/helpers/test_cleanup.rb +86 -0
- data/try/helpers/test_helpers.rb +6 -7
- data/try/horreum/auto_indexing_on_save_try.rb +212 -0
- data/try/horreum/base_try.rb +3 -2
- data/try/horreum/commands_try.rb +3 -1
- data/try/horreum/defensive_initialization_try.rb +86 -0
- data/try/horreum/destroy_related_fields_cleanup_try.rb +332 -0
- data/try/horreum/initialization_try.rb +11 -7
- data/try/horreum/relations_try.rb +21 -13
- data/try/horreum/serialization_try.rb +12 -11
- data/try/horreum/settings_try.rb +2 -0
- data/try/integration/cross_component_try.rb +3 -3
- data/try/memory/memory_basic_test.rb +1 -1
- data/try/memory/memory_docker_ruby_dump.sh +2 -2
- data/try/models/customer_safe_dump_try.rb +1 -1
- data/try/models/customer_try.rb +13 -15
- data/try/models/datatype_base_try.rb +3 -3
- data/try/models/familia_object_try.rb +9 -8
- data/try/performance/benchmarks_try.rb +2 -2
- data/try/prototypes/atomic_saves_v1_context_proxy.rb +2 -2
- data/try/prototypes/atomic_saves_v3_connection_pool.rb +3 -3
- data/try/prototypes/atomic_saves_v4.rb +1 -1
- data/try/prototypes/lib/atomic_saves_v2_connection_switching_helpers.rb +4 -4
- data/try/prototypes/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/atomic_saves_v3_connection_pool_helpers.rb +4 -4
- data/try/prototypes/pooling/lib/connection_pool_metrics.rb +5 -5
- data/try/prototypes/pooling/lib/connection_pool_stress_test.rb +26 -26
- data/try/prototypes/pooling/lib/connection_pool_threading_models.rb +7 -7
- data/try/prototypes/pooling/lib/visualize_stress_results.rb +1 -1
- data/try/prototypes/pooling/pool_siege.rb +11 -11
- data/try/prototypes/pooling/run_stress_tests.rb +7 -7
- data/try/refinements/dear_json_array_methods_try.rb +53 -0
- data/try/refinements/dear_json_hash_methods_try.rb +54 -0
- data/try/refinements/logger_trace_methods_try.rb +44 -0
- data/try/refinements/time_literals_numeric_methods_try.rb +141 -0
- data/try/refinements/time_literals_string_methods_try.rb +80 -0
- data/try/valkey.conf +26 -0
- metadata +92 -52
- data/.rubocop_todo.yml +0 -208
- data/docs/connection_pooling.md +0 -192
- data/docs/guides/Connection-Pooling-Guide.md +0 -437
- data/docs/guides/Encrypted-Fields-Overview.md +0 -101
- data/docs/guides/Feature-System-Autoloading.md +0 -198
- data/docs/guides/Home.md +0 -116
- data/docs/guides/Relationships-Guide.md +0 -737
- data/docs/guides/relationships-methods.md +0 -266
- data/docs/reference/auditing_database_commands.rb +0 -228
- data/examples/permissions.rb +0 -240
- data/lib/familia/features/relationships/cascading.rb +0 -437
- data/lib/familia/features/relationships/membership.rb +0 -497
- data/lib/familia/features/relationships/permission_management.rb +0 -264
- data/lib/familia/features/relationships/querying.rb +0 -615
- data/lib/familia/features/relationships/redis_operations.rb +0 -274
- data/lib/familia/features/relationships/tracking.rb +0 -418
- data/lib/familia/horreum/core/connection.rb +0 -73
- data/lib/familia/horreum/core.rb +0 -21
- data/lib/familia/refinements/snake_case.rb +0 -40
- data/lib/familia/validation/command_recorder.rb +0 -336
- data/lib/familia/validation/expectations.rb +0 -519
- data/lib/familia/validation/validation_helpers.rb +0 -443
- data/lib/familia/validation/validator.rb +0 -412
- data/lib/familia/validation.rb +0 -140
- data/try/data_types/set_try.rb +0 -33
- data/try/features/relationships/categorical_permissions_try.rb +0 -515
- data/try/features/safe_dump/module_based_extensions_try.rb +0 -100
- data/try/features/safe_dump/safe_dump_autoloading_try.rb +0 -107
- data/try/validation/atomic_operations_try.rb.disabled +0 -320
- data/try/validation/command_validation_try.rb.disabled +0 -207
- data/try/validation/performance_validation_try.rb.disabled +0 -324
- data/try/validation/real_world_scenarios_try.rb.disabled +0 -390
@@ -0,0 +1,684 @@
|
|
1
|
+
# Relationship Methods Reference
|
2
|
+
|
3
|
+
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.
|
4
|
+
|
5
|
+
## Method Generation Overview
|
6
|
+
|
7
|
+
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.
|
8
|
+
|
9
|
+
## Participation Methods (`participates_in`)
|
10
|
+
|
11
|
+
### Basic Participation Declaration
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
class Domain < Familia::Horreum
|
15
|
+
feature :relationships
|
16
|
+
participates_in Customer, :domains
|
17
|
+
end
|
18
|
+
|
19
|
+
class Customer < Familia::Horreum
|
20
|
+
feature :relationships
|
21
|
+
set :domains # Must define the collection
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
### Generated Instance Methods
|
26
|
+
|
27
|
+
**On Domain instances:**
|
28
|
+
- **`add_to_customer_domains(customer_obj_or_id)`** - Add this domain to customer's domains collection
|
29
|
+
- **`remove_from_customer_domains(customer_obj_or_id)`** - Remove this domain from customer's domains collection
|
30
|
+
- **`in_customer_domains?(customer_obj_or_id)`** - Check if this domain is in customer's domains collection
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
domain = Domain.new(name: "example.com")
|
34
|
+
customer = Customer.new(name: "Acme Corp")
|
35
|
+
|
36
|
+
# Explicit method calls
|
37
|
+
domain.add_to_customer_domains(customer)
|
38
|
+
domain.in_customer_domains?(customer) # => true
|
39
|
+
domain.remove_from_customer_domains(customer)
|
40
|
+
domain.in_customer_domains?(customer) # => false
|
41
|
+
```
|
42
|
+
|
43
|
+
### Collection Operator Support
|
44
|
+
|
45
|
+
**Ruby-like syntax with automatic bidirectional updates:**
|
46
|
+
```ruby
|
47
|
+
customer.domains << domain # Equivalent to domain.add_to_customer_domains(customer)
|
48
|
+
customer.domains.delete(domain.identifier) # Removes relationship bidirectionally
|
49
|
+
```
|
50
|
+
|
51
|
+
### Method Naming Pattern
|
52
|
+
`{action}_{to|from}_{lowercase_class_name}_{collection_name}` or `in_{lowercase_class_name}_{collection_name}?`
|
53
|
+
|
54
|
+
## Class-Level Tracking Methods (`class_participates_in`)
|
55
|
+
|
56
|
+
### Declaration
|
57
|
+
```ruby
|
58
|
+
class Customer < Familia::Horreum
|
59
|
+
feature :relationships
|
60
|
+
class_participates_in :all_customers, score: :created_at
|
61
|
+
class_participates_in :active_customers, score: ->(customer) {
|
62
|
+
customer.status == 'active' ? customer.last_activity : 0
|
63
|
+
}
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
### Generated Class Methods
|
68
|
+
|
69
|
+
**Collection access:**
|
70
|
+
- **`Customer.all_customers`** - Returns the sorted set collection directly
|
71
|
+
- **`Customer.active_customers`** - Returns the active customers sorted set
|
72
|
+
|
73
|
+
**Manual management (rarely needed):**
|
74
|
+
- **`Customer.add_to_all_customers(customer)`** - Manually add customer to tracking
|
75
|
+
- **`Customer.remove_from_all_customers(customer)`** - Manually remove customer from tracking
|
76
|
+
|
77
|
+
### Collection Operations
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
# Query operations (delegated to underlying sorted set)
|
81
|
+
Customer.all_customers.size # Count of tracked customers
|
82
|
+
Customer.all_customers.range(0, 9) # First 10 customers (by score)
|
83
|
+
Customer.all_customers.range_by_score(min_score, max_score) # Customers in score range
|
84
|
+
|
85
|
+
# Score-based queries
|
86
|
+
recent_customers = Customer.all_customers.range_by_score(
|
87
|
+
(Time.now - 1.week).to_i, '+inf'
|
88
|
+
)
|
89
|
+
|
90
|
+
# Get customer with scores
|
91
|
+
customers_with_scores = Customer.all_customers.range(0, -1, with_scores: true)
|
92
|
+
# => [["customer_id_1", 1634567890], ["customer_id_2", 1634567920], ...]
|
93
|
+
```
|
94
|
+
|
95
|
+
### Automatic Behavior
|
96
|
+
- Objects are **automatically added** to class-level tracking collections when saved
|
97
|
+
- Objects are **automatically removed** when destroyed
|
98
|
+
- Scores are **automatically calculated** using the provided field or lambda
|
99
|
+
- No manual method calls required for basic lifecycle tracking
|
100
|
+
|
101
|
+
### Scored Participation in Parent Collections
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
class User < Familia::Horreum
|
105
|
+
feature :relationships
|
106
|
+
participates_in Team, :active_users, score: :last_seen
|
107
|
+
participates_in Team, :top_performers, score: ->(user) { user.performance_score }
|
108
|
+
end
|
109
|
+
|
110
|
+
class Team < Familia::Horreum
|
111
|
+
feature :relationships
|
112
|
+
sorted_set :active_users, :top_performers
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
**Generated methods on Team instances:**
|
117
|
+
- **`team.active_users`** - Access the scored collection
|
118
|
+
- **`team.top_performers`** - Access the performance-scored collection
|
119
|
+
|
120
|
+
**Usage:**
|
121
|
+
```ruby
|
122
|
+
team = Team.new(name: "Development")
|
123
|
+
user = User.new(name: "Alice", last_seen: Time.now.to_i)
|
124
|
+
|
125
|
+
# User automatically added with score when relationship established
|
126
|
+
team.active_users << user
|
127
|
+
|
128
|
+
# Query by score ranges
|
129
|
+
recently_active = team.active_users.range_by_score(
|
130
|
+
(Time.now - 1.hour).to_i, '+inf'
|
131
|
+
)
|
132
|
+
```
|
133
|
+
|
134
|
+
## Indexing Methods (`indexed_by`)
|
135
|
+
|
136
|
+
The `indexed_by` declaration creates Valkey/Redis hash-based indexes for O(1) field lookups with automatic management.
|
137
|
+
|
138
|
+
### Class-Level Indexing (`class_indexed_by`)
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
class Customer < Familia::Horreum
|
142
|
+
feature :relationships
|
143
|
+
field :email, :username, :api_key
|
144
|
+
|
145
|
+
class_indexed_by :email, :email_lookup
|
146
|
+
class_indexed_by :username, :username_lookup
|
147
|
+
class_indexed_by :api_key, :api_key_lookup
|
148
|
+
end
|
149
|
+
```
|
150
|
+
|
151
|
+
### Generated Class Methods
|
152
|
+
|
153
|
+
**Index access:**
|
154
|
+
- **`Customer.email_lookup`** - Returns the hash index directly
|
155
|
+
- **`Customer.username_lookup`** - Returns the username hash index
|
156
|
+
- **`Customer.api_key_lookup`** - Returns the API key hash index
|
157
|
+
|
158
|
+
**Convenience lookup methods:**
|
159
|
+
- **`Customer.find_by_email(email)`** - Find customer ID by email (O(1) lookup)
|
160
|
+
- **`Customer.find_by_username(username)`** - Find customer ID by username
|
161
|
+
- **`Customer.find_by_api_key(api_key)`** - Find customer ID by API key
|
162
|
+
|
163
|
+
### Generated Instance Methods (rarely used manually)
|
164
|
+
|
165
|
+
**Index management:**
|
166
|
+
- **`customer.add_to_class_email_lookup`** - Manually add to email index
|
167
|
+
- **`customer.remove_from_class_email_lookup`** - Manually remove from email index
|
168
|
+
- **`customer.update_class_email_lookup(old_email)`** - Update index when email changes
|
169
|
+
|
170
|
+
### Usage Examples
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
# Automatic indexing on save
|
174
|
+
customer = Customer.new(
|
175
|
+
email: "alice@example.com",
|
176
|
+
username: "alice123",
|
177
|
+
api_key: "ak_abcd1234"
|
178
|
+
)
|
179
|
+
customer.save # Automatically added to all indexes
|
180
|
+
|
181
|
+
# O(1) lookups
|
182
|
+
customer_id = Customer.find_by_email("alice@example.com")
|
183
|
+
customer = Customer.load(customer_id) if customer_id
|
184
|
+
|
185
|
+
# Direct index access
|
186
|
+
all_emails = Customer.email_lookup.to_h
|
187
|
+
# => {"alice@example.com" => "customer_1", "bob@example.com" => "customer_2"}
|
188
|
+
|
189
|
+
# Index operations
|
190
|
+
Customer.email_lookup.get("alice@example.com") # => "customer_1"
|
191
|
+
Customer.email_lookup.set("new@example.com", "customer_3")
|
192
|
+
```
|
193
|
+
|
194
|
+
### Automatic Behavior
|
195
|
+
- Objects are **automatically indexed** when saved
|
196
|
+
- Indexes are **automatically updated** when indexed fields change
|
197
|
+
- Objects are **automatically removed** from indexes when destroyed
|
198
|
+
- No manual index management required for standard lifecycle operations
|
199
|
+
|
200
|
+
**Redis key pattern:** `{class_name.downcase}:{index_name}`
|
201
|
+
|
202
|
+
### Relationship-Scoped Indexing (`indexed_by` with `target:`)
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
class Customer < Familia::Horreum
|
206
|
+
feature :relationships
|
207
|
+
field :name
|
208
|
+
set :domains
|
209
|
+
end
|
210
|
+
|
211
|
+
class Domain < Familia::Horreum
|
212
|
+
feature :relationships
|
213
|
+
field :name, :subdomain, :port
|
214
|
+
participates_in Customer, :domains
|
215
|
+
|
216
|
+
# Index domains by name within each customer (domains can have same name across customers)
|
217
|
+
indexed_by :name, :domain_index, target: Customer
|
218
|
+
indexed_by :subdomain, :subdomain_index, target: Customer
|
219
|
+
indexed_by :port, :port_index, target: Customer
|
220
|
+
end
|
221
|
+
```
|
222
|
+
|
223
|
+
### Generated Methods on Target Class (Customer)
|
224
|
+
|
225
|
+
**Single lookups:**
|
226
|
+
- **`customer.find_by_name(domain_name)`** - Find domain ID by name within this customer
|
227
|
+
- **`customer.find_by_subdomain(subdomain)`** - Find domain ID by subdomain within this customer
|
228
|
+
- **`customer.find_by_port(port)`** - Find domain ID by port within this customer
|
229
|
+
|
230
|
+
**Multiple lookups:**
|
231
|
+
- **`customer.find_all_by_name(domain_names)`** - Find multiple domain IDs by names
|
232
|
+
- **`customer.find_all_by_subdomain(subdomains)`** - Find multiple domain IDs by subdomains
|
233
|
+
- **`customer.find_all_by_port(ports)`** - Find multiple domain IDs by ports
|
234
|
+
|
235
|
+
**Direct index access:**
|
236
|
+
- **`customer.domain_index`** - Access the name index hash directly
|
237
|
+
- **`customer.subdomain_index`** - Access the subdomain index hash directly
|
238
|
+
- **`customer.port_index`** - Access the port index hash directly
|
239
|
+
|
240
|
+
### Usage Examples
|
241
|
+
|
242
|
+
```ruby
|
243
|
+
customer = Customer.new(name: "Acme Corp")
|
244
|
+
domain1 = Domain.new(name: "example.com", subdomain: "www", port: 443)
|
245
|
+
domain2 = Domain.new(name: "api.example.com", subdomain: "api", port: 443)
|
246
|
+
|
247
|
+
# Establish relationships (automatic indexing)
|
248
|
+
customer.domains << domain1
|
249
|
+
customer.domains << domain2
|
250
|
+
|
251
|
+
# O(1) lookups within customer scope
|
252
|
+
domain_id = customer.find_by_name("example.com")
|
253
|
+
api_domain_id = customer.find_by_subdomain("api")
|
254
|
+
|
255
|
+
# Multiple lookups
|
256
|
+
ssl_domains = customer.find_all_by_port([443, 8443])
|
257
|
+
|
258
|
+
# Direct index access
|
259
|
+
all_domain_names = customer.domain_index.to_h
|
260
|
+
# => {"example.com" => "domain_1", "api.example.com" => "domain_2"}
|
261
|
+
|
262
|
+
# Check if customer has domain with specific name
|
263
|
+
has_domain = customer.domain_index.get("example.com").present?
|
264
|
+
```
|
265
|
+
|
266
|
+
### Generated Methods on Indexed Class (Domain)
|
267
|
+
|
268
|
+
**Index management (automatic):**
|
269
|
+
- **`domain.add_to_customer_domain_index(customer)`** - Add to customer's domain name index
|
270
|
+
- **`domain.remove_from_customer_domain_index(customer)`** - Remove from customer's domain name index
|
271
|
+
- **`domain.update_customer_domain_index(customer, old_name)`** - Update index when name changes
|
272
|
+
|
273
|
+
### Automatic Behavior
|
274
|
+
- Domain is **automatically indexed** when added to customer's domains collection
|
275
|
+
- Index is **automatically updated** when domain's indexed fields change
|
276
|
+
- Domain is **automatically removed** from index when relationship is removed
|
277
|
+
- All index management happens transparently during relationship operations
|
278
|
+
|
279
|
+
**Redis key pattern:** `{target_class.downcase}:{target_id}:{index_name}`
|
280
|
+
|
281
|
+
### When to Use Each Indexing Context
|
282
|
+
|
283
|
+
**Class-level indexing (`class_indexed_by`):**
|
284
|
+
- Use for **system-wide unique** field lookups
|
285
|
+
- Examples: email addresses, usernames, API keys, social security numbers
|
286
|
+
- Best when field values should be unique across all instances
|
287
|
+
|
288
|
+
**Relationship-scoped indexing (`indexed_by` with `target:`):**
|
289
|
+
- Use for **context-specific** field lookups
|
290
|
+
- Examples: domain names per customer, project names per team, usernames per organization
|
291
|
+
- Best when field values are unique within a specific parent context but may duplicate across different parents
|
292
|
+
|
293
|
+
## Complete API Reference
|
294
|
+
|
295
|
+
### Method Naming Conventions
|
296
|
+
|
297
|
+
**Participation methods:**
|
298
|
+
- `{add_to|remove_from}_{target_class_downcase}_{collection_name}(target)`
|
299
|
+
- `in_{target_class_downcase}_{collection_name}?(target)`
|
300
|
+
|
301
|
+
**Class-level tracking:**
|
302
|
+
- `{add_to|remove_from}_{collection_name}(object)` (class methods)
|
303
|
+
- `{ClassName}.{collection_name}` (collection accessor)
|
304
|
+
|
305
|
+
**Class-level indexing:**
|
306
|
+
- `{add_to|remove_from}_class_{index_name}` (instance methods)
|
307
|
+
- `{ClassName}.{index_name}` (index accessor)
|
308
|
+
- `{ClassName}.find_by_{field_name}(value)` (convenience lookup)
|
309
|
+
|
310
|
+
**Relationship-scoped indexing:**
|
311
|
+
- `{target_instance}.find_by_{field_name}(value)` (single lookup)
|
312
|
+
- `{target_instance}.find_all_by_{field_name}(values)` (multiple lookup)
|
313
|
+
- `{target_instance}.{index_name}` (direct index access)
|
314
|
+
|
315
|
+
## Key Benefits of the Relationships API
|
316
|
+
|
317
|
+
- **Automatic Management**: Save operations update indexes and tracking automatically
|
318
|
+
- **Ruby-Idiomatic**: Use `<<` operator for natural collection syntax
|
319
|
+
- **Consistent Storage**: All indexes stored at class level for architectural simplicity
|
320
|
+
- **Predictable Naming**: Method names follow consistent patterns for easy discovery
|
321
|
+
- **O(1) Performance**: Hash-based indexes provide constant-time lookups
|
322
|
+
- **Bidirectional Sync**: Relationship changes automatically update both sides
|
323
|
+
|
324
|
+
## Advanced Implementation Patterns
|
325
|
+
|
326
|
+
### Error Handling and Edge Cases
|
327
|
+
|
328
|
+
**Handling Missing Objects:**
|
329
|
+
```ruby
|
330
|
+
# Safe relationship operations
|
331
|
+
begin
|
332
|
+
customer.domains << domain
|
333
|
+
rescue Familia::Problem => e
|
334
|
+
Rails.logger.error "Failed to establish relationship: #{e.message}"
|
335
|
+
end
|
336
|
+
|
337
|
+
# Check if objects exist before relating
|
338
|
+
if domain.persisted? && customer.persisted?
|
339
|
+
customer.domains << domain
|
340
|
+
end
|
341
|
+
|
342
|
+
# Batch operations with error handling
|
343
|
+
domain_ids.each do |id|
|
344
|
+
next unless Domain.exists?(id) # Custom existence check
|
345
|
+
customer.domains.add(id)
|
346
|
+
end
|
347
|
+
```
|
348
|
+
|
349
|
+
**Index Consistency Validation:**
|
350
|
+
```ruby
|
351
|
+
class Customer < Familia::Horreum
|
352
|
+
feature :relationships
|
353
|
+
|
354
|
+
def validate_email_index_consistency
|
355
|
+
stored_id = self.class.email_lookup.get(email)
|
356
|
+
return true if stored_id == identifier
|
357
|
+
|
358
|
+
Rails.logger.warn "Email index inconsistency: #{email} -> #{stored_id} vs #{identifier}"
|
359
|
+
# Repair the index
|
360
|
+
self.class.email_lookup.set(email, identifier)
|
361
|
+
false
|
362
|
+
end
|
363
|
+
|
364
|
+
after_save :validate_email_index_consistency
|
365
|
+
end
|
366
|
+
```
|
367
|
+
|
368
|
+
### Performance Optimization Techniques
|
369
|
+
|
370
|
+
**Lazy Loading Patterns:**
|
371
|
+
```ruby
|
372
|
+
class Customer < Familia::Horreum
|
373
|
+
feature :relationships
|
374
|
+
set :domains
|
375
|
+
|
376
|
+
# Memoized relationship loading
|
377
|
+
def domain_objects
|
378
|
+
@domain_objects ||= begin
|
379
|
+
domain_ids = domains.to_a
|
380
|
+
Domain.multiget(*domain_ids).compact
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
# Paginated relationship loading
|
385
|
+
def recent_domains(limit = 10)
|
386
|
+
recent_ids = domains.range(0, limit - 1)
|
387
|
+
Domain.multiget(*recent_ids).compact
|
388
|
+
end
|
389
|
+
|
390
|
+
# Selective field loading
|
391
|
+
def domain_names
|
392
|
+
domains.to_a.map do |id|
|
393
|
+
Domain.new(domain_id: id).name # Load only name field
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|
397
|
+
```
|
398
|
+
|
399
|
+
**Bulk Operations with Transaction Safety:**
|
400
|
+
```ruby
|
401
|
+
class Team < Familia::Horreum
|
402
|
+
feature :relationships
|
403
|
+
set :members
|
404
|
+
|
405
|
+
def bulk_add_members(user_ids)
|
406
|
+
# Validate all IDs exist first
|
407
|
+
valid_ids = user_ids.select { |id| User.exists?(id) }
|
408
|
+
|
409
|
+
# Use Valkey/Redis pipeline for bulk operations
|
410
|
+
Familia.redis.pipelined do |pipeline|
|
411
|
+
valid_ids.each do |user_id|
|
412
|
+
members.add(user_id)
|
413
|
+
# Update reverse indexes in same pipeline
|
414
|
+
user = User.new(user_id: user_id)
|
415
|
+
user.add_to_team_members(self)
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
valid_ids.size
|
420
|
+
end
|
421
|
+
|
422
|
+
def bulk_remove_members(user_ids)
|
423
|
+
# Atomic bulk removal
|
424
|
+
removed_count = 0
|
425
|
+
user_ids.each do |user_id|
|
426
|
+
if members.delete(user_id)
|
427
|
+
removed_count += 1
|
428
|
+
# Clean up reverse relationship
|
429
|
+
user = User.new(user_id: user_id)
|
430
|
+
user.remove_from_team_members(self)
|
431
|
+
end
|
432
|
+
end
|
433
|
+
removed_count
|
434
|
+
end
|
435
|
+
end
|
436
|
+
```
|
437
|
+
|
438
|
+
### Custom Scoring and Complex Relationships
|
439
|
+
|
440
|
+
**Dynamic Scoring with Context:**
|
441
|
+
```ruby
|
442
|
+
class Project < Familia::Horreum
|
443
|
+
feature :relationships
|
444
|
+
field :priority, :created_at, :deadline
|
445
|
+
|
446
|
+
participates_in Team, :projects, score: :calculated_priority
|
447
|
+
|
448
|
+
private
|
449
|
+
|
450
|
+
def calculated_priority
|
451
|
+
base_priority = priority.to_i
|
452
|
+
urgency_multiplier = deadline_urgency_factor
|
453
|
+
age_factor = project_age_factor
|
454
|
+
|
455
|
+
(base_priority * urgency_multiplier * age_factor).to_i
|
456
|
+
end
|
457
|
+
|
458
|
+
def deadline_urgency_factor
|
459
|
+
return 1.0 unless deadline
|
460
|
+
|
461
|
+
days_until_deadline = (deadline.to_time - Time.now) / 1.day
|
462
|
+
return 3.0 if days_until_deadline <= 1 # Critical
|
463
|
+
return 2.0 if days_until_deadline <= 7 # High
|
464
|
+
1.0 # Normal
|
465
|
+
end
|
466
|
+
|
467
|
+
def project_age_factor
|
468
|
+
days_old = (Time.now - created_at.to_time) / 1.day
|
469
|
+
[1.0 + (days_old / 30.0), 2.0].min # Cap at 2x multiplier
|
470
|
+
end
|
471
|
+
end
|
472
|
+
```
|
473
|
+
|
474
|
+
**Multi-Level Relationships:**
|
475
|
+
```ruby
|
476
|
+
class Organization < Familia::Horreum
|
477
|
+
feature :relationships
|
478
|
+
set :departments
|
479
|
+
|
480
|
+
# Find all users across all departments
|
481
|
+
def all_users
|
482
|
+
department_ids = departments.to_a
|
483
|
+
user_ids = []
|
484
|
+
|
485
|
+
department_ids.each do |dept_id|
|
486
|
+
dept = Department.load(dept_id)
|
487
|
+
user_ids.concat(dept.users.to_a) if dept
|
488
|
+
end
|
489
|
+
|
490
|
+
User.multiget(*user_ids.uniq).compact
|
491
|
+
end
|
492
|
+
|
493
|
+
# Hierarchical relationship queries
|
494
|
+
def users_in_department(department_name)
|
495
|
+
dept_id = find_by_name(department_name)
|
496
|
+
return [] unless dept_id
|
497
|
+
|
498
|
+
dept = Department.load(dept_id)
|
499
|
+
return [] unless dept
|
500
|
+
|
501
|
+
user_ids = dept.users.to_a
|
502
|
+
User.multiget(*user_ids).compact
|
503
|
+
end
|
504
|
+
end
|
505
|
+
```
|
506
|
+
|
507
|
+
### Advanced Indexing Patterns
|
508
|
+
|
509
|
+
**Composite Index Keys:**
|
510
|
+
```ruby
|
511
|
+
class ApiKey < Familia::Horreum
|
512
|
+
feature :relationships
|
513
|
+
field :customer_id, :environment, :key_type, :key_hash
|
514
|
+
|
515
|
+
# Composite index for environment + type lookups
|
516
|
+
indexed_by :environment_and_type, :env_type_lookup, target: Customer
|
517
|
+
|
518
|
+
private
|
519
|
+
|
520
|
+
def environment_and_type
|
521
|
+
"#{environment}:#{key_type}" # e.g., "production:read_write"
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
# Usage
|
526
|
+
customer.find_by_environment_and_type("production:read_only")
|
527
|
+
customer.find_all_by_environment_and_type(["staging:read_write", "production:read_write"])
|
528
|
+
```
|
529
|
+
|
530
|
+
**Time-Based Index Partitioning:**
|
531
|
+
```ruby
|
532
|
+
class Event < Familia::Horreum
|
533
|
+
feature :relationships
|
534
|
+
field :event_type, :timestamp, :user_id
|
535
|
+
|
536
|
+
participates_in User, :events, score: :timestamp
|
537
|
+
indexed_by :daily_partition, :daily_events, target: User
|
538
|
+
|
539
|
+
private
|
540
|
+
|
541
|
+
def daily_partition
|
542
|
+
Time.at(timestamp).strftime('%Y%m%d') # e.g., "20241215"
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
# Usage - find today's events for user
|
547
|
+
today = Time.now.strftime('%Y%m%d')
|
548
|
+
todays_event_ids = user.find_all_by_daily_partition([today])
|
549
|
+
```
|
550
|
+
|
551
|
+
### Testing Relationship Methods
|
552
|
+
|
553
|
+
**Unit Testing Patterns:**
|
554
|
+
```ruby
|
555
|
+
# test/models/relationship_test.rb
|
556
|
+
class RelationshipTest < Minitest::Test
|
557
|
+
def setup
|
558
|
+
@customer = Customer.create(email: "test@example.com", name: "Test Corp")
|
559
|
+
@domain = Domain.create(name: "test.com", status: "active")
|
560
|
+
end
|
561
|
+
|
562
|
+
def test_bidirectional_relationship_establishment
|
563
|
+
@customer.domains << @domain
|
564
|
+
|
565
|
+
# Test both sides of relationship
|
566
|
+
assert @customer.domains.member?(@domain.identifier)
|
567
|
+
assert @domain.in_customer_domains?(@customer.identifier)
|
568
|
+
end
|
569
|
+
|
570
|
+
def test_relationship_removal
|
571
|
+
@customer.domains << @domain
|
572
|
+
@customer.domains.delete(@domain.identifier)
|
573
|
+
|
574
|
+
# Verify complete cleanup
|
575
|
+
refute @customer.domains.member?(@domain.identifier)
|
576
|
+
refute @domain.in_customer_domains?(@customer.identifier)
|
577
|
+
end
|
578
|
+
|
579
|
+
def test_index_automatic_maintenance
|
580
|
+
@customer.save # Should trigger indexing
|
581
|
+
|
582
|
+
found_id = Customer.find_by_email("test@example.com")
|
583
|
+
assert_equal @customer.identifier, found_id
|
584
|
+
end
|
585
|
+
|
586
|
+
def test_scoped_index_lookup
|
587
|
+
@customer.domains << @domain
|
588
|
+
|
589
|
+
found_id = @customer.find_by_name("test.com")
|
590
|
+
assert_equal @domain.identifier, found_id
|
591
|
+
end
|
592
|
+
|
593
|
+
def test_scored_relationship_ordering
|
594
|
+
project = Project.create(name: "Test Project")
|
595
|
+
task1 = Task.create(title: "Low priority", priority: 1)
|
596
|
+
task2 = Task.create(title: "High priority", priority: 10)
|
597
|
+
|
598
|
+
project.tasks << task1
|
599
|
+
project.tasks << task2
|
600
|
+
|
601
|
+
# Should be ordered by priority (high to low)
|
602
|
+
task_ids = project.tasks.range(0, -1, order: 'DESC')
|
603
|
+
assert_equal task2.identifier, task_ids.first
|
604
|
+
assert_equal task1.identifier, task_ids.last
|
605
|
+
end
|
606
|
+
end
|
607
|
+
```
|
608
|
+
|
609
|
+
### Complete Production Example
|
610
|
+
|
611
|
+
```ruby
|
612
|
+
# Real-world e-commerce system
|
613
|
+
class Customer < Familia::Horreum
|
614
|
+
feature :relationships
|
615
|
+
field :email, :username, :tier, :created_at, :last_activity
|
616
|
+
|
617
|
+
# Global unique lookups
|
618
|
+
class_indexed_by :email, :email_lookup
|
619
|
+
class_indexed_by :username, :username_lookup
|
620
|
+
|
621
|
+
# Tiered customer tracking
|
622
|
+
class_participates_in :all_customers, score: :created_at
|
623
|
+
class_participates_in :active_customers, score: :last_activity
|
624
|
+
class_participates_in :premium_customers,
|
625
|
+
score: ->(c) { c.tier == 'premium' ? c.last_activity : 0 }
|
626
|
+
|
627
|
+
# Relationships
|
628
|
+
set :orders, :addresses, :payment_methods
|
629
|
+
|
630
|
+
def recent_orders(limit = 10)
|
631
|
+
order_ids = orders.range(0, limit - 1)
|
632
|
+
Order.multiget(*order_ids).compact
|
633
|
+
end
|
634
|
+
|
635
|
+
def total_spent
|
636
|
+
recent_orders(100).sum(&:total_amount)
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
class Order < Familia::Horreum
|
641
|
+
feature :relationships
|
642
|
+
field :total_amount, :status, :created_at, :order_number
|
643
|
+
|
644
|
+
participates_in Customer, :orders, score: :created_at
|
645
|
+
indexed_by :order_number, :order_lookup, target: Customer
|
646
|
+
indexed_by :status, :status_lookup, target: Customer
|
647
|
+
|
648
|
+
set :line_items
|
649
|
+
end
|
650
|
+
|
651
|
+
class Address < Familia::Horreum
|
652
|
+
feature :relationships
|
653
|
+
field :street, :city, :state, :zip_code, :address_type
|
654
|
+
|
655
|
+
participates_in Customer, :addresses
|
656
|
+
indexed_by :address_type, :address_type_lookup, target: Customer
|
657
|
+
end
|
658
|
+
|
659
|
+
# Usage in production
|
660
|
+
customer = Customer.create(
|
661
|
+
email: "alice@example.com",
|
662
|
+
username: "alice_smith",
|
663
|
+
tier: "premium"
|
664
|
+
)
|
665
|
+
|
666
|
+
# Automatic indexing and tracking
|
667
|
+
Customer.find_by_email("alice@example.com") # => customer.identifier
|
668
|
+
Customer.premium_customers.range(0, 9) # Recent premium customers
|
669
|
+
|
670
|
+
# Complex relationship queries
|
671
|
+
order = Order.create(order_number: "ORD-12345", status: "shipped")
|
672
|
+
customer.orders << order
|
673
|
+
|
674
|
+
customer.find_by_order_number("ORD-12345") # => order.identifier
|
675
|
+
shipped_orders = customer.find_all_by_status(["shipped", "delivered"])
|
676
|
+
```
|
677
|
+
|
678
|
+
---
|
679
|
+
|
680
|
+
## See Also
|
681
|
+
|
682
|
+
- **[Relationships Feature Guide](feature-relationships.md)** - Conceptual introduction to relationships
|
683
|
+
- **[Technical Reference](../reference/api-technical.md#relationships-feature-v200-pre7)** - Advanced implementation patterns
|
684
|
+
- **[Feature System Guide](feature-system.md)** - Understanding Familia's feature architecture
|