Querying with Strict Eloquent: Preventing N+1 and Data Leaks
Stop N+1 issues and unauthorized data access in their tracks. Learn to configure strict Eloquent modes and global scopes for a rock-solid production architecture.
Previously in this course, we discussed the Service Layer Pattern to decouple our business logic from the infrastructure. While that pattern protects our domain, the underlying data access layer remains vulnerable to runtime errors like lazy loading and cross-tenant data leakage. This lesson adds a layer of "defensive database programming" to your toolkit, ensuring that your application fails loudly during development rather than silently failing in production.
The Case for Strict Eloquent
In a high-traffic SaaS, performance is often lost in small, silent increments. An N+1 query might be undetectable during local development with a small dataset but can cripple your database under load. Similarly, forgetting a where('tenant_id', ...) clause in a complex query is a catastrophic security vulnerability.
Laravel’s "Strict" modes are designed to turn these silent killers into immediate exceptions. By enabling them, you shift the burden of catching these bugs from your QA team to your test suite.
Enabling Strict Loading
Strict loading prevents the "lazy loading" of relationships, which is the primary driver of the N+1 problem. When enabled, any attempt to access a relationship that hasn't been eager-loaded will throw an Illuminate\Database\LazyLoadingViolationException.
To enable this globally, you typically do this in the boot method of your AppServiceProvider:
PHPuse Illuminate\Database\Eloquent\Model; public function boot(): void { #6A9955">// Enable strict mode in local/testing environments Model::preventLazyLoading(!app()->isProduction()); #6A9955">// Optional: Log violations in production instead of throwing #6A9955">// Model::preventLazyLoading(app()->isProduction()); }
By setting this to !app()->isProduction(), you ensure that your developers are forced to use with() or load() during the development lifecycle. If a developer writes $user->profile->avatar without eager loading profile, the test suite will fail immediately.
Enforcing Data Isolation with Global Scopes
In our SaaS platform project, data leakage between tenants is a non-negotiable failure. While you could manually add a where('tenant_id', ...) to every query, human error makes this approach risky. Global Scopes allow you to inject this constraint automatically into every Eloquent query for a specific model.
Here is how we implement a TenantScope for our Subscription model:
PHPnamespace App\Models\Scopes; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Scope; use Illuminate\Support\Facades\Auth; class TenantScope implements Scope { public function apply(Builder $builder, Model $model): void { #6A9955">// Ensure we only retrieve records for the authenticated tenant if (Auth::check()) { $builder->where('tenant_id', Auth::user()->tenant_id); } } }
Now, apply it to the model:
PHPnamespace App\Models; use App\Models\Scopes\TenantScope; use Illuminate\Database\Eloquent\Model; class Subscription extends Model { protected static function booted(): void { static::addGlobalScope(new TenantScope); } }
With this in place, Subscription::all() now automatically executes SELECT * FROM subscriptions WHERE tenant_id = ?. You’ve effectively eliminated the possibility of accidentally leaking data from other tenants in your controllers or service classes.
Hands-on Exercise: Implementing the Guard
- Enable Strict Mode: Open your
AppServiceProviderand add theModel::preventLazyLoading()call as shown above. - Create a Scope: Generate a new scope class for your
Projectmodel in our SaaS project to ensure users only ever see their own projects. - Verify: Write a Feature Test that attempts to access a relationship without eager loading. Assert that a
LazyLoadingViolationExceptionis thrown. - Test Isolation: Write a test where you attempt to query a
Projectbelonging to a different tenant. Assert that the record is not found, even without an explicitwhereclause.
Common Pitfalls
- The "Production Exception" Trap: Do not simply call
Model::preventLazyLoading(true)in production if you haven't audited your entire codebase. It will crash your site for users the moment they hit a page with a missingwith()call. Use the environment check or a custom log handler. - Global Scope Over-restriction: Global scopes are powerful, but they can be problematic when you need to access cross-tenant data (e.g., in a background job that aggregates statistics across all tenants). Use
Model::withoutGlobalScope(TenantScope::class)when you intentionally need to bypass these constraints. - Performance Overhead of Scopes: While minor, remember that global scopes are applied to every query, including
exists()checks orcount(). Ensure yourtenant_idcolumn has an index to prevent these automated queries from becoming slow.
Summary
By leveraging preventLazyLoading and GlobalScopes, you transform Eloquent from a helpful abstraction into a strict guardian of your data integrity. This approach prevents common N+1 performance bottlenecks and provides a declarative way to handle multi-tenancy. You are now essentially "baking in" security and performance at the framework level.
Up next: We will dive into Advanced Subqueries and Joins to handle complex reporting requirements that go beyond simple model relationships.
Work with me

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.

FilamentPHP Admin Panel & Dashboard Development
A powerful admin panel for your Laravel app — built with FilamentPHP so you can manage everything without touching the database.