familia 2.0.0.pre22 → 2.0.0.pre23
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/claude-code-review.yml +8 -5
- data/CHANGELOG.rst +33 -0
- data/Gemfile.lock +8 -8
- data/docs/1106-participates_in-bidirectional-solution.md +201 -58
- data/examples/through_relationships.rb +275 -0
- data/lib/familia/features/relationships/README.md +1 -1
- data/lib/familia/features/relationships/participation/participant_methods.rb +59 -10
- data/lib/familia/features/relationships/participation/target_methods.rb +51 -7
- data/lib/familia/features/relationships/participation/through_model_operations.rb +150 -0
- data/lib/familia/features/relationships/participation.rb +39 -15
- data/lib/familia/features/relationships/participation_relationship.rb +19 -1
- data/lib/familia/features/relationships.rb +1 -1
- data/lib/familia/version.rb +1 -1
- data/pr_agent.toml +6 -1
- data/try/features/relationships/participation_commands_verification_spec.rb +1 -1
- data/try/features/relationships/participation_commands_verification_try.rb +1 -1
- data/try/features/relationships/participation_method_prefix_try.rb +133 -0
- data/try/features/relationships/participation_reverse_index_try.rb +1 -1
- data/try/features/relationships/{participation_bidirectional_try.rb → participation_reverse_methods_try.rb} +6 -6
- data/try/features/relationships/participation_through_try.rb +173 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc6c562adc770554d50a5d8d97be46b8a924d04ed6a28e7447a765990e69d89b
|
|
4
|
+
data.tar.gz: 97812dfbbcd8b7cb2e3c9447558e7b1b6eb30181d9eed8c3ae86e0371f83cf7e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6d41492ba9f38dafacfe65a37a792c6f50ad90e406718131b0e226a6003b27008a1057851eb1d73ed1ff8baf8cf95312e684b290e23b3bbc2c9b5d453f0ba2ea
|
|
7
|
+
data.tar.gz: 2bf7cb92e0e06b623429dd0ff3b1da46686236329f5f3540e9f344195d1a51b50a209fc3b7d6538827c91e51194dd1014f9e5efd536a264c08283c505ea5044f
|
|
@@ -7,11 +7,11 @@ on:
|
|
|
7
7
|
|
|
8
8
|
jobs:
|
|
9
9
|
claude-review:
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
# Run on opened/synchronize, but only on 'labeled' if label is 'claude-review'
|
|
11
|
+
if: |
|
|
12
|
+
github.event.action == 'opened' ||
|
|
13
|
+
github.event.action == 'synchronize' ||
|
|
14
|
+
(github.event.action == 'labeled' && github.event.label.name == 'claude-review')
|
|
15
15
|
|
|
16
16
|
runs-on: ubuntu-latest
|
|
17
17
|
permissions:
|
|
@@ -50,3 +50,6 @@ jobs:
|
|
|
50
50
|
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
|
51
51
|
# or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
|
|
52
52
|
allowed_tools: "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"
|
|
53
|
+
|
|
54
|
+
# Allow bot-initiated workflows (e.g., qodo, dependabot)
|
|
55
|
+
allowed_bots: "*"
|
data/CHANGELOG.rst
CHANGED
|
@@ -7,6 +7,39 @@ The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.1.0/>`
|
|
|
7
7
|
|
|
8
8
|
<!--scriv-insert-here-->
|
|
9
9
|
|
|
10
|
+
.. _changelog-2.0.0.pre23:
|
|
11
|
+
|
|
12
|
+
2.0.0.pre23 — 2025-12-22
|
|
13
|
+
========================
|
|
14
|
+
|
|
15
|
+
Added
|
|
16
|
+
-----
|
|
17
|
+
|
|
18
|
+
- Add ``:through`` option to ``participates_in`` for join model support.
|
|
19
|
+
Enables storing additional attributes (role, permissions, metadata) on
|
|
20
|
+
participation relationships via an intermediate model. The through model
|
|
21
|
+
uses deterministic keys and supports idempotent operations - adding an
|
|
22
|
+
existing participant updates rather than duplicates.
|
|
23
|
+
|
|
24
|
+
Security
|
|
25
|
+
--------
|
|
26
|
+
|
|
27
|
+
- Add validation for through model attributes to prevent arbitrary method
|
|
28
|
+
invocation. Only fields defined on the through model schema can be set
|
|
29
|
+
via the ``through_attrs`` parameter.
|
|
30
|
+
|
|
31
|
+
Documentation
|
|
32
|
+
-------------
|
|
33
|
+
|
|
34
|
+
- Add YARD documentation for the ``:through`` parameter on both
|
|
35
|
+
``participates_in`` and ``class_participates_in`` methods.
|
|
36
|
+
|
|
37
|
+
AI Assistance
|
|
38
|
+
-------------
|
|
39
|
+
|
|
40
|
+
- Implementation design and code review assistance provided by Claude.
|
|
41
|
+
Security hardening for attribute validation added based on Qodo review.
|
|
42
|
+
|
|
10
43
|
.. _changelog-2.0.0.pre22:
|
|
11
44
|
|
|
12
45
|
2.0.0.pre22 — 2025-12-03
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
familia (2.0.0.
|
|
4
|
+
familia (2.0.0.pre23)
|
|
5
5
|
benchmark (~> 0.4)
|
|
6
6
|
concurrent-ruby (~> 1.3)
|
|
7
7
|
connection_pool (~> 2.5)
|
|
@@ -17,10 +17,10 @@ GEM
|
|
|
17
17
|
specs:
|
|
18
18
|
ast (2.4.3)
|
|
19
19
|
base64 (0.3.0)
|
|
20
|
-
benchmark (0.
|
|
21
|
-
bigdecimal (3.
|
|
22
|
-
concurrent-ruby (1.3.
|
|
23
|
-
connection_pool (2.5.
|
|
20
|
+
benchmark (0.5.0)
|
|
21
|
+
bigdecimal (3.3.1)
|
|
22
|
+
concurrent-ruby (1.3.6)
|
|
23
|
+
connection_pool (2.5.5)
|
|
24
24
|
csv (3.3.5)
|
|
25
25
|
date (3.5.0)
|
|
26
26
|
debug (1.11.0)
|
|
@@ -69,7 +69,7 @@ GEM
|
|
|
69
69
|
lint_roller (1.1.0)
|
|
70
70
|
logger (1.7.0)
|
|
71
71
|
minitest (5.26.0)
|
|
72
|
-
oj (3.16.
|
|
72
|
+
oj (3.16.13)
|
|
73
73
|
bigdecimal (>= 3.0)
|
|
74
74
|
ostruct (>= 0.2)
|
|
75
75
|
ostruct (0.6.3)
|
|
@@ -99,7 +99,7 @@ GEM
|
|
|
99
99
|
redcarpet (3.6.1)
|
|
100
100
|
redis (5.4.1)
|
|
101
101
|
redis-client (>= 0.22.0)
|
|
102
|
-
redis-client (0.
|
|
102
|
+
redis-client (0.26.2)
|
|
103
103
|
connection_pool
|
|
104
104
|
reek (6.5.0)
|
|
105
105
|
dry-schema (~> 1.13)
|
|
@@ -154,7 +154,7 @@ GEM
|
|
|
154
154
|
base64
|
|
155
155
|
ruby-progressbar (1.13.0)
|
|
156
156
|
stackprof (0.2.27)
|
|
157
|
-
stringio (3.1.
|
|
157
|
+
stringio (3.1.9)
|
|
158
158
|
timecop (0.9.10)
|
|
159
159
|
tryouts (3.7.1)
|
|
160
160
|
concurrent-ruby (~> 1.0, < 2)
|
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
# Bidirectional Relationships in Familia
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Document Status**: Updated 2025-12 to reflect current implementation including
|
|
4
|
+
> `:through` option and `config_name`-based method naming.
|
|
5
|
+
|
|
6
|
+
## The Original Naming Problem (Now Resolved)
|
|
7
|
+
|
|
8
|
+
The parameter was renamed from `bidirectional:` to `generate_participant_methods:`.
|
|
9
|
+
|
|
10
|
+
`bidirectional: true` was misleading because:
|
|
4
11
|
|
|
5
12
|
1. It's not truly bidirectional - It only helps you manage membership in specific instances, not query all memberships
|
|
6
|
-
2. Better name
|
|
13
|
+
2. Better name: `generate_participant_methods: true` (**now implemented**)
|
|
7
14
|
3. True bidirectionality would mean both sides can easily query their relationships
|
|
8
15
|
|
|
9
|
-
What True Bidirectionality Should Look Like
|
|
16
|
+
## What True Bidirectionality Should Look Like
|
|
10
17
|
|
|
11
|
-
Option 1: Auto-generate reverse collections
|
|
18
|
+
### Option 1: Auto-generate reverse collections
|
|
12
19
|
|
|
20
|
+
```ruby
|
|
13
21
|
class Customer < Familia::Horreum
|
|
14
22
|
participates_in Team, :members, bidirectional: true, reverse: :teams
|
|
15
23
|
end
|
|
@@ -18,9 +26,11 @@ end
|
|
|
18
26
|
customer.teams # All teams this customer is in
|
|
19
27
|
customer.teams.count # How many teams
|
|
20
28
|
customer.teams.include?(team_id) # Check membership
|
|
29
|
+
```
|
|
21
30
|
|
|
22
|
-
Option 2: Make bidirectional actually bidirectional
|
|
31
|
+
### Option 2: Make bidirectional actually bidirectional
|
|
23
32
|
|
|
33
|
+
```ruby
|
|
24
34
|
class Customer < Familia::Horreum
|
|
25
35
|
participates_in Team, :members, bidirectional: true
|
|
26
36
|
end
|
|
@@ -28,10 +38,13 @@ end
|
|
|
28
38
|
# Should auto-generate (using pluralized class name):
|
|
29
39
|
customer.teams # Since we're participating in Team class
|
|
30
40
|
customer.organizations # If also participating in Organization class
|
|
41
|
+
```
|
|
31
42
|
|
|
32
|
-
|
|
43
|
+
## The Implementation Gap (Historical Context)
|
|
33
44
|
|
|
34
|
-
Looking at the
|
|
45
|
+
Looking at the original usage pattern:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
35
48
|
# Easy to go from Team → Customers
|
|
36
49
|
team.members.to_a # Simple!
|
|
37
50
|
customers = Customer.multiget(*team.members.to_a)
|
|
@@ -41,89 +54,219 @@ customer.participations.members
|
|
|
41
54
|
.select { |k| k.start_with?("team:") }
|
|
42
55
|
.map { |k| k.split(':')[1] }
|
|
43
56
|
# ... etc - complicated!
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### What Was Really Happening
|
|
44
60
|
|
|
45
|
-
|
|
61
|
+
The bidirectional flag only controlled whether these instance-to-instance methods were generated:
|
|
62
|
+
- `customer.add_to_team_members(specific_team)`
|
|
63
|
+
- `customer.in_team_members?(specific_team)`
|
|
46
64
|
|
|
47
|
-
|
|
48
|
-
- customer.
|
|
49
|
-
- customer.
|
|
65
|
+
It did NOT create instance-to-collection methods:
|
|
66
|
+
- `customer.teams` ❌
|
|
67
|
+
- `customer.all_team_memberships` ❌
|
|
50
68
|
|
|
51
|
-
|
|
52
|
-
- customer.teams ❌
|
|
53
|
-
- customer.all_team_memberships ❌
|
|
69
|
+
---
|
|
54
70
|
|
|
71
|
+
## Current Implementation (Familia 2.x)
|
|
55
72
|
|
|
56
|
-
The
|
|
57
|
-
Bidirectional Relationships Feature Spec
|
|
73
|
+
The solution now auto-generates reverse collection methods on participant classes.
|
|
58
74
|
|
|
59
|
-
|
|
75
|
+
### Implemented API (Using `_instance` Suffix + `config_name` Naming)
|
|
60
76
|
|
|
61
|
-
|
|
62
|
-
|
|
77
|
+
**Important**: Method names are based on the target class's `config_name` (snake_case of
|
|
78
|
+
the full class name), not just the class basename. This ensures uniqueness when
|
|
79
|
+
multiple classes have the same basename (e.g., `Admin::Team` vs `Public::Team`).
|
|
63
80
|
|
|
64
|
-
|
|
81
|
+
```ruby
|
|
82
|
+
class Domain < Familia::Horreum
|
|
83
|
+
# Customer.config_name => "customer"
|
|
84
|
+
participates_in Customer, :domains
|
|
85
|
+
# Auto-generates on Domain:
|
|
86
|
+
# domain.customer_instances
|
|
87
|
+
# domain.customer_ids
|
|
88
|
+
# domain.customer?
|
|
89
|
+
# domain.customer_count
|
|
65
90
|
|
|
66
|
-
|
|
91
|
+
# With custom naming via `as:`
|
|
92
|
+
participates_in Customer, :partner_domains, as: :partners
|
|
93
|
+
# Auto-generates:
|
|
94
|
+
# domain.partners_instances
|
|
95
|
+
# domain.partners_ids
|
|
96
|
+
# domain.partners?
|
|
97
|
+
# domain.partners_count
|
|
98
|
+
end
|
|
67
99
|
|
|
68
|
-
|
|
100
|
+
class ProjectTeam < Familia::Horreum
|
|
101
|
+
# Note: config_name is "project_team", not "team"
|
|
102
|
+
end
|
|
69
103
|
|
|
70
104
|
class User < Familia::Horreum
|
|
71
|
-
participates_in
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
105
|
+
participates_in ProjectTeam, :members
|
|
106
|
+
# Auto-generates: user.project_team_instances (NOT user.team_instances)
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Forward Direction (Target Class Methods)
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
customer.domains # → SortedSet of domain IDs
|
|
114
|
+
customer.add_domains_instance(domain) # → Adds + tracks participation
|
|
115
|
+
customer.remove_domains_instance(domain) # → Removes + untracks
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Reverse Direction (Participant Class Methods)
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
domain.customer_instances # → Array of Customer instances
|
|
122
|
+
domain.customer_ids # → Array of customer IDs (no object loading)
|
|
123
|
+
domain.customer? # → Boolean: participates in any customers?
|
|
124
|
+
domain.customer_count # → Integer count without loading objects
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Implementation Status
|
|
128
|
+
|
|
129
|
+
| Method Pattern | Forward (Target) | Reverse (Participant) |
|
|
130
|
+
|----------------|------------------|----------------------|
|
|
131
|
+
| `*_instances` | `add_*_instance`, `remove_*_instance` | `{config_name}_instances` |
|
|
132
|
+
| `*_ids` | N/A (use collection directly) | `{config_name}_ids` |
|
|
133
|
+
| `*?` | N/A | `{config_name}?` |
|
|
134
|
+
| `*_count` | N/A (use `collection.size`) | `{config_name}_count` |
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Through Models (New in 2.x)
|
|
139
|
+
|
|
140
|
+
The `:through` option enables rich join model patterns for storing additional
|
|
141
|
+
membership data (roles, timestamps, status).
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
class OrganizationMembership < Familia::Horreum
|
|
145
|
+
feature :object_identifier # Required for through models
|
|
146
|
+
prefix :org_membership
|
|
147
|
+
|
|
148
|
+
field :organization_id
|
|
149
|
+
field :customer_id
|
|
150
|
+
field :role # 'owner', 'admin', 'member'
|
|
151
|
+
field :status # 'active', 'pending', 'declined'
|
|
152
|
+
field :invited_at
|
|
153
|
+
field :joined_at
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
class Customer < Familia::Horreum
|
|
157
|
+
participates_in Organization, :members,
|
|
158
|
+
score: :joined,
|
|
159
|
+
through: :OrganizationMembership
|
|
75
160
|
end
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Through Model Behavior
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
# Adding creates/updates the through model
|
|
167
|
+
membership = org.add_members_instance(customer, through_attrs: { role: 'admin' })
|
|
168
|
+
membership.role # => 'admin'
|
|
169
|
+
|
|
170
|
+
# Chaining pattern also works
|
|
171
|
+
membership = org.add_members_instance(customer)
|
|
172
|
+
membership.role = 'admin'
|
|
173
|
+
membership.save
|
|
76
174
|
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
team.remove_members_instance(user) # → Removes from collection + untracks
|
|
175
|
+
# Removing destroys the through model
|
|
176
|
+
org.remove_members_instance(customer) # OrganizationMembership is deleted
|
|
177
|
+
```
|
|
81
178
|
|
|
82
|
-
|
|
83
|
-
user.team_instances # → Array of Team instances user belongs to
|
|
84
|
-
user.team_ids # → Array of team IDs (efficient, no loading)
|
|
85
|
-
user.team? # → Boolean: belongs to any teams?
|
|
86
|
-
user.team_count # → Count without loading objects
|
|
179
|
+
### Through Model Requirements
|
|
87
180
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
181
|
+
- Must have `feature :object_identifier` enabled
|
|
182
|
+
- Through model is auto-created on add, auto-destroyed on remove
|
|
183
|
+
- Attributes can be passed inline via `through_attrs:` or set after
|
|
184
|
+
|
|
185
|
+
---
|
|
93
186
|
|
|
94
187
|
## Naming Rationale: Why `_instance` Suffix?
|
|
95
188
|
|
|
96
|
-
The implementation uses an `_instance` suffix pattern instead of pluralization/singularization
|
|
189
|
+
The implementation uses an `_instance` suffix pattern instead of pluralization/singularization:
|
|
97
190
|
|
|
98
191
|
**Target Methods (Forward Direction):**
|
|
99
|
-
- `
|
|
100
|
-
- `
|
|
192
|
+
- `customer.add_domains_instance(domain)` instead of `customer.add_domain(domain)`
|
|
193
|
+
- `customer.remove_domains_instance(domain)` instead of `customer.remove_domain(domain)`
|
|
101
194
|
|
|
102
195
|
**Reverse Collection Methods:**
|
|
103
|
-
- `
|
|
196
|
+
- `domain.customer_instances` instead of `domain.customers`
|
|
104
197
|
- `user.organization_instances` instead of `user.organizations`
|
|
105
198
|
|
|
106
199
|
**Benefits:**
|
|
107
|
-
1. **No irregular plurals** - Avoids issues with
|
|
200
|
+
1. **No irregular plurals** - Avoids issues with "person/people", "child/children", "foot/feet"
|
|
108
201
|
2. **Clear intent** - The suffix makes it obvious you're working with instances, not counts or IDs
|
|
109
202
|
3. **Consistent pattern** - Same suffix for both forward and reverse operations
|
|
110
|
-
4. **No external dependencies** -
|
|
203
|
+
4. **No external dependencies** - No need for inflection libraries like `dry-inflector`
|
|
111
204
|
5. **Predictable** - Easy to remember and document
|
|
112
205
|
|
|
113
206
|
**Trade-off:**
|
|
114
207
|
- Slightly more verbose, but eliminates an entire class of edge case bugs
|
|
115
208
|
|
|
116
|
-
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## The `method_prefix:` Option
|
|
212
|
+
|
|
213
|
+
The default implementation uses `config_name` which can be verbose for namespaced
|
|
214
|
+
classes (e.g., `admin_project_team_instances`). The `method_prefix:` option provides
|
|
215
|
+
explicit control over reverse method naming:
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
participates_in Admin::ProjectTeam, :members, method_prefix: :team
|
|
219
|
+
# Generates: user.team_instances instead of user.admin_project_team_instances
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Parameter Priority
|
|
223
|
+
|
|
224
|
+
When multiple naming options are provided, the most specific wins:
|
|
225
|
+
|
|
226
|
+
| Parameter | Scope | Priority |
|
|
227
|
+
|-----------|-------|----------|
|
|
228
|
+
| `as:` | Single collection only | 1 (highest) |
|
|
229
|
+
| `method_prefix:` | All collections for target class | 2 |
|
|
230
|
+
| (default) | Uses `config_name` | 3 (lowest) |
|
|
231
|
+
|
|
232
|
+
### Examples
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
# Default: uses config_name
|
|
236
|
+
participates_in Admin::ProjectTeam, :members
|
|
237
|
+
# → user.admin_project_team_instances, user.admin_project_team_ids, etc.
|
|
238
|
+
|
|
239
|
+
# With method_prefix: shorter, cleaner names
|
|
240
|
+
participates_in Admin::ProjectTeam, :members, method_prefix: :team
|
|
241
|
+
# → user.team_instances, user.team_ids, user.team?, user.team_count
|
|
242
|
+
|
|
243
|
+
# With as: overrides for specific collection only
|
|
244
|
+
participates_in Admin::ProjectTeam, :members, as: :my_teams
|
|
245
|
+
# → user.my_teams_instances, user.my_teams_ids, etc.
|
|
246
|
+
|
|
247
|
+
# Both as: and method_prefix: - as: wins (more specific)
|
|
248
|
+
participates_in Admin::ProjectTeam, :members, method_prefix: :team, as: :my_teams
|
|
249
|
+
# → user.my_teams_instances (as: takes precedence)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
This gives developers explicit control over method naming while maintaining
|
|
253
|
+
the predictability of the default system.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Key Requirements (All Implemented)
|
|
117
258
|
|
|
118
|
-
1. Automatic generation - No manual method definitions needed
|
|
119
|
-
2. Multiple collections - Union of all collections to same target class
|
|
120
|
-
3. Performance - Efficient ID-only access without loading objects
|
|
121
|
-
4. Custom naming - Override auto-generated names
|
|
122
|
-
5. Thread-safe - No caching means no stale data or cache invalidation complexity
|
|
259
|
+
1. **Automatic generation** - No manual method definitions needed ✓
|
|
260
|
+
2. **Multiple collections** - Union of all collections to same target class ✓
|
|
261
|
+
3. **Performance** - Efficient ID-only access without loading objects ✓
|
|
262
|
+
4. **Custom naming** - Override auto-generated names via `as:` or `method_prefix:` parameters ✓
|
|
263
|
+
5. **Thread-safe** - No caching means no stale data or cache invalidation complexity ✓
|
|
264
|
+
6. **Through models** - Rich join data via `:through` option ✓
|
|
123
265
|
|
|
124
|
-
Benefits
|
|
266
|
+
## Benefits
|
|
125
267
|
|
|
126
|
-
- Symmetry - Both directions equally convenient
|
|
127
|
-
- Discoverability - Natural Ruby method names
|
|
128
|
-
- Efficiency - Choose between full objects, IDs, or counts
|
|
129
|
-
- Backwards compatible - All existing code continues to work
|
|
268
|
+
- **Symmetry** - Both directions equally convenient
|
|
269
|
+
- **Discoverability** - Natural Ruby method names
|
|
270
|
+
- **Efficiency** - Choose between full objects, IDs, or counts
|
|
271
|
+
- **Backwards compatible** - All existing code continues to work
|
|
272
|
+
- **Extensible** - Through models enable rich membership data
|