Join vs Model vs Relationship in Laravel
Understanding SQL joins, Eloquent relationships, and model accessors.
1) Join (Database level)
A join is a database operation used to combine rows from multiple tables. It happens at the SQL level and is focused on performance and filtering.
Example:
SELECT users.name, posts.title
FROM users
JOIN posts ON posts.user_id = users.id;
- Runs in the database
- Returns raw rows
- Very fast
2) Relationship (Eloquent ORM level)
A relationship defines how models are connected. It represents business meaning, not just raw data.
class User extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
}
Usage:
$user->posts;
- Returns model objects
- Supports lazy & eager loading
- Encodes domain logic
3) Model Accessor (Presentation level)
An accessor is a computed attribute on a model. It does not fetch data from the database.
public function getFullNameAttribute()
{
return "{$this->first_name} {$this->last_name}";
}
- No database query
- Pure PHP logic
- Used for formatting or derived values
Quick comparison
| Concept | Level | Purpose |
|---|---|---|
| Join | Database | Combine tables |
| Relationship | ORM | Connect models |
| Accessor | Model | Transform data |
Rule of thumb
- Need speed & filtering? → Join
- Navigating data? → Relationship
- Formatting / computed values? → Accessor
Lazy vs Eager Loading
1) Lazy Loading (default behavior)
What it is:
Data is loaded only when you access it.
Think of it like:
“I’ll fetch related data if and when I actually need it.”
Example:
public function getFullNameAttribute()
$users = User::all(); // 1 query
foreach ($users as $user) {
echo $user->posts->count(); // triggers a query EACH time
}
What actually happens:
SELECT * FROM users; -- 1 query
SELECT * FROM posts WHERE user_id=1; -- N queries
SELECT * FROM posts WHERE user_id=2;
SELECT * FROM posts WHERE user_id=3;
...
This is the famous N+1 problem 😬
Pros:
- Simple
- Fine for one record
Cons:
- Can destroy performance in loops
- Hidden queries (hard to notice)
2) Eager Loading (recommended for collections)
What it is:
You tell Laravel upfront which relationships you’ll need.
Think of it like:
“I know I’ll need this data — fetch it now.”
Example:
$users = User::with('posts')->get(); // 2 queries total
foreach ($users as $user) {
echo $user->posts->count(); // no extra queries
}
What actually happens:
SELECT * FROM users;
SELECT * FROM posts WHERE user_id IN (1, 2, 3, ...);
Pros:
- Solves N+1
- Predictable query count
- Clean code
Cons:
- Can load unused data if overused
3) Lazy Eager Loading (best of both worlds)
What it is:
Load relationships after the main query, but in bulk.
Example:
$users = User::all(); // 1 query
$users->load('posts'); // 1 more query (not N)
foreach ($users as $user) {
echo $user->posts->count();
}
When to use it:
- You don’t know in advance if you’ll need the relationship
- A condition decides later
4) Eager loading with constraints
You can filter what gets loaded:
$users = User::with(['posts' => function ($q) {
$q->where('published', true);
}])->get();
Only published posts are loaded — still just 2 queries.
5) withCount (super common & efficient)
If you only need counts:
$users = User::withCount('posts')->get();
echo $user->posts_count;
This uses a subquery and does not load posts at all.
6) Quick comparison table
| Type | When queries run | Query count | Best use case |
|---|---|---|---|
| Lazy loading | On access | N+1 risk | Single record |
| Eager loading | Before access | Predictable | Lists, loops |
| Lazy eager loading | After fetch | Efficient | Conditional logic |
Rule of thumb (memorize this)
- Looping? → EAGER LOAD
- Single model? → Lazy is fine
- Need counts only? → withCount
- Unsure yet? → load()
Pro tip:
Turn on query logging or use Laravel Debugbar once — seeing lazy loading in action is eye-opening 😄
Comments
Post a Comment