A one-to-many relationship in Laravel 13 means one parent model owns many child models, for example one Post with many Comment rows. You define hasMany() on the parent, belongsTo() on the child, and store a foreign key such as post_id on the child table.
This tutorial builds that setup step by step, then shows how to create, query and eager load related records without N+1 queries.
What do I need first?
- Laravel 13 (current stable when this was written) and a PHP version it supports.
- A configured database connection.
- Two models:
Post(parent) andComment(child).
How do I create the models and migrations?
php artisan make:model Post -m
php artisan make:model Comment -mThe child table needs a foreign key. foreignId()->constrained() creates the column and the database constraint in one line.
// posts migration
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body');
$table->timestamps();
});
// comments migration
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
$table->string('author');
$table->text('body');
$table->timestamps();
});php artisan migratecascadeOnDelete() removes comments when their post is deleted. Leave it out if you want deletes to fail while comments exist.
How do I define hasMany and belongsTo?
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Post extends Model
{
protected $fillable = ['title', 'body'];
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
}<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Comment extends Model
{
protected $fillable = ['author', 'body'];
public function post(): BelongsTo
{
return $this->belongsTo(Post::class);
}
}Laravel guesses the foreign key from the parent name, so Post gives post_id. If your column has another name, pass it as the second argument: hasMany(Comment::class, 'article_id').
How do I create related records?
Call create() on the relationship. Laravel fills post_id for you.
$post = Post::create(['title' => 'Hello', 'body' => 'First post']);
$post->comments()->create([
'author' => 'Sara',
'body' => 'Nice article.',
]);
$post->comments()->createMany([
['author' => 'Ali', 'body' => 'Thanks for sharing.'],
['author' => 'Noor', 'body' => 'Very clear.'],
]);Note that post_id is not in $fillable. You do not need it there because the relationship sets it.
How do I read related records?
$post = Post::find(1);
foreach ($post->comments as $comment) {
echo $comment->author;
}
$recent = $post->comments()->latest()->take(5)->get();
$comment = Comment::find(1);
echo $comment->post->title;Use $post->comments (no parentheses) to get the loaded collection. Use $post->comments() to get a query builder you can keep filtering.
How do I avoid the N+1 query problem?
Looping over posts and reading $post->comments inside the loop runs one extra query per post. Eager load instead.
// 2 queries total, not 1 + N
$posts = Post::with('comments')->latest()->paginate(10);
// count without loading the comments
$posts = Post::withCount('comments')->get();
echo $posts[0]->comments_count;In development you can make Laravel throw an exception on accidental lazy loading by calling Model::preventLazyLoading(! app()->isProduction()); in AppServiceProvider::boot().
How do I filter parents by their children?
// posts that have at least one comment
$posts = Post::has('comments')->get();
// posts with 3 or more comments
$posts = Post::has('comments', '>=', 3)->get();
// posts with a comment by Sara
$posts = Post::whereHas('comments', fn ($q) => $q->where('author', 'Sara'))->get();What are the common mistakes?
- Missing foreign key column. The error "Unknown column post_id" means the migration or key name is wrong.
- Forgetting eager loading. Pages get slower as rows grow. Check the query count with Laravel Debugbar or Telescope in development.
- Using the property when you need the builder.
$post->comments->where(...)filters in memory.$post->comments()->where(...)filters in SQL. - Mass assignment errors. Add child fields to
$fillableor use$guardeddeliberately.
How do I verify it works?
Open php artisan tinker and run the examples below, or write a feature test.
public function test_post_has_many_comments(): void
{
$post = Post::create(['title' => 'T', 'body' => 'B']);
$post->comments()->create(['author' => 'A', 'body' => 'C']);
$this->assertCount(1, $post->fresh()->comments);
$this->assertSame($post->id, $post->comments->first()->post->id);
}FAQ
What is the difference between hasMany and belongsTo?
hasMany sits on the parent that owns many rows. belongsTo sits on the child that stores the foreign key.
Can I order the related records by default?
Yes. Chain it in the relationship: $this->hasMany(Comment::class)->latest().
Should I add an index to the foreign key?
foreignId()->constrained() creates a foreign key constraint, which most databases index. Check your database if you create the column another way.



