Introduction

Eloquent's expressive CRUD API replaces raw SQL for the 95% case — find, create, update, delete, plus firstOrCreate/updateOrCreate for idempotent inserts and SoftDeletes for trash-can semantics.

Key Concepts

  • create / save: Two ways to persist a new record. create is one-shot mass assignment; save persists an instance you've populated.
  • find / findOrFail / first / firstOrFail: Retrieval helpers. The *OrFail variants throw ModelNotFoundException (auto-404 in HTTP).
  • firstOrCreate / updateOrCreate: Idempotent helpers — find by criteria and create (or update) if missing.
  • Dirty tracking (isDirty, wasChanged, getOriginal): Methods that tell you which attributes changed and what their prior values were.
  • Soft delete: The SoftDeletes trait + a nullable deleted_at column, giving you delete, restore, forceDelete, withTrashed, onlyTrashed.

Real World Context

Every controller action in a typical Laravel app is CRUD. Knowing Eloquent's idioms (findOrFail, firstOrCreate, chunk, cursor) keeps your code short, correct, and fast across a wide range of scales.

Deep Dive

Learn how to Create, Read, Update, and Delete records using Eloquent's expressive API.

Create Operations

Using save()

php
// Create instance, set properties, save
$post = new Post;
$post->title = 'My First Post';
$post->body = 'Post content here...';
$post->user_id = auth()->id();
$post->save();

// Now it has an ID
echo $post->id;  // 1

Using create()

php
// Create and save in one step
$post = Post::create([
    'title' => 'My First Post',
    'body' => 'Post content here...',
    'user_id' => auth()->id(),
]);

Note: create() requires the attributes to be in $fillable.

firstOrCreate and firstOrNew

php
// Find or create (saves to database)
$user = User::firstOrCreate(
    ['email' => 'john@example.com'],                       // Search criteria
    ['name' => 'John', 'password' => Hash::make('secret')] // Additional fields if creating
);

// Find or instantiate (doesn't save)
$user = User::firstOrNew(
    ['email' => 'john@example.com'],
    ['name' => 'John']
);
$user->save();  // Save manually

updateOrCreate (Upsert)

php
// Update if exists, create if not
$post = Post::updateOrCreate(
    ['slug' => 'my-post'],           // Search criteria
    ['title' => 'Updated Title', ...] // Values to update/create
);

Read Operations

Retrieving Single Records

php
// Find by primary key
$post = Post::find(1);

// Find multiple by primary key
$posts = Post::find([1, 2, 3]);

// Find or fail (throws ModelNotFoundException)
$post = Post::findOrFail(1);

// Find or 404
Route::get('/posts/{id}', function ($id) {
    $post = Post::findOrFail($id);  // Auto 404 if not found
    return view('posts.show', compact('post'));
});

// First matching record
$post = Post::where('slug', 'my-post')->first();
$post = Post::where('slug', 'my-post')->firstOrFail();

Retrieving Multiple Records

php
// All records
$posts = Post::all();

// With constraints
$posts = Post::where('published', true)->get();

// Multiple conditions
$posts = Post::where('published', true)
    ->where('category_id', 1)
    ->orderBy('created_at', 'desc')
    ->get();

// Or conditions
$posts = Post::where('featured', true)
    ->orWhere('views', '>', 1000)
    ->get();

Chunking for Large Datasets

php
// Process 100 records at a time
Post::chunk(100, function ($posts) {
    foreach ($posts as $post) {
        // Process each post
    }
});

// Using lazy() for memory efficiency
foreach (Post::lazy() as $post) {
    // Process one at a time
}

// Cursor for ultimate memory efficiency
foreach (Post::cursor() as $post) {
    // Hydrates one model at a time
}

Aggregates

php
$count = Post::count();
$max = Post::max('views');
$min = Post::min('views');
$avg = Post::avg('views');
$sum = Post::sum('views');

$exists = Post::where('slug', 'my-post')->exists();
$doesntExist = Post::where('slug', 'my-post')->doesntExist();

Update Operations

Updating Single Records

php
$post = Post::find(1);
$post->title = 'Updated Title';
$post->save();

// Or in one line
$post = Post::find(1);
$post->update(['title' => 'Updated Title']);

Mass Updates

php
// Update all matching records
Post::where('published', false)
    ->update(['status' => 'draft']);

// Increment/decrement
Post::where('id', 1)->increment('views');
Post::where('id', 1)->increment('views', 5);  // By 5
Post::where('id', 1)->decrement('stock');

// With additional updates
$post->increment('views', 1, ['last_viewed_at' => now()]);

Checking for Changes

php
$post = Post::find(1);
$post->title = 'New Title';

$post->isDirty();           // true - has unsaved changes
$post->isDirty('title');    // true
$post->isDirty('body');     // false
$post->isClean();           // false

$post->save();

$post->wasChanged();        // true - was just saved with changes
$post->wasChanged('title'); // true

Delete Operations

php
// Delete a single model
$post = Post::find(1);
$post->delete();

// Delete by primary key
Post::destroy(1);
Post::destroy([1, 2, 3]);
Post::destroy(1, 2, 3);

// Delete matching records
Post::where('published', false)->delete();

// Truncate (delete all)
Post::truncate();  // Warning: No events fired!

Soft Deletes

Keep records but mark as deleted:

php
use Illuminate\Database\Eloquent\SoftDeletes;

class Post extends Model
{
    use SoftDeletes;
}

Requires deleted_at column in migration:

php
$table->softDeletes();  // Adds deleted_at column

Usage:

php
$post->delete();  // Sets deleted_at, doesn't remove row

// Query soft deleted records
Post::withTrashed()->get();      // Include deleted
Post::onlyTrashed()->get();      // Only deleted

// Restore
$post->restore();

// Permanently delete
$post->forceDelete();

Common Pitfalls

  1. Using all() on large tables — Loads every row into memory and crashes on production data.
  2. truncate bypassing events — If your model fires events that clean up related data, truncate skips them.
  3. Forgetting soft-delete scopes — Queries against soft-deletable models exclude deleted rows by default; if you need them, call withTrashed().

Best Practices

  1. Prefer chunking (chunk, lazy, cursor) over all() beyond ~1000 rows — Keeps memory usage flat.
  2. Use findOrFail for required records — Let the framework 404 automatically instead of writing if (!$x) abort(404); boilerplate.
  3. Reach for firstOrCreate / updateOrCreate for idempotent inserts — Makes re-running seeders and imports safe.

Summary

  • Create with save() (instance-first) or create([...]) (mass assignment).
  • Read with find, findOrFail, first, firstOrFail, where(...)->get().
  • Update with update([...]) or mutate properties and call save().
  • Delete with delete(), destroy(id), or a filtered ->delete().
  • SoftDeletes trait + deleted_at column enables trash-can semantics with restore and forceDelete.
✓ Completed