In one sentence

A relationship is a method on a model that tells Eloquent how two tables are linked, so you can write $user->posts instead of a join.

πŸ‘ͺThink of a family tree. You don't memorise everyone's ID number; you just say "my mother", "my children" or "my cousins". Relationships give your models the same kind of names for their connections.

Why it matters

Real data is connected

πŸ”—

No hand-written joins

$post->comments fetches the right rows. You describe the link once, in the model.

✍️

Create through the link

$user->posts()->create([...]) fills in user_id for you, so ownership is never forgotten.

🧭

Code that reads like the domain

"A post belongs to a user and has many comments" becomes two short methods that anyone can read.

The big picture

The blog's tables and how they link

1 β†’ many 1 β†’ many 1 β†’ many many ↔ many (via pivot) a user writes many comments users idnameemail categories idnameslug posts id user_id category_id title body comments id post_id user_id body post_tag post_id tag_id tags idname
Coloured columns are foreign keys. The dashed post_tag box is a pivot table: it only exists to link posts and tags.
RelationshipMeaningBlog exampleForeign key lives on…
hasOneowns exactly oneUser β†’ Profilethe other table (profiles.user_id)
hasManyowns manyUser β†’ Posts, Post β†’ Commentsthe other table (posts.user_id)
belongsTois owned by onePost β†’ User, Post β†’ Categorythis table (posts.user_id)
belongsToManymany on both sidesPost ↔ Tagsa pivot table (post_tag)
hasManyThroughreach across a middle tableCategory β†’ Comments (via Posts)the middle and far tables
Step by step

Wire up the blog

The tables come from the Migrations lesson. Now we teach the models about each other.

  1. hasMany and belongsTo: the everyday pair

    Most links come in pairs. The side without the foreign key says hasMany; the side with the foreign key says belongsTo. Eloquent guesses the key name (user_id) from the method and model names.

    
              use Illuminate\Database\Eloquent\Relations\HasMany;
    
              public function posts(): HasMany
              {
                  return $this->hasMany(Post::class);
              }
            
    
              use Illuminate\Database\Eloquent\Relations\BelongsTo;
              use Illuminate\Database\Eloquent\Relations\HasMany;
    
              public function user(): BelongsTo
              {
                  return $this->belongsTo(User::class);
              }
    
              public function category(): BelongsTo
              {
                  return $this->belongsTo(Category::class);
              }
    
              public function comments(): HasMany
              {
                  return $this->hasMany(Comment::class);
              }
            
  2. Property vs method: two ways to use a relationship

    Access it as a property to get the results. Call it as a method to get a query builder you can keep chaining or create through.

    
              $post->user->name;                     // the author (a User)
              $post->comments;                       // Collection of Comments
    
              $post->comments()->latest()->take(3)->get(); // a query you can refine
    
              // Create through the relationship: user_id is filled in for you
              $request->user()->posts()->create([
                  'title' => 'Relationships are neat',
                  'slug'  => 'relationships-are-neat',
                  'body'  => '...',
              ]);
            
    
              select * from `users` where `users`.`id` = 1 limit 1;
              select * from `comments` where `comments`.`post_id` = 7;
            
  3. hasOne: exactly one

    Use hasOne when each parent has at most one child, like a user's profile. It works like hasMany but returns a single model (or null).

    
              use Illuminate\Database\Eloquent\Relations\HasOne;
    
              public function profile(): HasOne
              {
                  return $this->hasOne(Profile::class); // profiles.user_id
              }
            
    
              $user->profile?->bio;  // ?-> because the profile might not exist yet
            
  4. belongsToMany: many-to-many via a pivot

    A post has many tags, and a tag has many posts. Neither table can hold the foreign key, so a pivot table sits in between. Laravel expects it to be named after both models, singular and alphabetical: post_tag.

    
              Schema::create('post_tag', function (Blueprint $table) {
                  $table->foreignId('post_id')->constrained()->cascadeOnDelete();
                  $table->foreignId('tag_id')->constrained()->cascadeOnDelete();
                  $table->primary(['post_id', 'tag_id']); // no duplicate pairs
              });
            
    
              use Illuminate\Database\Eloquent\Relations\BelongsToMany;
    
              public function tags(): BelongsToMany
              {
                  return $this->belongsToMany(Tag::class);
              }
              // ...and in Tag.php: return $this->belongsToMany(Post::class);
            

    Manage the links with attach, detach and sync:

    
              $post->tags()->attach([1, 2]);   // add tags 1 and 2
              $post->tags()->detach(2);        // remove tag 2
              $post->tags()->sync([1, 3, 4]);  // make it EXACTLY these tags (great for forms)
            
  5. hasManyThrough: a shortcut across tables

    Sometimes you want to skip a level. A category has many posts, and each post has many comments, so a category effectively "has many comments through posts".

    
              use Illuminate\Database\Eloquent\Relations\HasManyThrough;
    
              public function comments(): HasManyThrough
              {
                  return $this->hasManyThrough(Comment::class, Post::class);
              }
            
    
              $category->comments()->count();  // every comment on every post in this category
            
⚠️ Watch out for the N+1 problem

Looping over 20 posts and printing $post->user->name runs 1 query for the posts + 20 queries for the users. Fix it by loading the relationship up front with Post::with('user')->get(), which needs just 2 queries. The full story is in Eager Loading & N+1.

Common mistakes

Avoid this, do that

❌ Avoid

          Post::create([
              'title'   => $request->title,
              'user_id' => $request->user_id,
          ]);
        

Trusting a user_id from the form lets anyone post as someone else.

βœ… Do

          $request->user()->posts()->create([
              'title' => $request->title,
          ]);
        

Creating through the relationship sets user_id from the logged-in user.

❌ Avoid

          public function comments()
          {
              return $this->hasMany(Comment::class);
          }
          // ...then: $post->comments()->title
        

Calling the method gives you a query, not results, so ->title fails.

βœ… Do

          public function comments(): HasMany { /* ... */ }

          foreach ($post->comments as $comment) { /* ... */ }
        

Add a return type, and use the property when you want the actual records.

πŸ’‘ Pro tip: count without loading

Post::withCount('comments')->get() adds a comments_count attribute to each post using one extra subquery, without loading any comment rows.

πŸ‹οΈ Try it yourself
  1. Add comments() (hasMany) to User, and user() + post() (belongsTo) to Comment.
  2. Add posts() (hasMany) to Category.
  3. Create the post_tag pivot migration and the tags() / posts() methods on both models.
  4. In Tinker, attach two tags to a post, then sync to a different set and inspect $post->tags.
  5. Print each post's title with its author's name, first with Post::all(), then with Post::with('user')->get(), and compare the query counts in Debugbar.
Recap

Key takeaways

Check yourself

Mini quiz