N+1 is a serious problem that can bring your app to a crawl. This issue is not always immediately evident and often surfaces after a while when you have more data. The good news for Rails developers is that this is easily solved with `preload()`, `includes()`, and `eager_load()` methods.

- `eager_load()` loads the associated records with a single query using a LEFT OUTER JOIN.
```ruby
User.eager_load(:posts)
```
Would result in a following SQL:
```sql
SELECT users.*, posts.* 
FROM users 
LEFT OUTER JOIN posts ON posts.user_id = users.id
```

- `preload()` loads in multiple queries: one for the main records and one for each association.
```ruby
User.preload(:posts)
```
Would result in a following SQL:
```sql
SELECT * FROM users
SELECT * FROM posts WHERE user_id IN (1, 2, 3, ...)
```

- `includes()` is the most commonly used method for eager loading associations. It's smart enough to decide between two strategies:
	1. If no where clause references the included tables, it uses `preload()`.
	2. If there's a where clause referencing the included tables, it switches to `eager_load()`.


It's harder to notice places where we've introduced N+1 queries, and this is an area that many people try to figure out. First, we had the `bullet` gem, but its biggest issue is the many false positives it tends to highlight. Nowadays, `prosopite` seems to be the coolest kid on the block, promising no false positives.

My controversial opinion is that the `strict_loading` feature in Rails is all we need these days to detect most of these issues. One just needs to learn to cook it right!

# Enter strict_loading
The main purpose of strict_loading is to enforce eager loading of associations and preventing accidental N+1 queries. When strict_loading is enabled, Rails will raise an error (ActiveRecord::StrictLoadingViolationError) if you try to load an association that wasn't explicitly preloaded.

There are multiple ways to enable strict_loading. Let me walk you through all the methods I know off.

## Individual queries
```ruby
u = User.where(..).includes(:posts).strict_loading

# raises ActiveRecord::StrictLoadingViolationError
u.posts.first.category
```

## Individual records
```ruby
u = User.strict_loading.find(..)

# raises ActiveRecord::StrictLoadingViolationError
u.posts 

u = User.find(..)
u.strict_loading!

# Or you can even disable it
u = User.find(..)
u.strict_loading!(false)

```

##  Entire associations

```ruby
class User < ActiveRecord
  has_many :posts, strict_loading: true
end
```

## Entire models
```ruby
class User < ActiveRecord
  self.strict_loading_by_default = true
end
```
## Entire applications
```ruby
config.active_record.strict_loading_by_default = true
```

## n_plus_one_only mode
The `:n_plus_one_only` mode is designed to specifically target and prevent N+1 query problems while still allowing some flexibility in association loading. It allows lazy loading of associations when dealing with a single record. But It raises an `ActiveRecord::StrictLoadingViolationError` when it detects an N+1 query pattern.

```ruby
users = User.all.strict_loading!(mode: :n_plus_one_only)

# This works fine (lazy loading for a single record)
first_user = users.first
first_user.posts.to_a  # This is allowed

# This will raise an error (N+1 query detected)
users.each do |user|
	user.posts.to_a  # This will raise ActiveRecord::StrictLoadingViolationError
end
```

In ~~Rails 7.2~~ (turns out this change landed in master, but didn't land with rails 7.2), it's now possible to enable this mode for the entire application. This was the last puzzle piece missing to make this functionality widely usable.
```ruby
config.active_record.strict_loading_mode = :n_plus_one_only
```

# Integrating

Even in the smallest applications, it's difficult to enable `strict_loading_by_default` for the entire application immediately. It's always better to introduce `strict_loading` gradually. I propose the following 3-step approach:

1. As a first step, start marking individual queries with `.strict_loading` on your app's main pages - those that have many graphs and data points. Once obvious offenders are eliminated, you need to cast a wider net.
2. Identify "god models" in your application and mark those for `strict_loading_by_default`. In a large blog application, this would probably be the `User` and `Post` models.
3. The final boss of strict loading would be to enable `strict_loading_by_default` for the entire application. But even here, you can find ways to ease into it.

```ruby
# config/application.rb
config.active_record.strict_loading_by_default = true
# Will only raise, if multiple records have to loaded from database
config.active_record.strict_loading_mode = :n_plus_one_only
# instead of raising an error - emits a log message
config.active_record.action_on_strict_loading_violation = :log
```

When in _:log_ mode, Rails emits an event for every lazy load. We can listen for those and make our own structured log message:

```ruby
# config/initializers/strict_loading.rb
ActiveSupport::Notifications.subscribe(
  "strict_loading_violation.active_record"
) { |_name, _started, _finished, _unique_id, data|
  model = data.fetch(:owner)
  ref = data.fetch(:reflection)

  Rails.logger.warn({
    event: "strict_loading_violation",
    model: model.name,
    association: ref.name,
    trace: caller
  }.to_json)
  
  # Add example with AppSignal::EventFormatter.
}

```

# Final thoughts
`strict_loading` was announced with a lot of fanfare, but most Ruby developers quickly forgot about it and continued with their regular usage of `bullet` and/or `prosopite`. However, if `strict_loading_mode` could be set for entire app, all the features are in place to make this feature shine. It would finally feel feature-complete, to the point where I would consider removing `prosopite` from all my projects and have one less dependency in Gemfiles.