anima-core 0.2.1 → 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 +19 -0
- data/README.md +213 -43
- 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 +195 -45
- data/app/decorators/user_message_decorator.rb +16 -5
- data/app/jobs/agent_request_job.rb +55 -2
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +15 -4
- data/app/models/concerns/event/broadcasting.rb +81 -0
- data/app/models/event.rb +20 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +366 -21
- data/config/application.rb +2 -0
- data/config/initializers/event_subscribers.rb +0 -1
- data/config/routes.rb +0 -6
- data/db/migrate/20260313010000_add_status_to_events.rb +8 -0
- data/db/migrate/20260313020000_add_processing_to_sessions.rb +7 -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 -6
- 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 +5 -40
- 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/events/subscribers/persister.rb +1 -0
- data/lib/events/user_message.rb +17 -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 +11 -20
- 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 +226 -0
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +151 -0
- 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/tools/write.rb +86 -0
- data/lib/tui/app.rb +985 -26
- data/lib/tui/cable_client.rb +69 -31
- data/lib/tui/message_store.rb +103 -8
- data/lib/tui/screens/chat.rb +293 -45
- 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 +290 -3
- data/app/controllers/api/sessions_controller.rb +0 -25
- data/lib/events/subscribers/action_cable_bridge.rb +0 -59
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Batch Processing Examples
|
|
2
|
+
# Demonstrates find_each, find_in_batches, in_batches for large datasets
|
|
3
|
+
|
|
4
|
+
# ============================================
|
|
5
|
+
# The Problem: Loading All Records
|
|
6
|
+
# ============================================
|
|
7
|
+
|
|
8
|
+
# BAD - loads ALL records into memory at once
|
|
9
|
+
User.all.each do |user|
|
|
10
|
+
NewsMailer.weekly_digest(user).deliver_later
|
|
11
|
+
end
|
|
12
|
+
# With 1 million users = 1 million User objects in memory!
|
|
13
|
+
|
|
14
|
+
# GOOD - loads in batches of 1000, GC can clean up between batches
|
|
15
|
+
User.find_each do |user|
|
|
16
|
+
NewsMailer.weekly_digest(user).deliver_later
|
|
17
|
+
end
|
|
18
|
+
# Memory stays constant regardless of total count
|
|
19
|
+
|
|
20
|
+
# ============================================
|
|
21
|
+
# find_each - Process Individual Records
|
|
22
|
+
# ============================================
|
|
23
|
+
|
|
24
|
+
# Basic usage - yields one record at a time
|
|
25
|
+
User.find_each do |user|
|
|
26
|
+
user.calculate_monthly_stats
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# With batch size
|
|
30
|
+
User.find_each(batch_size: 500) do |user|
|
|
31
|
+
# Process in batches of 500
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# With start/finish - process subset by primary key
|
|
35
|
+
User.find_each(start: 1000, finish: 5000) do |user|
|
|
36
|
+
# Only users with id between 1000 and 5000
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# With scope
|
|
40
|
+
User.active.find_each do |user|
|
|
41
|
+
# Only active users
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Enumerator form (for chaining)
|
|
45
|
+
User.find_each.map(&:email)
|
|
46
|
+
User.find_each.with_index { |user, i| puts "#{i}: #{user.name}" }
|
|
47
|
+
|
|
48
|
+
# ============================================
|
|
49
|
+
# find_in_batches - Process Batches of Records
|
|
50
|
+
# ============================================
|
|
51
|
+
|
|
52
|
+
# Yields arrays of records
|
|
53
|
+
User.find_in_batches(batch_size: 100) do |users|
|
|
54
|
+
# users is Array<User> with up to 100 elements
|
|
55
|
+
ExternalApi.bulk_sync(users.map(&:external_id))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Practical: Bulk API calls
|
|
59
|
+
Product.find_in_batches(batch_size: 50) do |products|
|
|
60
|
+
SearchIndex.bulk_update(products)
|
|
61
|
+
sleep(0.5) # Rate limiting
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# With eager loading
|
|
65
|
+
User.includes(:profile, :preferences).find_in_batches do |users|
|
|
66
|
+
users.each do |user|
|
|
67
|
+
user.profile # No N+1
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# ============================================
|
|
72
|
+
# in_batches - Process Batches as Relations
|
|
73
|
+
# ============================================
|
|
74
|
+
|
|
75
|
+
# Yields ActiveRecord::Relation objects
|
|
76
|
+
User.in_batches do |batch|
|
|
77
|
+
# batch is a Relation, not Array
|
|
78
|
+
batch.update_all(newsletter_sent_at: Time.current)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Bulk update pattern
|
|
82
|
+
User.where(legacy: true).in_batches.update_all(migrated: true)
|
|
83
|
+
|
|
84
|
+
# Bulk delete with throttling
|
|
85
|
+
User.where("created_at < ?", 5.years.ago).in_batches do |batch|
|
|
86
|
+
batch.delete_all
|
|
87
|
+
sleep(0.1) # Reduce database load
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Bulk operations via relation
|
|
91
|
+
Order.where(status: "pending").in_batches do |batch|
|
|
92
|
+
batch.update_all(status: "cancelled", cancelled_at: Time.current)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# With load: true to also get records
|
|
96
|
+
User.in_batches(load: true) do |batch|
|
|
97
|
+
batch.each { |user| user.some_instance_method }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# ============================================
|
|
101
|
+
# Comparison: When to Use Which
|
|
102
|
+
# ============================================
|
|
103
|
+
|
|
104
|
+
# find_each - Individual record processing
|
|
105
|
+
# - Sending emails one by one
|
|
106
|
+
# - Complex per-record logic
|
|
107
|
+
# - Instance methods needed
|
|
108
|
+
User.find_each { |u| u.send_notification }
|
|
109
|
+
|
|
110
|
+
# find_in_batches - Batch operations on loaded records
|
|
111
|
+
# - Bulk API calls with arrays
|
|
112
|
+
# - Batch exports
|
|
113
|
+
# - When you need the actual objects in groups
|
|
114
|
+
User.find_in_batches { |users| CsvExporter.export(users) }
|
|
115
|
+
|
|
116
|
+
# in_batches - SQL-level bulk operations
|
|
117
|
+
# - update_all, delete_all
|
|
118
|
+
# - Bulk SQL operations
|
|
119
|
+
# - Maximum efficiency, no Ruby object overhead
|
|
120
|
+
User.in_batches.update_all(processed: true)
|
|
121
|
+
|
|
122
|
+
# ============================================
|
|
123
|
+
# Important Caveats
|
|
124
|
+
# ============================================
|
|
125
|
+
|
|
126
|
+
# ORDERING IS IGNORED
|
|
127
|
+
# Batch processing always orders by primary key
|
|
128
|
+
User.order(:name).find_each { |u| }
|
|
129
|
+
# WARNING: Scoped order is ignored, will use primary key order
|
|
130
|
+
|
|
131
|
+
# RESULTS MAY BE INCONSISTENT
|
|
132
|
+
# If records are added/deleted during iteration, may skip/duplicate
|
|
133
|
+
# Solution: Use in_batches with explicit locking for critical operations
|
|
134
|
+
|
|
135
|
+
# CUSTOM ORDER WITH cursor (Rails 7.1+)
|
|
136
|
+
User.find_each(cursor: [:created_at, :id]) { |u| }
|
|
137
|
+
# Orders by created_at, then id for stability
|
|
138
|
+
|
|
139
|
+
# ============================================
|
|
140
|
+
# Practical Examples
|
|
141
|
+
# ============================================
|
|
142
|
+
|
|
143
|
+
# Example 1: Data migration
|
|
144
|
+
class BackfillUserSettings < ActiveRecord::Migration[7.1]
|
|
145
|
+
def up
|
|
146
|
+
User.in_batches do |batch|
|
|
147
|
+
batch.update_all(settings: { notifications: true }.to_json)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Example 2: Export to CSV
|
|
153
|
+
require "csv"
|
|
154
|
+
|
|
155
|
+
def export_users_to_csv(file_path)
|
|
156
|
+
CSV.open(file_path, "w") do |csv|
|
|
157
|
+
csv << ["ID", "Name", "Email", "Created At"]
|
|
158
|
+
|
|
159
|
+
User.find_each do |user|
|
|
160
|
+
csv << [user.id, user.name, user.email, user.created_at]
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Example 3: Background job processing
|
|
166
|
+
class ProcessAllOrdersJob < ApplicationJob
|
|
167
|
+
def perform
|
|
168
|
+
Order.pending.find_each do |order|
|
|
169
|
+
ProcessOrderJob.perform_later(order.id)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Example 4: Batch API sync with rate limiting
|
|
175
|
+
def sync_products_to_external_service
|
|
176
|
+
Product.active.find_in_batches(batch_size: 25) do |products|
|
|
177
|
+
ExternalService.bulk_upsert(
|
|
178
|
+
products.map { |p| p.as_external_format }
|
|
179
|
+
)
|
|
180
|
+
sleep(1) # Respect rate limits
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Example 5: Data cleanup with progress tracking
|
|
185
|
+
def cleanup_old_sessions
|
|
186
|
+
total = Session.where("created_at < ?", 30.days.ago).count
|
|
187
|
+
deleted = 0
|
|
188
|
+
|
|
189
|
+
Session.where("created_at < ?", 30.days.ago).in_batches do |batch|
|
|
190
|
+
count = batch.delete_all
|
|
191
|
+
deleted += count
|
|
192
|
+
Rails.logger.info "Deleted #{deleted}/#{total} old sessions"
|
|
193
|
+
sleep(0.05)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Example 6: Memory-efficient aggregation
|
|
198
|
+
def calculate_total_balance
|
|
199
|
+
total = 0
|
|
200
|
+
|
|
201
|
+
Account.active.find_each do |account|
|
|
202
|
+
total += account.calculated_balance # Complex calculation
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
total
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Better: Use database when possible
|
|
209
|
+
Account.active.sum(:balance) # Single SQL query
|
|
210
|
+
|
|
211
|
+
# ============================================
|
|
212
|
+
# Batch Processing with Transactions
|
|
213
|
+
# ============================================
|
|
214
|
+
|
|
215
|
+
# Each batch in its own transaction
|
|
216
|
+
User.in_batches do |batch|
|
|
217
|
+
batch.transaction do
|
|
218
|
+
batch.update_all(processed: true)
|
|
219
|
+
AuditLog.create!(action: "batch_processed", count: batch.count)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Whole operation in one transaction (careful with large datasets!)
|
|
224
|
+
User.transaction do
|
|
225
|
+
User.in_batches.update_all(processed: true)
|
|
226
|
+
end
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Eager Loading Examples
|
|
2
|
+
# Demonstrates includes, preload, eager_load, joins and N+1 prevention
|
|
3
|
+
|
|
4
|
+
# ============================================
|
|
5
|
+
# Sample Models for Examples
|
|
6
|
+
# ============================================
|
|
7
|
+
|
|
8
|
+
class Author < ApplicationRecord
|
|
9
|
+
has_many :posts
|
|
10
|
+
has_many :comments, through: :posts
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class Post < ApplicationRecord
|
|
14
|
+
belongs_to :author
|
|
15
|
+
has_many :comments
|
|
16
|
+
has_many :tags, through: :taggings
|
|
17
|
+
has_many :taggings
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Comment < ApplicationRecord
|
|
21
|
+
belongs_to :post
|
|
22
|
+
belongs_to :user
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# ============================================
|
|
26
|
+
# The N+1 Problem
|
|
27
|
+
# ============================================
|
|
28
|
+
|
|
29
|
+
# BAD - N+1 queries (1 query + N additional queries)
|
|
30
|
+
posts = Post.limit(10)
|
|
31
|
+
posts.each do |post|
|
|
32
|
+
puts post.author.name # Each iteration triggers a query!
|
|
33
|
+
end
|
|
34
|
+
# Query 1: SELECT * FROM posts LIMIT 10
|
|
35
|
+
# Query 2: SELECT * FROM authors WHERE id = 1
|
|
36
|
+
# Query 3: SELECT * FROM authors WHERE id = 2
|
|
37
|
+
# ... 10 more queries!
|
|
38
|
+
|
|
39
|
+
# GOOD - Eager loading (2 queries total)
|
|
40
|
+
posts = Post.includes(:author).limit(10)
|
|
41
|
+
posts.each do |post|
|
|
42
|
+
puts post.author.name # No additional queries!
|
|
43
|
+
end
|
|
44
|
+
# Query 1: SELECT * FROM posts LIMIT 10
|
|
45
|
+
# Query 2: SELECT * FROM authors WHERE id IN (1, 2, 3, ...)
|
|
46
|
+
|
|
47
|
+
# ============================================
|
|
48
|
+
# includes - Smart Default (Auto-chooses strategy)
|
|
49
|
+
# ============================================
|
|
50
|
+
|
|
51
|
+
# Separate queries (preload strategy) - when just accessing data
|
|
52
|
+
Post.includes(:author)
|
|
53
|
+
# SELECT * FROM posts
|
|
54
|
+
# SELECT * FROM authors WHERE id IN (1, 2, 3, ...)
|
|
55
|
+
|
|
56
|
+
# Single JOIN (eager_load strategy) - when filtering by association
|
|
57
|
+
Post.includes(:author).where(authors: { verified: true })
|
|
58
|
+
# SELECT posts.*, authors.*
|
|
59
|
+
# FROM posts LEFT OUTER JOIN authors ON authors.id = posts.author_id
|
|
60
|
+
# WHERE authors.verified = true
|
|
61
|
+
|
|
62
|
+
# Multiple associations
|
|
63
|
+
Post.includes(:author, :comments)
|
|
64
|
+
Post.includes(:author, comments: :user) # Nested
|
|
65
|
+
|
|
66
|
+
# ============================================
|
|
67
|
+
# references - Required for String Conditions
|
|
68
|
+
# ============================================
|
|
69
|
+
|
|
70
|
+
# ERROR - Rails doesn't know to JOIN
|
|
71
|
+
Post.includes(:author).where("authors.created_at > ?", 1.week.ago)
|
|
72
|
+
# PG::UndefinedTable: ERROR: missing FROM-clause entry for table "authors"
|
|
73
|
+
|
|
74
|
+
# CORRECT - explicitly reference the table
|
|
75
|
+
Post.includes(:author)
|
|
76
|
+
.where("authors.created_at > ?", 1.week.ago)
|
|
77
|
+
.references(:authors)
|
|
78
|
+
# SELECT posts.*, authors.*
|
|
79
|
+
# FROM posts LEFT OUTER JOIN authors ON ...
|
|
80
|
+
# WHERE authors.created_at > '2024-01-23'
|
|
81
|
+
|
|
82
|
+
# Hash conditions auto-reference
|
|
83
|
+
Post.includes(:author).where(authors: { verified: true }) # Works!
|
|
84
|
+
|
|
85
|
+
# ============================================
|
|
86
|
+
# preload - Always Separate Queries
|
|
87
|
+
# ============================================
|
|
88
|
+
|
|
89
|
+
# Forces separate queries regardless of conditions
|
|
90
|
+
Author.preload(:posts, :comments)
|
|
91
|
+
# SELECT * FROM authors
|
|
92
|
+
# SELECT * FROM posts WHERE author_id IN (1, 2, 3)
|
|
93
|
+
# SELECT * FROM comments WHERE author_id IN (1, 2, 3)
|
|
94
|
+
|
|
95
|
+
# CANNOT filter by preloaded association
|
|
96
|
+
Author.preload(:posts).where(posts: { published: true })
|
|
97
|
+
# ERROR! posts is not in the FROM clause
|
|
98
|
+
|
|
99
|
+
# Use When:
|
|
100
|
+
# - Large datasets (JOINs cause row multiplication)
|
|
101
|
+
# - Not filtering by associated data
|
|
102
|
+
# - Want predictable query behavior
|
|
103
|
+
|
|
104
|
+
# Example: preload is better here
|
|
105
|
+
authors = Author.preload(:posts) # 2 queries
|
|
106
|
+
# vs eager_load with 1000 posts per author = massive result set
|
|
107
|
+
|
|
108
|
+
# ============================================
|
|
109
|
+
# eager_load - Always LEFT OUTER JOIN
|
|
110
|
+
# ============================================
|
|
111
|
+
|
|
112
|
+
# Forces single query with LEFT OUTER JOIN
|
|
113
|
+
Author.eager_load(:posts)
|
|
114
|
+
# SELECT authors.*, posts.*
|
|
115
|
+
# FROM authors
|
|
116
|
+
# LEFT OUTER JOIN posts ON posts.author_id = authors.id
|
|
117
|
+
|
|
118
|
+
# Use When:
|
|
119
|
+
# - Filtering by association attributes
|
|
120
|
+
# - Sorting by association attributes
|
|
121
|
+
# - Need records even without associations (LEFT join includes NULLs)
|
|
122
|
+
|
|
123
|
+
# Example: authors sorted by latest post
|
|
124
|
+
Author.eager_load(:posts)
|
|
125
|
+
.order("posts.created_at DESC NULLS LAST")
|
|
126
|
+
.distinct
|
|
127
|
+
|
|
128
|
+
# ============================================
|
|
129
|
+
# joins - INNER JOIN (Filtering Only)
|
|
130
|
+
# ============================================
|
|
131
|
+
|
|
132
|
+
# Creates JOIN but does NOT load associated records
|
|
133
|
+
Author.joins(:posts).where(posts: { published: true }).distinct
|
|
134
|
+
# SELECT DISTINCT authors.*
|
|
135
|
+
# FROM authors
|
|
136
|
+
# INNER JOIN posts ON posts.author_id = authors.id
|
|
137
|
+
# WHERE posts.published = true
|
|
138
|
+
|
|
139
|
+
# WARNING: Accessing association still causes N+1!
|
|
140
|
+
Author.joins(:posts).each do |author|
|
|
141
|
+
author.posts # N+1 query here!
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Use When:
|
|
145
|
+
# - Only need to filter, not access associated data
|
|
146
|
+
# - Combine with includes for filtering + loading
|
|
147
|
+
|
|
148
|
+
# Pattern: joins + includes together
|
|
149
|
+
Author.joins(:posts)
|
|
150
|
+
.includes(:posts)
|
|
151
|
+
.where(posts: { published: true })
|
|
152
|
+
.distinct
|
|
153
|
+
|
|
154
|
+
# ============================================
|
|
155
|
+
# left_outer_joins - Include Records Without Association
|
|
156
|
+
# ============================================
|
|
157
|
+
|
|
158
|
+
# Authors including those without posts
|
|
159
|
+
Author.left_outer_joins(:posts)
|
|
160
|
+
.select("authors.*, COUNT(posts.id) AS posts_count")
|
|
161
|
+
.group("authors.id")
|
|
162
|
+
|
|
163
|
+
# Find authors with no posts
|
|
164
|
+
Author.left_outer_joins(:posts).where(posts: { id: nil })
|
|
165
|
+
|
|
166
|
+
# ============================================
|
|
167
|
+
# Nested Eager Loading
|
|
168
|
+
# ============================================
|
|
169
|
+
|
|
170
|
+
# One level
|
|
171
|
+
Post.includes(:author)
|
|
172
|
+
|
|
173
|
+
# Two levels
|
|
174
|
+
Post.includes(author: :profile)
|
|
175
|
+
|
|
176
|
+
# Multiple at same level
|
|
177
|
+
Post.includes(:author, :comments)
|
|
178
|
+
|
|
179
|
+
# Multiple nested
|
|
180
|
+
Post.includes(author: [:profile, :posts])
|
|
181
|
+
|
|
182
|
+
# Deep nesting
|
|
183
|
+
Post.includes(comments: { user: :profile })
|
|
184
|
+
|
|
185
|
+
# Mixed
|
|
186
|
+
Post.includes(:author, comments: { user: :profile }, tags: :category)
|
|
187
|
+
|
|
188
|
+
# ============================================
|
|
189
|
+
# Eager Loading Decision Tree (Applied)
|
|
190
|
+
# ============================================
|
|
191
|
+
|
|
192
|
+
# Scenario 1: Display posts with author names
|
|
193
|
+
# Need: Access author → Use includes
|
|
194
|
+
posts = Post.includes(:author).limit(20)
|
|
195
|
+
posts.each { |p| "#{p.title} by #{p.author.name}" }
|
|
196
|
+
|
|
197
|
+
# Scenario 2: Find posts by verified authors only
|
|
198
|
+
# Need: Filter by association → Use eager_load or includes with references
|
|
199
|
+
posts = Post.eager_load(:author).where(authors: { verified: true })
|
|
200
|
+
|
|
201
|
+
# Scenario 3: Count posts per author
|
|
202
|
+
# Need: Filter only, no data access → Use joins
|
|
203
|
+
Post.joins(:author)
|
|
204
|
+
.group("authors.id")
|
|
205
|
+
.count
|
|
206
|
+
|
|
207
|
+
# Scenario 4: Load authors with their many posts (large dataset)
|
|
208
|
+
# Need: Access data, large join explosion risk → Use preload
|
|
209
|
+
authors = Author.preload(:posts).limit(10)
|
|
210
|
+
|
|
211
|
+
# Scenario 5: Load posts with multiple associations
|
|
212
|
+
# Need: Mixed access patterns
|
|
213
|
+
Post.includes(:author) # Always accessed
|
|
214
|
+
.includes(:comments) # Sometimes accessed
|
|
215
|
+
.preload(:tags) # Large count, rarely accessed
|
|
216
|
+
|
|
217
|
+
# ============================================
|
|
218
|
+
# Performance Comparison
|
|
219
|
+
# ============================================
|
|
220
|
+
|
|
221
|
+
# Setup: 100 posts, each with 1 author and 50 comments
|
|
222
|
+
|
|
223
|
+
# N+1 (worst)
|
|
224
|
+
Post.all.each { |p| p.author; p.comments.to_a }
|
|
225
|
+
# 1 + 100 + 100 = 201 queries
|
|
226
|
+
|
|
227
|
+
# includes (better)
|
|
228
|
+
Post.includes(:author, :comments).each { |p| p.author; p.comments.to_a }
|
|
229
|
+
# 3 queries (posts, authors, comments)
|
|
230
|
+
|
|
231
|
+
# eager_load with many comments (can be slow)
|
|
232
|
+
Post.eager_load(:author, :comments).each { |p| p.author; p.comments.to_a }
|
|
233
|
+
# 1 query, but returns 100 * 50 = 5000 rows!
|
|
234
|
+
|
|
235
|
+
# preload is safer for large associations
|
|
236
|
+
Post.preload(:author, :comments).each { |p| p.author; p.comments.to_a }
|
|
237
|
+
# 3 queries, 100 + 100 + 5000 rows loaded separately
|
|
238
|
+
|
|
239
|
+
# ============================================
|
|
240
|
+
# Strict Loading - Prevent N+1 at Runtime
|
|
241
|
+
# ============================================
|
|
242
|
+
|
|
243
|
+
# Relation level - raises if lazy loading attempted
|
|
244
|
+
posts = Post.strict_loading
|
|
245
|
+
posts.first.author # ActiveRecord::StrictLoadingViolationError!
|
|
246
|
+
|
|
247
|
+
# Must eager load to access
|
|
248
|
+
posts = Post.strict_loading.includes(:author)
|
|
249
|
+
posts.first.author # Works!
|
|
250
|
+
|
|
251
|
+
# Model level - all queries are strict
|
|
252
|
+
class Post < ApplicationRecord
|
|
253
|
+
self.strict_loading_by_default = true
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Association level
|
|
257
|
+
class Author < ApplicationRecord
|
|
258
|
+
has_many :posts, strict_loading: true
|
|
259
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Finder Methods Examples
|
|
2
|
+
# Demonstrates find, find_by, where, and find_or_create_by
|
|
3
|
+
|
|
4
|
+
# ============================================
|
|
5
|
+
# Sample Models for Examples
|
|
6
|
+
# ============================================
|
|
7
|
+
|
|
8
|
+
class User < ApplicationRecord
|
|
9
|
+
has_many :posts
|
|
10
|
+
has_many :orders
|
|
11
|
+
|
|
12
|
+
scope :active, -> { where(active: true) }
|
|
13
|
+
scope :admins, -> { where(role: "admin") }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# ============================================
|
|
17
|
+
# find - Retrieve by Primary Key
|
|
18
|
+
# ============================================
|
|
19
|
+
|
|
20
|
+
# Single record - raises RecordNotFound if not found
|
|
21
|
+
user = User.find(1)
|
|
22
|
+
# SELECT * FROM users WHERE id = 1
|
|
23
|
+
|
|
24
|
+
# Multiple records - returns array, raises if ANY not found
|
|
25
|
+
users = User.find([1, 2, 3])
|
|
26
|
+
users = User.find(1, 2, 3) # Same result
|
|
27
|
+
# SELECT * FROM users WHERE id IN (1, 2, 3)
|
|
28
|
+
|
|
29
|
+
# Use when record MUST exist (will crash otherwise)
|
|
30
|
+
def show
|
|
31
|
+
@user = User.find(params[:id]) # 404 if not found
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# ============================================
|
|
35
|
+
# find_by - Retrieve First Match
|
|
36
|
+
# ============================================
|
|
37
|
+
|
|
38
|
+
# Returns nil if not found
|
|
39
|
+
user = User.find_by(email: "test@example.com")
|
|
40
|
+
# SELECT * FROM users WHERE email = 'test@example.com' LIMIT 1
|
|
41
|
+
|
|
42
|
+
# Multiple conditions
|
|
43
|
+
user = User.find_by(email: "test@example.com", active: true)
|
|
44
|
+
|
|
45
|
+
# With string conditions (use placeholders!)
|
|
46
|
+
user = User.find_by("email LIKE ?", "%@company.com")
|
|
47
|
+
|
|
48
|
+
# find_by! raises RecordNotFound
|
|
49
|
+
user = User.find_by!(email: "test@example.com")
|
|
50
|
+
|
|
51
|
+
# Use when absence is acceptable
|
|
52
|
+
def authenticate(email, password)
|
|
53
|
+
user = User.find_by(email:)
|
|
54
|
+
return nil unless user
|
|
55
|
+
user if user.authenticate(password)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# ============================================
|
|
59
|
+
# where - Build Conditions (Returns Relation)
|
|
60
|
+
# ============================================
|
|
61
|
+
|
|
62
|
+
# Hash conditions - safest, auto-escaped
|
|
63
|
+
User.where(active: true)
|
|
64
|
+
User.where(role: ["admin", "moderator"]) # IN clause
|
|
65
|
+
User.where(age: 18..65) # BETWEEN
|
|
66
|
+
User.where(deleted_at: nil) # IS NULL
|
|
67
|
+
|
|
68
|
+
# Chaining - lazy evaluation
|
|
69
|
+
users = User.where(active: true)
|
|
70
|
+
.where(role: "admin")
|
|
71
|
+
.order(created_at: :desc)
|
|
72
|
+
# Query not executed until iteration
|
|
73
|
+
|
|
74
|
+
# String conditions - ALWAYS use placeholders
|
|
75
|
+
User.where("age > ?", 18)
|
|
76
|
+
User.where("name LIKE ?", "%#{User.sanitize_sql_like(query)}%")
|
|
77
|
+
User.where("created_at > :date", date: 1.week.ago)
|
|
78
|
+
|
|
79
|
+
# DANGER - SQL injection!
|
|
80
|
+
# User.where("name = '#{params[:name]}'") # NEVER do this!
|
|
81
|
+
|
|
82
|
+
# ============================================
|
|
83
|
+
# where.not - Negation
|
|
84
|
+
# ============================================
|
|
85
|
+
|
|
86
|
+
User.where.not(role: "admin")
|
|
87
|
+
# WHERE role != 'admin'
|
|
88
|
+
|
|
89
|
+
User.where.not(deleted_at: nil)
|
|
90
|
+
# WHERE deleted_at IS NOT NULL
|
|
91
|
+
|
|
92
|
+
User.where.not(status: ["banned", "suspended"])
|
|
93
|
+
# WHERE status NOT IN ('banned', 'suspended')
|
|
94
|
+
|
|
95
|
+
# ============================================
|
|
96
|
+
# where.associated / where.missing (Rails 7+)
|
|
97
|
+
# ============================================
|
|
98
|
+
|
|
99
|
+
# Users who have at least one post
|
|
100
|
+
User.where.associated(:posts)
|
|
101
|
+
# SELECT users.* FROM users
|
|
102
|
+
# INNER JOIN posts ON posts.user_id = users.id
|
|
103
|
+
|
|
104
|
+
# Users with no posts
|
|
105
|
+
User.where.missing(:posts)
|
|
106
|
+
# SELECT users.* FROM users
|
|
107
|
+
# LEFT OUTER JOIN posts ON posts.user_id = users.id
|
|
108
|
+
# WHERE posts.id IS NULL
|
|
109
|
+
|
|
110
|
+
# ============================================
|
|
111
|
+
# find_or_create_by / find_or_initialize_by
|
|
112
|
+
# ============================================
|
|
113
|
+
|
|
114
|
+
# Finds existing or creates new record
|
|
115
|
+
user = User.find_or_create_by(email: "new@example.com") do |u|
|
|
116
|
+
u.name = "New User" # Only set for new records
|
|
117
|
+
u.role = "member"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Finds or builds (doesn't save)
|
|
121
|
+
user = User.find_or_initialize_by(email: "maybe@example.com")
|
|
122
|
+
user.new_record? # true if not found, false if found
|
|
123
|
+
user.save if user.new_record?
|
|
124
|
+
|
|
125
|
+
# With scoped relation
|
|
126
|
+
admin = User.admins.find_or_create_by(email: "admin@example.com")
|
|
127
|
+
|
|
128
|
+
# ============================================
|
|
129
|
+
# Handling Race Conditions
|
|
130
|
+
# ============================================
|
|
131
|
+
|
|
132
|
+
# find_or_create_by can fail under concurrent access
|
|
133
|
+
# when two requests try to create the same unique record
|
|
134
|
+
|
|
135
|
+
# Solution 1: Rescue and retry
|
|
136
|
+
def find_or_create_user(email)
|
|
137
|
+
User.find_or_create_by(email:)
|
|
138
|
+
rescue ActiveRecord::RecordNotUnique
|
|
139
|
+
retry
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Solution 2: Use create_or_find_by (Rails 6+)
|
|
143
|
+
# Creates first, finds on unique constraint violation
|
|
144
|
+
User.create_or_find_by(email: "user@example.com")
|
|
145
|
+
|
|
146
|
+
# ============================================
|
|
147
|
+
# Practical Examples
|
|
148
|
+
# ============================================
|
|
149
|
+
|
|
150
|
+
# API controller filtering
|
|
151
|
+
class UsersController < ApplicationController
|
|
152
|
+
def index
|
|
153
|
+
@users = User.all
|
|
154
|
+
|
|
155
|
+
@users = @users.where(role: params[:role]) if params[:role].present?
|
|
156
|
+
@users = @users.where(active: true) if params[:active] == "true"
|
|
157
|
+
@users = @users.where("created_at >= ?", params[:since]) if params[:since]
|
|
158
|
+
|
|
159
|
+
@users = @users.order(params[:sort] || :created_at)
|
|
160
|
+
@users = @users.limit(params[:limit] || 20)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Search with sanitization
|
|
165
|
+
def search_users(query)
|
|
166
|
+
return User.none if query.blank?
|
|
167
|
+
|
|
168
|
+
sanitized = User.sanitize_sql_like(query)
|
|
169
|
+
User.where("name ILIKE :q OR email ILIKE :q", q: "%#{sanitized}%")
|
|
170
|
+
end
|