ORM — Object-Relational Mapper
debt(d7/e3/b7/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints.tools field is empty/unspecified. N+1 query problems are silent in normal development — the app works correctly but issues hundreds of queries. Detection requires deliberate query logging (e.g. Laravel Debugbar, query log inspection) or noticing slow pages under load. No default linter or compiler catches lazy loading in loops; it takes runtime profiling or careful code review to spot.
Closest to 'simple parameterised fix' (e3). The quick_fix confirms the fix is a targeted pattern replacement: swap Post::all() with Post::with(['author', 'comments'])->get(). This is a small, localised change — typically one or a few lines per offending query — but requires identifying every affected call site, which may span several controllers or services within a component.
Closest to 'strong gravitational pull' (b7). ORM is the default data access layer for Laravel/Symfony (applies_to: web, cli) and shapes every database interaction across the codebase. Relationship loading patterns, query construction habits, and architectural decisions (raw SQL vs ORM) are pervasive. Every feature touching the database is influenced by ORM choices, and switching or significantly changing the ORM approach would require cross-cutting refactoring.
Closest to 'serious trap' (t7). The misconception field documents a two-level trap: developers first believe ORMs are inherently slow (wrong — the ORM is not the bottleneck), then fall into lazy loading in loops which genuinely causes N+1 queries. This contradicts developer intuition — code that looks clean and correct ($post->author->name in a loop) silently degrades performance. It contradicts how similar iteration patterns behave in non-ORM contexts where there's no hidden per-iteration DB call.
Also Known As
TL;DR
Explanation
An ORM bridges the object-oriented PHP world and the relational database world. Instead of writing SQL, you work with PHP objects (Eloquent models, Doctrine entities) and the ORM generates appropriate SQL. Two main patterns: Active Record (Eloquent in Laravel) — the model is both the domain object and the database gateway; User::find(1) issues SELECT and returns a User object; Doctrine in Data Mapper mode — entities are plain PHP objects, a separate Repository class handles persistence. ORM advantages: no raw SQL boilerplate; database-agnostic code (switch MySQL to PostgreSQL by changing config); built-in relationships (hasMany, belongsTo) with lazy loading; migrations. ORM pitfalls: lazy loading causes N+1 queries (loading 100 posts then accessing post->author triggers 100 individual author queries); ORM-generated SQL is not always optimal; complex reporting queries are easier in raw SQL.
Common Misconception
Why It Matters
Common Mistakes
- Using lazy loading in loops — always eager-load relationships with with() when you know you will access them.
- Calling ->get() and then ->count() separately — use ->count() alone which generates SELECT COUNT(*), not SELECT * then PHP count().
- Using Model::all() on large tables — always add ->limit() or pagination to prevent loading entire tables into memory.
- Over-using ORMs for reporting queries with multiple JOINs and aggregations — raw SQL with DB::select() is often cleaner and more readable for complex analytics.
Code Examples
// N+1 — 1 query for posts + 1 per post for author
$posts = Post::all(); // SELECT * FROM posts
foreach ($posts as $post) {
echo $post->author->name; // SELECT * FROM users WHERE id = ? (×N)
echo $post->comments->count(); // SELECT * FROM comments WHERE... (×N)
}
// Eager loading — 3 queries total regardless of post count
$posts = Post::with(['author', 'comments'])->paginate(20);
// SELECT * FROM posts LIMIT 20
// SELECT * FROM users WHERE id IN (1,2,3...)
// SELECT * FROM comments WHERE post_id IN (1,2,3...)
foreach ($posts as $post) {
echo $post->author->name; // no query — already loaded
echo $post->comments->count(); // no query — already loaded
}