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.createis one-shot mass assignment;savepersists an instance you've populated.find/findOrFail/first/firstOrFail: Retrieval helpers. The*OrFailvariants throwModelNotFoundException(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
SoftDeletestrait + a nullabledeleted_atcolumn, giving youdelete,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:
phpuse 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
- Using
all()on large tables — Loads every row into memory and crashes on production data. truncatebypassing events — If your model fires events that clean up related data,truncateskips them.- Forgetting soft-delete scopes — Queries against soft-deletable models exclude deleted rows by default; if you need them, call
withTrashed().
Best Practices
- Prefer chunking (
chunk,lazy,cursor) overall()beyond ~1000 rows — Keeps memory usage flat. - Use
findOrFailfor required records — Let the framework 404 automatically instead of writingif (!$x) abort(404);boilerplate. - Reach for
firstOrCreate/updateOrCreatefor idempotent inserts — Makes re-running seeders and imports safe.
Summary
- Create with
save()(instance-first) orcreate([...])(mass assignment). - Read with
find,findOrFail,first,firstOrFail,where(...)->get(). - Update with
update([...])or mutate properties and callsave(). - Delete with
delete(),destroy(id), or a filtered->delete(). SoftDeletestrait +deleted_atcolumn enables trash-can semantics withrestoreandforceDelete.