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,738 @@
|
|
|
1
|
+
# ActiveRecord Callbacks Reference
|
|
2
|
+
|
|
3
|
+
Comprehensive reference for model callbacks: lifecycle hooks, transaction callbacks, ordering, halting behavior, and when to use callbacks vs alternatives.
|
|
4
|
+
|
|
5
|
+
## Callback Execution Order
|
|
6
|
+
|
|
7
|
+
### Creating a Record
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
1. before_validation
|
|
11
|
+
2. after_validation
|
|
12
|
+
3. before_save
|
|
13
|
+
4. around_save (before yield)
|
|
14
|
+
5. before_create
|
|
15
|
+
6. around_create (before yield)
|
|
16
|
+
─── INSERT ───
|
|
17
|
+
7. around_create (after yield)
|
|
18
|
+
8. after_create
|
|
19
|
+
9. around_save (after yield)
|
|
20
|
+
10. after_save
|
|
21
|
+
─── COMMIT ───
|
|
22
|
+
11. after_commit / after_rollback
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Updating a Record
|
|
26
|
+
|
|
27
|
+
Same as create, but `before_update`, `around_update`, `after_update` replace create callbacks.
|
|
28
|
+
|
|
29
|
+
### Destroying a Record
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
1. before_destroy
|
|
33
|
+
2. around_destroy (before yield)
|
|
34
|
+
─── DELETE ───
|
|
35
|
+
3. around_destroy (after yield)
|
|
36
|
+
4. after_destroy
|
|
37
|
+
─── COMMIT ───
|
|
38
|
+
5. after_commit / after_rollback
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Special Callbacks
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
after_initialize # After new() or loading from DB
|
|
45
|
+
after_find # After loading from DB (before after_initialize)
|
|
46
|
+
after_touch # After touch() is called
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Callback Declaration
|
|
50
|
+
|
|
51
|
+
### Method Reference (Preferred)
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
class Article < ApplicationRecord
|
|
55
|
+
before_save :normalize_title
|
|
56
|
+
after_create :notify_subscribers
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def normalize_title
|
|
61
|
+
self.title = title.strip.titleize
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def notify_subscribers
|
|
65
|
+
NotificationJob.perform_later(id)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Inline Block
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
class User < ApplicationRecord
|
|
74
|
+
before_validation { self.email = email&.downcase&.strip }
|
|
75
|
+
|
|
76
|
+
after_create do |user|
|
|
77
|
+
AuditLog.create!(action: "user_created", record: user)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Callback Object
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
class AuditLogger
|
|
86
|
+
def after_create(record)
|
|
87
|
+
AuditLog.create!(action: "created", record:)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def after_update(record)
|
|
91
|
+
AuditLog.create!(action: "updated", record:, changes: record.previous_changes)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class Order < ApplicationRecord
|
|
96
|
+
after_create AuditLogger.new
|
|
97
|
+
after_update AuditLogger.new
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Conditional Callbacks
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# Symbol (method name) - preferred for readability
|
|
105
|
+
before_save :normalize_card_number, if: :paid_with_card?
|
|
106
|
+
|
|
107
|
+
# Proc/Lambda - for one-liners
|
|
108
|
+
after_create :send_welcome_email, if: -> { email.present? }
|
|
109
|
+
|
|
110
|
+
# Multiple conditions (all must be true)
|
|
111
|
+
before_validation :set_defaults, if: [:new_record?, :draft?]
|
|
112
|
+
|
|
113
|
+
# Using both :if and :unless
|
|
114
|
+
after_save :sync_to_search, if: :published?, unless: :skip_indexing?
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Halting the Callback Chain
|
|
118
|
+
|
|
119
|
+
Use `throw :abort` in `before_*` callbacks to halt execution:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
class Order < ApplicationRecord
|
|
123
|
+
before_save :check_stock
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def check_stock
|
|
128
|
+
throw(:abort) if items.any? { |item| item.out_of_stock? }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Behavior when halted:**
|
|
134
|
+
- `save` returns `false`
|
|
135
|
+
- `save!` raises `ActiveRecord::RecordNotSaved`
|
|
136
|
+
- `destroy` returns `false`
|
|
137
|
+
- `destroy!` raises `ActiveRecord::RecordNotDestroyed`
|
|
138
|
+
- Transaction is rolled back
|
|
139
|
+
|
|
140
|
+
**Important**: `throw :abort` does NOT add errors. Add them explicitly:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
def check_stock
|
|
144
|
+
if items.any?(&:out_of_stock?)
|
|
145
|
+
errors.add(:base, "Some items are out of stock")
|
|
146
|
+
throw(:abort)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Transaction Callbacks
|
|
152
|
+
|
|
153
|
+
### The Critical Distinction
|
|
154
|
+
|
|
155
|
+
All `after_*` callbacks run INSIDE the transaction. External systems can't see your changes yet:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
# WRONG - Race condition!
|
|
159
|
+
after_save :enqueue_processing
|
|
160
|
+
|
|
161
|
+
def enqueue_processing
|
|
162
|
+
ProcessingJob.perform_later(id) # Job starts before COMMIT
|
|
163
|
+
# Sidekiq: "Couldn't find Record with 'id'=123"
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Use `after_commit` for external interactions:
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
# CORRECT - Runs after COMMIT
|
|
171
|
+
after_commit :enqueue_processing, on: :create
|
|
172
|
+
|
|
173
|
+
def enqueue_processing
|
|
174
|
+
ProcessingJob.perform_later(id) # Record guaranteed to exist
|
|
175
|
+
end
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### When to Use after_commit
|
|
179
|
+
|
|
180
|
+
- Enqueuing background jobs
|
|
181
|
+
- Updating search indexes (Elasticsearch, Algolia)
|
|
182
|
+
- Clearing caches
|
|
183
|
+
- Sending emails/notifications
|
|
184
|
+
- Making API calls to external services
|
|
185
|
+
- Any action that should only occur if the DB change is permanent
|
|
186
|
+
|
|
187
|
+
### Transaction Callback Variants
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
# Fires on create, update, or destroy after commit
|
|
191
|
+
after_commit :refresh_cache
|
|
192
|
+
|
|
193
|
+
# Scoped to specific actions (Rails 7.1+)
|
|
194
|
+
after_create_commit :send_welcome_email
|
|
195
|
+
after_update_commit :sync_changes
|
|
196
|
+
after_destroy_commit :cleanup_external
|
|
197
|
+
|
|
198
|
+
# Equivalent to the above
|
|
199
|
+
after_commit :send_welcome_email, on: :create
|
|
200
|
+
after_commit :sync_changes, on: :update
|
|
201
|
+
after_commit :cleanup_external, on: :destroy
|
|
202
|
+
|
|
203
|
+
# Multiple actions
|
|
204
|
+
after_commit :reindex, on: [:create, :update]
|
|
205
|
+
|
|
206
|
+
# Rollback callback
|
|
207
|
+
after_rollback :log_failure
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### after_save_commit (Rails 7.1+)
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
# Fires on create OR update, not destroy
|
|
214
|
+
after_save_commit :sync_to_search
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Transaction Callback Gotchas
|
|
218
|
+
|
|
219
|
+
### Gotcha 1: Callback Deduplication
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
# WRONG - Only the last one runs!
|
|
223
|
+
after_commit :do_something
|
|
224
|
+
after_commit :do_something
|
|
225
|
+
|
|
226
|
+
# Also deduplicated across variants
|
|
227
|
+
after_commit :sync_data
|
|
228
|
+
after_create_commit :sync_data
|
|
229
|
+
after_save_commit :sync_data
|
|
230
|
+
# Only one sync_data callback runs
|
|
231
|
+
|
|
232
|
+
# CORRECT - Use :on option
|
|
233
|
+
after_commit :sync_data, on: [:create, :update]
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Gotcha 2: previous_changes Behavior
|
|
237
|
+
|
|
238
|
+
`previous_changes` is reset on each save, not when the transaction closes:
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
after_commit :log_changes
|
|
242
|
+
|
|
243
|
+
def log_changes
|
|
244
|
+
# If record was saved twice in one transaction,
|
|
245
|
+
# previous_changes only contains the LAST save's changes
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Gotcha 3: Exception Handling
|
|
250
|
+
|
|
251
|
+
Exceptions in `after_commit` callbacks:
|
|
252
|
+
- Bubble up to the caller
|
|
253
|
+
- Stop remaining `after_commit` callbacks from running
|
|
254
|
+
- Do NOT rollback (commit already happened)
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
after_commit :might_fail
|
|
258
|
+
after_commit :wont_run_if_above_fails
|
|
259
|
+
|
|
260
|
+
def might_fail
|
|
261
|
+
ExternalService.notify(self) # Raises exception
|
|
262
|
+
rescue ExternalService::Error => e
|
|
263
|
+
Rails.logger.error("Notification failed: #{e}")
|
|
264
|
+
# Don't re-raise - let other callbacks run
|
|
265
|
+
end
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Gotcha 4: Testing Complications
|
|
269
|
+
|
|
270
|
+
Older Rails wrapped tests in transactions, preventing `after_commit` from firing. Fixed in Rails 5+, but be aware:
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
# Use transactional fixtures carefully
|
|
274
|
+
# after_commit runs in Rails 5+ with proper config
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Gotcha 5: Callback Ordering (Rails 7.1+)
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
# Rails 7.1+ default: callbacks run in definition order
|
|
281
|
+
config.active_record.run_after_transaction_callbacks_in_order_defined = true
|
|
282
|
+
|
|
283
|
+
# Pre-7.1 behavior: reverse order
|
|
284
|
+
config.active_record.run_after_transaction_callbacks_in_order_defined = false
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Around Callbacks
|
|
288
|
+
|
|
289
|
+
Must call `yield` or the action won't execute:
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
class Article < ApplicationRecord
|
|
293
|
+
around_save :measure_save_time
|
|
294
|
+
|
|
295
|
+
private
|
|
296
|
+
|
|
297
|
+
def measure_save_time
|
|
298
|
+
start = Time.current
|
|
299
|
+
yield # REQUIRED - executes the save
|
|
300
|
+
duration = Time.current - start
|
|
301
|
+
Rails.logger.info("Save took #{duration}s")
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Forgetting yield is a common bug** - the record won't be saved.
|
|
307
|
+
|
|
308
|
+
## Callback Ordering with prepend
|
|
309
|
+
|
|
310
|
+
Callbacks from associations (like `dependent: :destroy`) run before your callbacks. Use `prepend: true` to run first:
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
class Topic < ApplicationRecord
|
|
314
|
+
has_many :comments, dependent: :destroy
|
|
315
|
+
|
|
316
|
+
# WRONG - comments already deleted when this runs
|
|
317
|
+
before_destroy :log_comments
|
|
318
|
+
|
|
319
|
+
# CORRECT - runs before dependent: :destroy
|
|
320
|
+
before_destroy :log_comments, prepend: true
|
|
321
|
+
|
|
322
|
+
private
|
|
323
|
+
|
|
324
|
+
def log_comments
|
|
325
|
+
Rails.logger.info("Destroying topic with #{comments.count} comments")
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Callback Inheritance
|
|
331
|
+
|
|
332
|
+
Callbacks are inherited by subclasses:
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
class Animal < ApplicationRecord
|
|
336
|
+
before_save :set_kingdom
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
class Dog < Animal
|
|
340
|
+
before_save :set_species
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Dog.create runs both: set_kingdom, then set_species
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Critical**: Define callbacks BEFORE associations in parent classes for proper inheritance.
|
|
347
|
+
|
|
348
|
+
## Methods That Skip Callbacks
|
|
349
|
+
|
|
350
|
+
These methods bypass ALL callbacks:
|
|
351
|
+
|
|
352
|
+
| Method | Skips Callbacks |
|
|
353
|
+
|--------|-----------------|
|
|
354
|
+
| `delete` | Yes |
|
|
355
|
+
| `delete_all` | Yes |
|
|
356
|
+
| `update_column` | Yes |
|
|
357
|
+
| `update_columns` | Yes |
|
|
358
|
+
| `update_all` | Yes |
|
|
359
|
+
| `insert` / `insert_all` | Yes |
|
|
360
|
+
| `upsert` / `upsert_all` | Yes |
|
|
361
|
+
| `touch_all` | Yes |
|
|
362
|
+
| `increment!` / `decrement!` | Yes |
|
|
363
|
+
| `increment_counter` / `decrement_counter` | Yes |
|
|
364
|
+
|
|
365
|
+
**Warning**: Use with caution - you may bypass critical business logic.
|
|
366
|
+
|
|
367
|
+
## Debugging Callbacks
|
|
368
|
+
|
|
369
|
+
Inspect the callback chain:
|
|
370
|
+
|
|
371
|
+
```ruby
|
|
372
|
+
# All save callbacks
|
|
373
|
+
Article._save_callbacks
|
|
374
|
+
|
|
375
|
+
# Only before_save callbacks
|
|
376
|
+
Article._save_callbacks.select { |cb| cb.kind == :before }
|
|
377
|
+
|
|
378
|
+
# Check if a specific callback is registered
|
|
379
|
+
Article._save_callbacks.map(&:filter).include?(:normalize_title)
|
|
380
|
+
|
|
381
|
+
# All validation callbacks
|
|
382
|
+
Article._validation_callbacks
|
|
383
|
+
|
|
384
|
+
# All create callbacks
|
|
385
|
+
Article._create_callbacks
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Anti-Patterns
|
|
389
|
+
|
|
390
|
+
### 1. Callback Hell
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
# WRONG - Too much responsibility, hard to test
|
|
394
|
+
class User < ApplicationRecord
|
|
395
|
+
after_create :send_welcome_email
|
|
396
|
+
after_create :create_default_settings
|
|
397
|
+
after_create :notify_admin
|
|
398
|
+
after_create :sync_to_crm
|
|
399
|
+
after_create :update_analytics
|
|
400
|
+
after_update :sync_to_crm
|
|
401
|
+
after_update :invalidate_cache
|
|
402
|
+
after_destroy :cleanup_external_data
|
|
403
|
+
# ... 20 more callbacks
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# BETTER - Use a service object
|
|
407
|
+
class UserCreationService
|
|
408
|
+
def call(user_params)
|
|
409
|
+
user = User.create!(user_params)
|
|
410
|
+
send_welcome_email(user)
|
|
411
|
+
create_default_settings(user)
|
|
412
|
+
notify_admin(user)
|
|
413
|
+
sync_to_crm(user)
|
|
414
|
+
user
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### 2. Callbacks Modifying Other Models
|
|
420
|
+
|
|
421
|
+
```ruby
|
|
422
|
+
# WRONG - Violates Law of Demeter
|
|
423
|
+
class Message < ApplicationRecord
|
|
424
|
+
after_create :update_conversation_stats
|
|
425
|
+
|
|
426
|
+
def update_conversation_stats
|
|
427
|
+
conversation.update!(
|
|
428
|
+
message_count: conversation.messages.count,
|
|
429
|
+
last_message_at: created_at
|
|
430
|
+
)
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# BETTER - Use a service or let the parent handle it
|
|
435
|
+
class ConversationMessageService
|
|
436
|
+
def add_message(conversation, message_params)
|
|
437
|
+
message = conversation.messages.create!(message_params)
|
|
438
|
+
conversation.touch(:last_message_at)
|
|
439
|
+
conversation.increment!(:message_count)
|
|
440
|
+
message
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### 3. Using after_save for External Systems
|
|
446
|
+
|
|
447
|
+
```ruby
|
|
448
|
+
# WRONG - Race condition with background jobs
|
|
449
|
+
after_save :enqueue_processing
|
|
450
|
+
|
|
451
|
+
# CORRECT
|
|
452
|
+
after_commit :enqueue_processing, on: [:create, :update]
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### 4. Heavy Operations in Callbacks
|
|
456
|
+
|
|
457
|
+
```ruby
|
|
458
|
+
# WRONG - Blocks the request
|
|
459
|
+
after_create :generate_thumbnail
|
|
460
|
+
after_create :sync_to_external_api
|
|
461
|
+
after_create :send_notification
|
|
462
|
+
|
|
463
|
+
# CORRECT - Defer to background jobs
|
|
464
|
+
after_create_commit :enqueue_post_creation_jobs
|
|
465
|
+
|
|
466
|
+
def enqueue_post_creation_jobs
|
|
467
|
+
ThumbnailJob.perform_later(id)
|
|
468
|
+
ExternalSyncJob.perform_later(id)
|
|
469
|
+
NotificationJob.perform_later(id)
|
|
470
|
+
end
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### 5. Conditional Logic Explosion
|
|
474
|
+
|
|
475
|
+
```ruby
|
|
476
|
+
# WRONG - Hard to follow
|
|
477
|
+
before_save :do_a, if: :condition_x?
|
|
478
|
+
before_save :do_b, if: :condition_y?
|
|
479
|
+
before_save :do_c, if: -> { condition_x? && !condition_z? }
|
|
480
|
+
after_save :do_d, unless: -> { condition_x? || condition_y? }
|
|
481
|
+
|
|
482
|
+
# BETTER - Extract to a single callback or service
|
|
483
|
+
before_save :prepare_for_save
|
|
484
|
+
|
|
485
|
+
def prepare_for_save
|
|
486
|
+
if condition_x?
|
|
487
|
+
do_a
|
|
488
|
+
do_c unless condition_z?
|
|
489
|
+
end
|
|
490
|
+
do_b if condition_y?
|
|
491
|
+
end
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### 6. Throwing abort Without Error Messages
|
|
495
|
+
|
|
496
|
+
```ruby
|
|
497
|
+
# WRONG - No feedback to user
|
|
498
|
+
before_save :validate_complex_rules
|
|
499
|
+
|
|
500
|
+
def validate_complex_rules
|
|
501
|
+
throw(:abort) if invalid_state?
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# CORRECT - Add error message
|
|
505
|
+
def validate_complex_rules
|
|
506
|
+
if invalid_state?
|
|
507
|
+
errors.add(:base, "Cannot save in current state")
|
|
508
|
+
throw(:abort)
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
## When Callbacks Are Appropriate
|
|
514
|
+
|
|
515
|
+
**Good uses:**
|
|
516
|
+
- Setting defaults or computed attributes on the current model
|
|
517
|
+
- Data normalization (strip, downcase, format)
|
|
518
|
+
- Simple audit logging (who changed what)
|
|
519
|
+
- Counter cache updates
|
|
520
|
+
- Maintaining data consistency within the same model
|
|
521
|
+
|
|
522
|
+
```ruby
|
|
523
|
+
class User < ApplicationRecord
|
|
524
|
+
before_validation :normalize_email
|
|
525
|
+
before_create :generate_api_key
|
|
526
|
+
after_touch :update_full_name_cache
|
|
527
|
+
|
|
528
|
+
private
|
|
529
|
+
|
|
530
|
+
def normalize_email
|
|
531
|
+
self.email = email&.downcase&.strip
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def generate_api_key
|
|
535
|
+
self.api_key ||= SecureRandom.hex(32)
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def update_full_name_cache
|
|
539
|
+
update_column(:full_name, "#{first_name} #{last_name}")
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
## Alternatives to Callbacks
|
|
545
|
+
|
|
546
|
+
### Service Objects (Recommended)
|
|
547
|
+
|
|
548
|
+
```ruby
|
|
549
|
+
# app/services/user_registration_service.rb
|
|
550
|
+
class UserRegistrationService
|
|
551
|
+
def call(params)
|
|
552
|
+
user = User.create!(params)
|
|
553
|
+
WelcomeMailer.welcome(user).deliver_later
|
|
554
|
+
Analytics.track("user_registered", user_id: user.id)
|
|
555
|
+
CrmSync.create_contact(user)
|
|
556
|
+
user
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# In controller
|
|
561
|
+
def create
|
|
562
|
+
user = UserRegistrationService.new.call(user_params)
|
|
563
|
+
redirect_to user
|
|
564
|
+
end
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
**Benefits:**
|
|
568
|
+
- Explicit control flow
|
|
569
|
+
- Easy to test in isolation
|
|
570
|
+
- Clear dependencies
|
|
571
|
+
- No hidden side effects
|
|
572
|
+
|
|
573
|
+
### Domain Events
|
|
574
|
+
|
|
575
|
+
```ruby
|
|
576
|
+
# Using a simple pub/sub pattern
|
|
577
|
+
class User < ApplicationRecord
|
|
578
|
+
after_create_commit { EventBus.publish("user.created", self) }
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
# Subscribers
|
|
582
|
+
EventBus.subscribe("user.created") do |user|
|
|
583
|
+
WelcomeMailer.welcome(user).deliver_later
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
EventBus.subscribe("user.created") do |user|
|
|
587
|
+
Analytics.track("user_registered", user_id: user.id)
|
|
588
|
+
end
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
**Benefits:**
|
|
592
|
+
- Loose coupling
|
|
593
|
+
- Easy to add/remove handlers
|
|
594
|
+
- Better for complex event-driven architectures
|
|
595
|
+
|
|
596
|
+
### Form Objects
|
|
597
|
+
|
|
598
|
+
```ruby
|
|
599
|
+
# app/forms/registration_form.rb
|
|
600
|
+
class RegistrationForm
|
|
601
|
+
include ActiveModel::Model
|
|
602
|
+
|
|
603
|
+
attr_accessor :email, :password, :terms_accepted
|
|
604
|
+
|
|
605
|
+
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
606
|
+
validates :password, length: { minimum: 8 }
|
|
607
|
+
validates :terms_accepted, acceptance: true
|
|
608
|
+
|
|
609
|
+
def save
|
|
610
|
+
return false unless valid?
|
|
611
|
+
|
|
612
|
+
user = User.create!(email:, password:)
|
|
613
|
+
send_welcome_email(user)
|
|
614
|
+
true
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
private
|
|
618
|
+
|
|
619
|
+
def send_welcome_email(user)
|
|
620
|
+
WelcomeMailer.welcome(user).deliver_later
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
## Testing Callbacks
|
|
626
|
+
|
|
627
|
+
### Test the Behavior, Not the Callback
|
|
628
|
+
|
|
629
|
+
```ruby
|
|
630
|
+
# WRONG - Testing implementation
|
|
631
|
+
it "calls normalize_email before validation" do
|
|
632
|
+
expect(user).to receive(:normalize_email)
|
|
633
|
+
user.valid?
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# CORRECT - Testing behavior
|
|
637
|
+
it "normalizes email before saving" do
|
|
638
|
+
user = User.create!(email: " JOHN@EXAMPLE.COM ", ...)
|
|
639
|
+
expect(user.email).to eq("john@example.com")
|
|
640
|
+
end
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### Testing after_commit Callbacks
|
|
644
|
+
|
|
645
|
+
```ruby
|
|
646
|
+
# Ensure test database connection commits
|
|
647
|
+
# In rails_helper.rb or spec_helper.rb
|
|
648
|
+
|
|
649
|
+
RSpec.configure do |config|
|
|
650
|
+
config.use_transactional_fixtures = true
|
|
651
|
+
# Rails 5+ properly handles after_commit in tests
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
# Test the side effect
|
|
655
|
+
it "enqueues a job after creation" do
|
|
656
|
+
expect {
|
|
657
|
+
User.create!(email: "test@example.com")
|
|
658
|
+
}.to have_enqueued_job(WelcomeEmailJob)
|
|
659
|
+
end
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### Isolating Callback Effects
|
|
663
|
+
|
|
664
|
+
```ruby
|
|
665
|
+
# Skip callbacks when not relevant to test
|
|
666
|
+
RSpec.describe User do
|
|
667
|
+
describe "#full_name" do
|
|
668
|
+
it "returns first and last name" do
|
|
669
|
+
user = User.new(first_name: "John", last_name: "Doe")
|
|
670
|
+
expect(user.full_name).to eq("John Doe")
|
|
671
|
+
end
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
# Don't need to trigger callbacks for this test
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
## Performance Considerations
|
|
679
|
+
|
|
680
|
+
### Callbacks Slow Down Tests
|
|
681
|
+
|
|
682
|
+
Heavy use of callbacks in factories creates cascading effects:
|
|
683
|
+
|
|
684
|
+
```ruby
|
|
685
|
+
# Slow - every User.create runs all callbacks
|
|
686
|
+
FactoryBot.define do
|
|
687
|
+
factory :user do
|
|
688
|
+
email { Faker::Internet.email }
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# Faster - use build_stubbed when callbacks aren't needed
|
|
693
|
+
let(:user) { build_stubbed(:user) }
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### Consider Database Triggers
|
|
697
|
+
|
|
698
|
+
For high-performance counter caches:
|
|
699
|
+
|
|
700
|
+
```ruby
|
|
701
|
+
# ActiveRecord callback - runs in Ruby, per record
|
|
702
|
+
after_create { parent.increment!(:children_count) }
|
|
703
|
+
|
|
704
|
+
# Database trigger - runs in DB, faster for bulk operations
|
|
705
|
+
# See strong_migrations gem for safe trigger management
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
## Nested Transactions
|
|
709
|
+
|
|
710
|
+
Callbacks run inside the transaction. Nested transactions without `requires_new: true` don't create savepoints:
|
|
711
|
+
|
|
712
|
+
```ruby
|
|
713
|
+
User.transaction do
|
|
714
|
+
user = User.create!(name: "Alice")
|
|
715
|
+
|
|
716
|
+
User.transaction do
|
|
717
|
+
user.update!(name: "Bob")
|
|
718
|
+
raise ActiveRecord::Rollback # Does NOT rollback!
|
|
719
|
+
end
|
|
720
|
+
end
|
|
721
|
+
# User saved as "Bob" - the rollback was ignored
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
Use `requires_new: true` for independent rollback:
|
|
725
|
+
|
|
726
|
+
```ruby
|
|
727
|
+
User.transaction do
|
|
728
|
+
user = User.create!(name: "Alice")
|
|
729
|
+
|
|
730
|
+
User.transaction(requires_new: true) do
|
|
731
|
+
user.update!(name: "Bob")
|
|
732
|
+
raise ActiveRecord::Rollback # Creates savepoint, rolls back to "Alice"
|
|
733
|
+
end
|
|
734
|
+
end
|
|
735
|
+
# User saved as "Alice"
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
**PostgreSQL Warning**: Don't rescue `ActiveRecord::StatementInvalid` inside transactions - it poisons the transaction.
|