Laravel Eloquent ORM

Defining models, migrations, the Eloquent query builder, and hasMany/belongsTo relationships.

Defining a model

Eloquent is Laravel's ActiveRecord-style ORM — each model class maps to a database table, and an instance of that class maps to a single row. Generate one with Artisan, optionally alongside its migration in one command:

Bash
php artisan make:model Post -m
PHP
<?php
// app/Models/Post.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = ['title', 'body', 'published'];
}

By convention, Eloquent assumes the Post model maps to a posts table (the plural, snake_case form of the class name), with an auto-incrementing id primary key and created_at/updated_at timestamp columns — all overridable, but rarely needed to be. $fillable is an allow-list of columns that can be mass-assigned (via Post::create([...]) or $post->update([...])) — a required safeguard against a malicious request stuffing extra, unexpected fields into a form submission.

Migrations

A migration is a version-controlled, incremental description of a schema change — the PHP equivalent of Django's or Flask-Migrate's migration files:

Bash
php artisan make:migration create_posts_table
PHP
<?php
// database/migrations/xxxx_xx_xx_create_posts_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('body');
            $table->boolean('published')->default(false);
            $table->foreignId('author_id')->constrained('users')->cascadeOnDelete();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};
Bash
php artisan migrate

up() describes the change to apply; down() describes how to reverse it (used by php artisan migrate:rollback). $table->foreignId('author_id')->constrained('users')->cascadeOnDelete() is Laravel's shorthand for adding an author_id foreign key column referencing users.id, with the database itself deleting dependent posts when the referenced user is deleted.

Query builder basics

Every Eloquent model comes with a fluent, chainable query interface:

PHP
<?php

use App\Models\Post;

// Create
$post = Post::create([
    'title' => 'Hello, Laravel',
    'body' => 'My first post.',
]);

// Read — all rows
$allPosts = Post::all();

// Read — filtered and ordered
$recentPublished = Post::where('published', true)
    ->orderBy('created_at', 'desc')
    ->take(5)
    ->get();

// Read — a single row, or a 404 if it doesn't exist
$post = Post::findOrFail(1);

// Update
$post->title = 'Hello, Laravel (Updated)';
$post->save();

// or, in one call:
$post->update(['title' => 'Hello, Laravel (Updated)']);

// Delete
$post->delete();

Post::where(...) returns a query builder instance you can keep chaining (->orderBy(), ->take(), ->where() again for an additional condition) — nothing actually hits the database until a terminal method like ->get(), ->first(), or ->count() is called.

Relationships: hasMany and belongsTo

Relationships are defined as methods on the model, and Eloquent uses naming conventions (a user_id column, the calling method's name) to infer the foreign key unless told otherwise:

PHP
<?php
// app/Models/User.php

class User extends Authenticatable
{
    public function posts(): \Illuminate\Database\Eloquent\Relations\HasMany
    {
        return $this->hasMany(Post::class);
    }
}
PHP
<?php
// app/Models/Post.php

class Post extends Model
{
    protected $fillable = ['title', 'body', 'published', 'author_id'];

    public function author(): \Illuminate\Database\Eloquent\Relations\BelongsTo
    {
        return $this->belongsTo(User::class, 'author_id');
    }
}
PHP
<?php

$user = User::find(1);
$user->posts;              // every Post where author_id = 1 (a Collection)

$post = Post::find(5);
$post->author;             // the related User instance
$post->author->name;

Accessing $user->posts or $post->author as a property (not a method call) triggers Eloquent to run the relationship's query and cache the result on the model instance — calling it again on the same instance doesn't re-query.

Common mistakes

  • Forgetting $fillable (or the inverse, $guarded) on a model and then calling Model::create($request->all()) — this opens the door to mass assignment vulnerabilities, where a request can set columns it was never meant to touch.
  • Looping over a relationship in a way that triggers one query per row (the N+1 query problem) — e.g., foreach ($posts as $post) { $post->author->name } runs one query per post. Eager-load it instead with Post::with('author')->get(), which fetches all authors in a single additional query.
  • Confusing $post->save() (updates whatever is currently set on the model instance) with Post::create([...]) (mass-assigns and inserts a brand-new row) — using create() on an existing model doesn't update it, it inserts a second row.

Interview questions

Q: What's the difference between using Eloquent and using the plain query builder directly? Eloquent gives you an ActiveRecord-style model — an object with attributes, relationships, events, and behavior (accessors, casts, scopes) layered on top of a table, well suited to expressing business logic. The plain query builder (DB::table('posts')->where(...)->get()) returns plain stdClass objects (or arrays) with none of that model behavior attached, and is often a better fit for a large reporting query, bulk operation, or anything where the overhead and object identity of a full Eloquent model isn't needed. Under the hood, Eloquent is actually built on top of the query builder.

Q: What is the N+1 query problem, and how does Laravel help you avoid it? It's the pattern where fetching a list of records (1 query) and then accessing a relationship on each one inside a loop triggers one additional query per record (N queries) — for 100 posts, that's 101 total queries just to show each post's author. Laravel's fix is eager loading: Post::with('author')->get() fetches every post's related author in a single additional query up front, regardless of how many posts there are, and $post->author inside the loop then reads from that already-loaded data instead of triggering a new query.