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 😄


Total page views:

Comments

Popular posts from this blog

Useful aliases

Start all Docker containers