anima-core 0.3.0 → 1.0.0
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/.reek.yml +27 -1
- data/CHANGELOG.md +4 -0
- data/README.md +219 -25
- data/agents/codebase-analyzer.md +88 -0
- data/agents/codebase-pattern-finder.md +83 -0
- data/agents/documentation-researcher.md +59 -0
- data/agents/thoughts-analyzer.md +102 -0
- data/agents/web-search-researcher.md +71 -0
- data/anima-core.gemspec +3 -0
- data/app/channels/session_channel.rb +76 -28
- data/app/jobs/agent_request_job.rb +24 -0
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +1 -1
- data/app/models/concerns/event/broadcasting.rb +20 -2
- data/app/models/event.rb +1 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +347 -22
- data/config/application.rb +2 -0
- data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
- data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
- data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
- data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
- data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
- data/db/migrate/20260315140843_create_goals.rb +16 -0
- data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
- data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
- data/lib/agent_loop.rb +65 -9
- data/lib/agents/definition.rb +116 -0
- data/lib/agents/registry.rb +106 -0
- data/lib/analytical_brain/runner.rb +276 -0
- data/lib/analytical_brain/tools/activate_skill.rb +52 -0
- data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
- data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
- data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
- data/lib/analytical_brain/tools/finish_goal.rb +62 -0
- data/lib/analytical_brain/tools/read_workflow.rb +58 -0
- data/lib/analytical_brain/tools/rename_session.rb +63 -0
- data/lib/analytical_brain/tools/set_goal.rb +60 -0
- data/lib/analytical_brain/tools/update_goal.rb +60 -0
- data/lib/analytical_brain.rb +23 -0
- data/lib/anima/cli/mcp/secrets.rb +76 -0
- data/lib/anima/cli/mcp.rb +197 -0
- data/lib/anima/cli.rb +4 -0
- data/lib/anima/installer.rb +168 -0
- data/lib/anima/settings.rb +226 -0
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +9 -0
- data/lib/credential_store.rb +103 -0
- data/lib/environment_probe.rb +232 -0
- data/lib/llm/client.rb +29 -10
- data/lib/mcp/client_manager.rb +86 -0
- data/lib/mcp/config.rb +213 -0
- data/lib/mcp/health_check.rb +77 -0
- data/lib/mcp/secrets.rb +73 -0
- data/lib/mcp/stdio_transport.rb +206 -0
- data/lib/providers/anthropic.rb +8 -7
- data/lib/shell_session.rb +11 -10
- data/lib/skills/definition.rb +97 -0
- data/lib/skills/registry.rb +105 -0
- data/lib/tools/edit.rb +3 -4
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +15 -16
- data/lib/tools/registry.rb +14 -12
- data/lib/tools/request_feature.rb +121 -0
- data/lib/tools/return_result.rb +81 -0
- data/lib/tools/spawn_specialist.rb +109 -0
- data/lib/tools/spawn_subagent.rb +111 -0
- data/lib/tools/subagent_prompts.rb +12 -0
- data/lib/tools/web_get.rb +8 -9
- data/lib/tui/app.rb +332 -43
- data/lib/tui/message_store.rb +20 -0
- data/lib/tui/screens/chat.rb +207 -20
- data/lib/workflows/definition.rb +97 -0
- data/lib/workflows/registry.rb +89 -0
- data/skills/activerecord/SKILL.md +255 -0
- data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
- data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
- data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
- data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
- data/skills/activerecord/examples/associations/self_referential.rb +302 -0
- data/skills/activerecord/examples/associations/through_associations.rb +203 -0
- data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
- data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
- data/skills/activerecord/examples/basics/inheritance.rb +377 -0
- data/skills/activerecord/examples/basics/type_casting.rb +317 -0
- data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
- data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
- data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
- data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
- data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
- data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
- data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
- data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
- data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
- data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
- data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
- data/skills/activerecord/examples/querying/optimization.rb +275 -0
- data/skills/activerecord/examples/querying/scopes.rb +260 -0
- data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
- data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
- data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
- data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
- data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
- data/skills/activerecord/references/associations.md +709 -0
- data/skills/activerecord/references/basics.md +622 -0
- data/skills/activerecord/references/callbacks.md +738 -0
- data/skills/activerecord/references/migrations.md +657 -0
- data/skills/activerecord/references/querying.md +655 -0
- data/skills/activerecord/references/validations.md +596 -0
- data/skills/dragonruby/SKILL.md +250 -0
- data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
- data/skills/dragonruby/examples/audio/background_music.rb +29 -0
- data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
- data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
- data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
- data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
- data/skills/dragonruby/examples/core/hello_world.rb +24 -0
- data/skills/dragonruby/examples/core/labels.rb +22 -0
- data/skills/dragonruby/examples/core/sprites.rb +35 -0
- data/skills/dragonruby/examples/core/state_management.rb +29 -0
- data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
- data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
- data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
- data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
- data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
- data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
- data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
- data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
- data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
- data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
- data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
- data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
- data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
- data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
- data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
- data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
- data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
- data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
- data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
- data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
- data/skills/dragonruby/examples/input/controller_input.rb +28 -0
- data/skills/dragonruby/examples/input/directional_input.rb +24 -0
- data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
- data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
- data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
- data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
- data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
- data/skills/dragonruby/examples/rendering/labels.rb +32 -0
- data/skills/dragonruby/examples/rendering/layering.rb +51 -0
- data/skills/dragonruby/examples/rendering/solids.rb +61 -0
- data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
- data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
- data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
- data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
- data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
- data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
- data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
- data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
- data/skills/dragonruby/references/audio.md +396 -0
- data/skills/dragonruby/references/core.md +385 -0
- data/skills/dragonruby/references/distribution.md +434 -0
- data/skills/dragonruby/references/entities.md +516 -0
- data/skills/dragonruby/references/game-logic/persistence.md +386 -0
- data/skills/dragonruby/references/game-logic/state.md +389 -0
- data/skills/dragonruby/references/input.md +414 -0
- data/skills/dragonruby/references/rendering/animation.md +467 -0
- data/skills/dragonruby/references/rendering/primitives.md +403 -0
- data/skills/dragonruby/references/scenes.md +443 -0
- data/skills/draper-decorators/SKILL.md +344 -0
- data/skills/draper-decorators/examples/application_decorator.rb +61 -0
- data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
- data/skills/draper-decorators/examples/model_decorator.rb +152 -0
- data/skills/draper-decorators/references/anti-patterns.md +640 -0
- data/skills/draper-decorators/references/patterns.md +507 -0
- data/skills/draper-decorators/references/testing.md +559 -0
- data/skills/gh-issue.md +182 -0
- data/skills/mcp-server/SKILL.md +177 -0
- data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
- data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
- data/skills/mcp-server/examples/http_client.rb +48 -0
- data/skills/mcp-server/examples/http_server.rb +97 -0
- data/skills/mcp-server/examples/rails_integration.rb +88 -0
- data/skills/mcp-server/examples/stdio_server.rb +108 -0
- data/skills/mcp-server/examples/streaming_client.rb +95 -0
- data/skills/mcp-server/references/gotchas.md +183 -0
- data/skills/mcp-server/references/prompts.md +98 -0
- data/skills/mcp-server/references/resources.md +53 -0
- data/skills/mcp-server/references/server.md +140 -0
- data/skills/mcp-server/references/tools.md +146 -0
- data/skills/mcp-server/references/transport.md +104 -0
- data/skills/ratatui-ruby/SKILL.md +315 -0
- data/skills/ratatui-ruby/references/core-concepts.md +340 -0
- data/skills/ratatui-ruby/references/events.md +387 -0
- data/skills/ratatui-ruby/references/frameworks.md +522 -0
- data/skills/ratatui-ruby/references/layout.md +423 -0
- data/skills/ratatui-ruby/references/styling.md +268 -0
- data/skills/ratatui-ruby/references/testing.md +433 -0
- data/skills/ratatui-ruby/references/widgets.md +532 -0
- data/skills/rspec/SKILL.md +340 -0
- data/skills/rspec/examples/core/basic_structure.rb +69 -0
- data/skills/rspec/examples/core/configuration.rb +126 -0
- data/skills/rspec/examples/core/hooks.rb +126 -0
- data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
- data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
- data/skills/rspec/examples/core/shared_examples.rb +145 -0
- data/skills/rspec/examples/factory_bot/associations.rb +314 -0
- data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
- data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
- data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
- data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
- data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
- data/skills/rspec/examples/factory_bot/traits.rb +293 -0
- data/skills/rspec/examples/factory_bot/transients.rb +229 -0
- data/skills/rspec/examples/matchers/change.rb +115 -0
- data/skills/rspec/examples/matchers/collections.rb +154 -0
- data/skills/rspec/examples/matchers/comparisons.rb +79 -0
- data/skills/rspec/examples/matchers/composing.rb +155 -0
- data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
- data/skills/rspec/examples/matchers/equality.rb +58 -0
- data/skills/rspec/examples/matchers/errors.rb +136 -0
- data/skills/rspec/examples/matchers/output.rb +103 -0
- data/skills/rspec/examples/matchers/predicates.rb +87 -0
- data/skills/rspec/examples/matchers/truthiness.rb +101 -0
- data/skills/rspec/examples/matchers/types.rb +82 -0
- data/skills/rspec/examples/matchers/yield.rb +147 -0
- data/skills/rspec/examples/mocks/any_instance.rb +172 -0
- data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
- data/skills/rspec/examples/mocks/constants.rb +177 -0
- data/skills/rspec/examples/mocks/doubles.rb +139 -0
- data/skills/rspec/examples/mocks/expectations.rb +137 -0
- data/skills/rspec/examples/mocks/message_chains.rb +173 -0
- data/skills/rspec/examples/mocks/ordering.rb +144 -0
- data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
- data/skills/rspec/examples/mocks/responses.rb +223 -0
- data/skills/rspec/examples/mocks/spies.rb +149 -0
- data/skills/rspec/examples/mocks/stubbing.rb +133 -0
- data/skills/rspec/examples/rails/channels.rb +250 -0
- data/skills/rspec/examples/rails/controller_specs.rb +302 -0
- data/skills/rspec/examples/rails/helper_specs.rb +245 -0
- data/skills/rspec/examples/rails/job_specs.rb +256 -0
- data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
- data/skills/rspec/examples/rails/matchers.rb +374 -0
- data/skills/rspec/examples/rails/model_specs.rb +193 -0
- data/skills/rspec/examples/rails/request_specs.rb +275 -0
- data/skills/rspec/examples/rails/routing_specs.rb +276 -0
- data/skills/rspec/examples/rails/system_specs.rb +294 -0
- data/skills/rspec/examples/rails/transactions.rb +254 -0
- data/skills/rspec/examples/rails/view_specs.rb +252 -0
- data/skills/rspec/references/core.md +816 -0
- data/skills/rspec/references/factory_bot.md +641 -0
- data/skills/rspec/references/matchers.md +516 -0
- data/skills/rspec/references/mocks.md +381 -0
- data/skills/rspec/references/rails.md +528 -0
- data/templates/soul.md +40 -0
- data/workflows/commit.md +45 -0
- data/workflows/create_handoff.md +98 -0
- data/workflows/create_note.md +82 -0
- data/workflows/create_plan.md +457 -0
- data/workflows/decompose_ticket.md +109 -0
- data/workflows/feature.md +91 -0
- data/workflows/implement_plan.md +87 -0
- data/workflows/iterate_plan.md +247 -0
- data/workflows/research_codebase.md +210 -0
- data/workflows/resume_handoff.md +217 -0
- data/workflows/review_pr.md +320 -0
- data/workflows/thoughts_init.md +71 -0
- data/workflows/validate_plan.md +166 -0
- metadata +284 -1
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
# ActiveRecord Associations Reference
|
|
2
|
+
|
|
3
|
+
## Association Types Overview
|
|
4
|
+
|
|
5
|
+
### belongs_to - Child Side
|
|
6
|
+
|
|
7
|
+
The declaring model contains the foreign key. Use singular form.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class Book < ApplicationRecord
|
|
11
|
+
belongs_to :author # Required by default (Rails 5+)
|
|
12
|
+
belongs_to :publisher, optional: true # Allow NULL foreign key
|
|
13
|
+
belongs_to :category, class_name: "Genre", foreign_key: "genre_id"
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Key Options:**
|
|
18
|
+
- `optional: true` - Allow NULL foreign key
|
|
19
|
+
- `counter_cache: true` - Maintain count on parent (column goes on parent)
|
|
20
|
+
- `touch: true` - Update parent's `updated_at` on save
|
|
21
|
+
- `inverse_of: :association` - Bi-directional reference (required with custom FK)
|
|
22
|
+
- `polymorphic: true` - Belong to multiple model types
|
|
23
|
+
|
|
24
|
+
**Migration:**
|
|
25
|
+
```ruby
|
|
26
|
+
create_table :books do |t|
|
|
27
|
+
t.belongs_to :author, null: false, foreign_key: true
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
### has_one - Parent Side One-to-One
|
|
34
|
+
|
|
35
|
+
Another model has a reference to this model. Foreign key is on the other table.
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
class Supplier < ApplicationRecord
|
|
39
|
+
has_one :account, dependent: :destroy
|
|
40
|
+
has_one :representative, class_name: "Person"
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Critical Warning:** Rails does NOT enforce 1:1 at database level. Add unique index:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
# Migration - enforce true 1:1
|
|
48
|
+
add_index :accounts, :supplier_id, unique: true
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Key Options:**
|
|
52
|
+
- `dependent: :destroy` - Destroy associated when parent destroyed
|
|
53
|
+
- `as: :attachable` - Polymorphic target
|
|
54
|
+
- `through: :other_association` - Through another association
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
### has_many - One-to-Many
|
|
59
|
+
|
|
60
|
+
This model has multiple instances of another model.
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
class Author < ApplicationRecord
|
|
64
|
+
has_many :books, dependent: :destroy
|
|
65
|
+
has_many :published_books, -> { where(published: true) }, class_name: "Book"
|
|
66
|
+
has_many :chapters, through: :books
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Key Options:**
|
|
71
|
+
- `dependent:` - Cascade behavior (see Dependent Options below)
|
|
72
|
+
- `counter_cache:` - Custom column name for count
|
|
73
|
+
- `inverse_of:` - Bi-directional reference
|
|
74
|
+
- `through:` - Many-to-many via join model
|
|
75
|
+
|
|
76
|
+
**Collection Methods:**
|
|
77
|
+
```ruby
|
|
78
|
+
author.books # Returns Relation
|
|
79
|
+
author.books.build # Create unsaved
|
|
80
|
+
author.books.create # Create and save
|
|
81
|
+
author.books << book # Add (saves immediately!)
|
|
82
|
+
author.book_ids # Array of IDs
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
### has_and_belongs_to_many - Direct Many-to-Many
|
|
88
|
+
|
|
89
|
+
Simple many-to-many without join model. **Prefer `has_many :through` instead.**
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
class Assembly < ApplicationRecord
|
|
93
|
+
has_and_belongs_to_many :parts
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
class Part < ApplicationRecord
|
|
97
|
+
has_and_belongs_to_many :assemblies
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Migration (no primary key):**
|
|
102
|
+
```ruby
|
|
103
|
+
create_table :assemblies_parts, id: false do |t|
|
|
104
|
+
t.belongs_to :assembly, foreign_key: true
|
|
105
|
+
t.belongs_to :part, foreign_key: true
|
|
106
|
+
end
|
|
107
|
+
add_index :assemblies_parts, [:assembly_id, :part_id], unique: true
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Join Table Naming:** Lexically ordered - `papers_paper_boxes` not `paper_boxes_papers`. Use explicit `:join_table` to avoid surprises.
|
|
111
|
+
|
|
112
|
+
**Critical:** Declare `has_and_belongs_to_many` BEFORE `self.table_name =` in model.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Through Associations
|
|
117
|
+
|
|
118
|
+
### has_many :through
|
|
119
|
+
|
|
120
|
+
Many-to-many with join model. **Always prefer over HABTM.**
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
124
|
+
│ Physician │ │ Appointment │ │ Patient │
|
|
125
|
+
├──────────────┤ ├──────────────┤ ├──────────────┤
|
|
126
|
+
│ id │◄──────│ physician_id │ │ id │
|
|
127
|
+
│ name │ │ patient_id │──────►│ name │
|
|
128
|
+
│ │ │ scheduled_at │ │ │
|
|
129
|
+
└──────────────┘ └──────────────┘ └──────────────┘
|
|
130
|
+
│ ▲ │
|
|
131
|
+
│ has_many │ has_many │
|
|
132
|
+
└─────:through────────┴──────:through───────┘
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
class Physician < ApplicationRecord
|
|
137
|
+
has_many :appointments
|
|
138
|
+
has_many :patients, through: :appointments
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
class Appointment < ApplicationRecord
|
|
142
|
+
belongs_to :physician
|
|
143
|
+
belongs_to :patient
|
|
144
|
+
|
|
145
|
+
# Join model can have attributes and validations
|
|
146
|
+
validates :scheduled_at, presence: true
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
class Patient < ApplicationRecord
|
|
150
|
+
has_many :appointments
|
|
151
|
+
has_many :physicians, through: :appointments
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Advantages over HABTM:**
|
|
156
|
+
- Add attributes/validations to join model
|
|
157
|
+
- Store metadata (timestamps, status)
|
|
158
|
+
- Add callbacks to relationship changes
|
|
159
|
+
- Easier to extend later
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
### has_one :through
|
|
164
|
+
|
|
165
|
+
Access single record through intermediate association.
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
169
|
+
│ Supplier │ │ Account │ │ AccountHistory│
|
|
170
|
+
├──────────────┤ ├──────────────┤ ├──────────────┤
|
|
171
|
+
│ id │◄──────│ supplier_id │ │ id │
|
|
172
|
+
│ name │ │ id │◄──────│ account_id │
|
|
173
|
+
│ │ │ │ │ credit_rating│
|
|
174
|
+
└──────────────┘ └──────────────┘ └──────────────┘
|
|
175
|
+
│ │
|
|
176
|
+
│ has_one :through │
|
|
177
|
+
└─────────────────────┘
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
class Supplier < ApplicationRecord
|
|
182
|
+
has_one :account
|
|
183
|
+
has_one :account_history, through: :account
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
class Account < ApplicationRecord
|
|
187
|
+
belongs_to :supplier
|
|
188
|
+
has_one :account_history
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
class AccountHistory < ApplicationRecord
|
|
192
|
+
belongs_to :account
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
### Through Association Writability
|
|
199
|
+
|
|
200
|
+
**Critical Rule:** `:through` associations are only writable when join model uses `belongs_to`.
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
# WORKS - join model has belongs_to
|
|
204
|
+
class Tagging < ApplicationRecord
|
|
205
|
+
belongs_to :post
|
|
206
|
+
belongs_to :tag
|
|
207
|
+
end
|
|
208
|
+
post.tags << tag # Creates Tagging record
|
|
209
|
+
|
|
210
|
+
# READ-ONLY - join model has has_one/has_many
|
|
211
|
+
class Group < ApplicationRecord
|
|
212
|
+
has_many :users
|
|
213
|
+
has_many :avatars, through: :users # Read-only!
|
|
214
|
+
end
|
|
215
|
+
group.avatars << avatar # WON'T WORK
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Solution:** Manipulate the `:through` association directly.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Polymorphic Associations
|
|
223
|
+
|
|
224
|
+
Single association points to multiple model types.
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
228
|
+
│ Employee │ │ Picture │ │ Product │
|
|
229
|
+
├──────────────┤ ├──────────────┤ ├──────────────┤
|
|
230
|
+
│ id │◄──┐ │ id │ ┌──►│ id │
|
|
231
|
+
│ name │ │ │ imageable_type│ │ │ name │
|
|
232
|
+
│ │ └───│ imageable_id │───┘ │ │
|
|
233
|
+
└──────────────┘ │ name │ └──────────────┘
|
|
234
|
+
└──────────────┘
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
class Picture < ApplicationRecord
|
|
239
|
+
belongs_to :imageable, polymorphic: true
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
class Employee < ApplicationRecord
|
|
243
|
+
has_many :pictures, as: :imageable
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
class Product < ApplicationRecord
|
|
247
|
+
has_many :pictures, as: :imageable
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Migration:**
|
|
252
|
+
```ruby
|
|
253
|
+
create_table :pictures do |t|
|
|
254
|
+
t.string :name
|
|
255
|
+
t.belongs_to :imageable, polymorphic: true
|
|
256
|
+
t.timestamps
|
|
257
|
+
end
|
|
258
|
+
# Creates: imageable_type (string), imageable_id (integer)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Naming Convention:**
|
|
262
|
+
- Use `-able` suffix when association is recipient: `imageable`, `attachable`, `commentable`
|
|
263
|
+
- Use subject form when acting: `author` not `authorable`
|
|
264
|
+
|
|
265
|
+
**STI Compatibility Warning:**
|
|
266
|
+
When using polymorphic with STI, store base class in type column:
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
class Asset < ApplicationRecord
|
|
270
|
+
belongs_to :attachable, polymorphic: true
|
|
271
|
+
|
|
272
|
+
def attachable_type=(class_name)
|
|
273
|
+
super(class_name.constantize.base_class.to_s)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Limitations:**
|
|
279
|
+
- Cannot use database foreign key constraints
|
|
280
|
+
- Cannot use `joins`, only `includes` for eager loading
|
|
281
|
+
- When renaming models, must update `*_type` column values
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Self-Referential Associations
|
|
286
|
+
|
|
287
|
+
Model relates to itself (hierarchies, trees, graphs).
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
class Employee < ApplicationRecord
|
|
291
|
+
# Employee has one manager (who is also an Employee)
|
|
292
|
+
belongs_to :manager, class_name: "Employee", optional: true
|
|
293
|
+
|
|
294
|
+
# Employee has many subordinates (who are also Employees)
|
|
295
|
+
has_many :subordinates, class_name: "Employee", foreign_key: "manager_id"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Usage
|
|
299
|
+
employee.manager # Returns manager Employee
|
|
300
|
+
employee.subordinates # Returns Employee collection
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Migration:**
|
|
304
|
+
```ruby
|
|
305
|
+
create_table :employees do |t|
|
|
306
|
+
t.string :name
|
|
307
|
+
t.belongs_to :manager, foreign_key: { to_table: :employees }
|
|
308
|
+
t.timestamps
|
|
309
|
+
end
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Friendship Example (Many-to-Many Self-Join):**
|
|
313
|
+
```ruby
|
|
314
|
+
class User < ApplicationRecord
|
|
315
|
+
has_many :friendships
|
|
316
|
+
has_many :friends, through: :friendships
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
class Friendship < ApplicationRecord
|
|
320
|
+
belongs_to :user
|
|
321
|
+
belongs_to :friend, class_name: "User"
|
|
322
|
+
end
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Critical Association Options
|
|
328
|
+
|
|
329
|
+
### inverse_of - Bi-directional References
|
|
330
|
+
|
|
331
|
+
Tells Rails two associations represent same relationship from different sides.
|
|
332
|
+
|
|
333
|
+
**When Required:**
|
|
334
|
+
1. With custom `:foreign_key`
|
|
335
|
+
2. On `:through` join model associations
|
|
336
|
+
3. With `accepts_nested_attributes_for`
|
|
337
|
+
|
|
338
|
+
```ruby
|
|
339
|
+
class Author < ApplicationRecord
|
|
340
|
+
has_many :books, inverse_of: :author
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
class Book < ApplicationRecord
|
|
344
|
+
belongs_to :author, inverse_of: :books
|
|
345
|
+
end
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
**Why It Matters:**
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
# WITHOUT inverse_of - extra query
|
|
352
|
+
author = Author.first
|
|
353
|
+
book = author.books.first
|
|
354
|
+
book.author.name # Queries DB again!
|
|
355
|
+
|
|
356
|
+
# WITH inverse_of - no extra query
|
|
357
|
+
book.author.object_id == author.object_id # Same object
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**accepts_nested_attributes_for Requirement:**
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
class Notice < ApplicationRecord
|
|
364
|
+
has_many :entity_roles, inverse_of: :notice # REQUIRED!
|
|
365
|
+
accepts_nested_attributes_for :entity_roles
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
class EntityRole < ApplicationRecord
|
|
369
|
+
belongs_to :notice
|
|
370
|
+
validates :notice, presence: true # Fails without inverse_of
|
|
371
|
+
end
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Without `inverse_of`, Rails can't assign parent before validation, causing "can't be blank" errors.
|
|
375
|
+
|
|
376
|
+
**Automatic Detection Limitations:**
|
|
377
|
+
Rails auto-detects `:inverse_of` for simple associations but NOT when using:
|
|
378
|
+
- `:foreign_key` option
|
|
379
|
+
- `:through` option
|
|
380
|
+
- Custom scopes
|
|
381
|
+
- Non-standard naming
|
|
382
|
+
|
|
383
|
+
**Best Practice:** Always set `inverse_of` when using custom `:foreign_key`.
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
### dependent - Cascade Deletion
|
|
388
|
+
|
|
389
|
+
Controls what happens to associated records when parent is destroyed.
|
|
390
|
+
|
|
391
|
+
| Option | Behavior | Callbacks | Speed |
|
|
392
|
+
|--------|----------|-----------|-------|
|
|
393
|
+
| `:destroy` | Destroy each record | Yes | Slow |
|
|
394
|
+
| `:delete_all` | SQL DELETE (no load) | No | Fast |
|
|
395
|
+
| `:nullify` | Set FK to NULL | No | Fast |
|
|
396
|
+
| `:restrict_with_exception` | Raise if any exist | N/A | N/A |
|
|
397
|
+
| `:restrict_with_error` | Add error if any exist | N/A | N/A |
|
|
398
|
+
| `:destroy_async` | Background job destroy | Yes | Async |
|
|
399
|
+
|
|
400
|
+
**Decision Matrix:**
|
|
401
|
+
|
|
402
|
+
| Use Case | Option |
|
|
403
|
+
|----------|--------|
|
|
404
|
+
| Standard cascade | `:destroy` |
|
|
405
|
+
| No child callbacks needed | `:delete_all` |
|
|
406
|
+
| Keep orphan records | `:nullify` |
|
|
407
|
+
| Prevent accidental deletion | `:restrict_with_exception` |
|
|
408
|
+
| Large-scale deletions | `:destroy_async` |
|
|
409
|
+
|
|
410
|
+
**Warning - delete_all Breaks Grandchildren:**
|
|
411
|
+
|
|
412
|
+
```ruby
|
|
413
|
+
class Parent < ApplicationRecord
|
|
414
|
+
has_many :children, dependent: :delete_all
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
class Child < ApplicationRecord
|
|
418
|
+
has_many :grandchildren, dependent: :destroy
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
parent.destroy # Deletes children, ORPHANS grandchildren!
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Warning - destroy_async + FK Constraints:**
|
|
425
|
+
Do NOT use `:destroy_async` with database foreign key constraints. FK actions occur in same transaction, but async job runs later causing violations.
|
|
426
|
+
|
|
427
|
+
**Orphan-Then-Purge Pattern (Large Datasets):**
|
|
428
|
+
|
|
429
|
+
```ruby
|
|
430
|
+
class Blog < ApplicationRecord
|
|
431
|
+
has_many :posts, dependent: :nullify # Fast orphaning
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Background job cleans up
|
|
435
|
+
Post.where(blog_id: nil).find_each(&:destroy) # Proper callbacks
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**Scoped Association Warning:**
|
|
439
|
+
|
|
440
|
+
```ruby
|
|
441
|
+
has_many :comments, -> { where(published: true) }, dependent: :destroy
|
|
442
|
+
```
|
|
443
|
+
Only published comments destroyed - unpublished become orphans!
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
### counter_cache - Count Optimization
|
|
448
|
+
|
|
449
|
+
Caches association count to eliminate COUNT queries.
|
|
450
|
+
|
|
451
|
+
```ruby
|
|
452
|
+
class Comment < ApplicationRecord
|
|
453
|
+
belongs_to :post, counter_cache: true
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
class Post < ApplicationRecord
|
|
457
|
+
has_many :comments
|
|
458
|
+
attr_readonly :comments_count # Prevent manual updates
|
|
459
|
+
end
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**Migration:**
|
|
463
|
+
```ruby
|
|
464
|
+
add_column :posts, :comments_count, :integer, default: 0, null: false
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
**Backfilling Existing Data:**
|
|
468
|
+
|
|
469
|
+
```ruby
|
|
470
|
+
# Option 1: Simple reset
|
|
471
|
+
Post.find_each { |post| Post.reset_counters(post.id, :comments) }
|
|
472
|
+
|
|
473
|
+
# Option 2: For large tables, disable during backfill
|
|
474
|
+
belongs_to :post, counter_cache: { active: false }
|
|
475
|
+
# After backfill complete, change to:
|
|
476
|
+
belongs_to :post, counter_cache: true
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
**Custom Column Name:**
|
|
480
|
+
```ruby
|
|
481
|
+
belongs_to :post, counter_cache: :my_comments_count
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
**Gotchas:**
|
|
485
|
+
- Only updates via callbacks (`.delete` bypasses)
|
|
486
|
+
- Doesn't support scoped counts (use `counter_culture` gem)
|
|
487
|
+
- Consider database triggers for high-write scenarios
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
### autosave - Automatic Associated Saving
|
|
492
|
+
|
|
493
|
+
Controls when associated records are saved with parent.
|
|
494
|
+
|
|
495
|
+
| Setting | Behavior |
|
|
496
|
+
|---------|----------|
|
|
497
|
+
| Not specified | Save new records only |
|
|
498
|
+
| `true` | Save new AND updated records |
|
|
499
|
+
| `false` | Never auto-save |
|
|
500
|
+
|
|
501
|
+
```ruby
|
|
502
|
+
class Author < ApplicationRecord
|
|
503
|
+
has_one :profile, autosave: true
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
author = Author.new(name: "Jane")
|
|
507
|
+
author.build_profile(bio: "Writer")
|
|
508
|
+
author.save # Saves both author AND profile
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
**accepts_nested_attributes_for Auto-Enables:**
|
|
512
|
+
```ruby
|
|
513
|
+
accepts_nested_attributes_for :books # Sets autosave: true automatically
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
**Callback Order Warning:**
|
|
517
|
+
Autosave defines callbacks. Define associations BEFORE custom callbacks:
|
|
518
|
+
|
|
519
|
+
```ruby
|
|
520
|
+
class Author < ApplicationRecord
|
|
521
|
+
has_many :books, autosave: true # First
|
|
522
|
+
before_save :do_something # Second - runs after autosave setup
|
|
523
|
+
end
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## Association Extensions
|
|
529
|
+
|
|
530
|
+
Add custom methods to association proxies.
|
|
531
|
+
|
|
532
|
+
```ruby
|
|
533
|
+
class Project < ApplicationRecord
|
|
534
|
+
has_many :tasks do
|
|
535
|
+
def active
|
|
536
|
+
where(status: 'active')
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def by_priority
|
|
540
|
+
order(priority: :desc)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def total_hours
|
|
544
|
+
sum(:estimated_hours)
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# Usage
|
|
550
|
+
project.tasks.active.by_priority
|
|
551
|
+
project.tasks.total_hours
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
**Shared Extensions via Module:**
|
|
555
|
+
|
|
556
|
+
```ruby
|
|
557
|
+
module StatusFilter
|
|
558
|
+
def active
|
|
559
|
+
where(status: 'active')
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def completed
|
|
563
|
+
where(status: 'completed')
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
class Project < ApplicationRecord
|
|
568
|
+
has_many :tasks, -> { extending StatusFilter }
|
|
569
|
+
has_many :milestones, -> { extending StatusFilter }
|
|
570
|
+
end
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
**Accessing Parent Object:**
|
|
574
|
+
```ruby
|
|
575
|
+
has_many :tasks do
|
|
576
|
+
def recent_for_owner
|
|
577
|
+
where("created_at > ?", proxy_association.owner.created_at)
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
## N+1 Query Prevention
|
|
585
|
+
|
|
586
|
+
### Eager Loading Methods
|
|
587
|
+
|
|
588
|
+
| Method | Strategy | Use When |
|
|
589
|
+
|--------|----------|----------|
|
|
590
|
+
| `includes` | Auto-choose | Default choice |
|
|
591
|
+
| `preload` | Separate queries | Large datasets, no filtering |
|
|
592
|
+
| `eager_load` | LEFT OUTER JOIN | Filtering/sorting by association |
|
|
593
|
+
| `joins` | INNER JOIN | Filtering only, not accessing data |
|
|
594
|
+
|
|
595
|
+
**Examples:**
|
|
596
|
+
|
|
597
|
+
```ruby
|
|
598
|
+
# includes - smart default (Rails decides preload vs eager_load)
|
|
599
|
+
Author.includes(:books).each { |a| a.books.size }
|
|
600
|
+
|
|
601
|
+
# preload - always separate queries
|
|
602
|
+
Author.preload(:books).limit(10) # 2 queries total
|
|
603
|
+
|
|
604
|
+
# eager_load - always single JOIN
|
|
605
|
+
Author.eager_load(:books).where(books: { published: true })
|
|
606
|
+
|
|
607
|
+
# joins - filtering only (doesn't load association)
|
|
608
|
+
Author.joins(:books).where(books: { published: true }).distinct
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
**Strict Loading (Rails 6.1+):**
|
|
612
|
+
```ruby
|
|
613
|
+
Author.strict_loading.first
|
|
614
|
+
author.books # Raises StrictLoadingViolationError!
|
|
615
|
+
|
|
616
|
+
# Per-association
|
|
617
|
+
has_many :books, strict_loading: true
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
**Nested Eager Loading:**
|
|
621
|
+
```ruby
|
|
622
|
+
Author.includes(books: :publisher)
|
|
623
|
+
Author.includes(books: [:publisher, :reviews])
|
|
624
|
+
Author.includes(books: { publisher: :address })
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
## Anti-Patterns
|
|
630
|
+
|
|
631
|
+
### 1. Naming After AR Methods
|
|
632
|
+
```ruby
|
|
633
|
+
# BAD - conflicts with AR::Base methods
|
|
634
|
+
has_many :attributes
|
|
635
|
+
has_one :connection
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### 2. Relying on Validations for Uniqueness
|
|
639
|
+
```ruby
|
|
640
|
+
# BAD - race condition
|
|
641
|
+
validates :supplier, uniqueness: true
|
|
642
|
+
|
|
643
|
+
# GOOD - database constraint
|
|
644
|
+
add_index :accounts, :supplier_id, unique: true
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### 3. Callbacks for Business Logic
|
|
648
|
+
```ruby
|
|
649
|
+
# BAD - hidden side effects
|
|
650
|
+
after_create :send_email
|
|
651
|
+
after_update :sync_to_api
|
|
652
|
+
|
|
653
|
+
# GOOD - explicit service object
|
|
654
|
+
class ProjectCreator
|
|
655
|
+
def call(params)
|
|
656
|
+
project = Project.create!(params)
|
|
657
|
+
ProjectMailer.created(project).deliver_later
|
|
658
|
+
ExternalApi.sync(project)
|
|
659
|
+
project
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
### 4. HABTM Without Future Consideration
|
|
665
|
+
```ruby
|
|
666
|
+
# BAD - hard to extend later
|
|
667
|
+
has_and_belongs_to_many :tags
|
|
668
|
+
|
|
669
|
+
# GOOD - flexible from start
|
|
670
|
+
has_many :taggings
|
|
671
|
+
has_many :tags, through: :taggings
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### 5. Missing inverse_of with Custom FK
|
|
675
|
+
```ruby
|
|
676
|
+
# BAD - causes extra queries, validation failures
|
|
677
|
+
belongs_to :author, foreign_key: "writer_id"
|
|
678
|
+
|
|
679
|
+
# GOOD
|
|
680
|
+
belongs_to :author, foreign_key: "writer_id", inverse_of: :books
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### 6. Eager Loading with Limit
|
|
684
|
+
```ruby
|
|
685
|
+
# WARNING - limit ignored with includes!
|
|
686
|
+
has_many :recent_comments, -> { order(created_at: :desc).limit(5) }
|
|
687
|
+
Post.includes(:recent_comments).first.recent_comments # Returns ALL comments!
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## Naming Conventions
|
|
693
|
+
|
|
694
|
+
Follow GitLab style guide for scope naming:
|
|
695
|
+
|
|
696
|
+
| Pattern | Purpose | Example |
|
|
697
|
+
|---------|---------|---------|
|
|
698
|
+
| `for_*` | Filter by belongs_to | `scope :for_user, ->(u) { where(user: u) }` |
|
|
699
|
+
| `with_*` | Joins/eager load or filter has_* | `scope :with_comments, -> { joins(:comments) }` |
|
|
700
|
+
| `order_by_*` | Ordering | `scope :order_by_recent, -> { order(created_at: :desc) }` |
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
## See Also
|
|
705
|
+
|
|
706
|
+
- `examples/associations/` - Working code examples
|
|
707
|
+
- `references/querying.md` - Eager loading details
|
|
708
|
+
- `references/callbacks.md` - Callback alternatives
|
|
709
|
+
- `references/migrations.md` - Foreign key constraints
|