How to Use Laravel Eloquent: Querying, Updating and Deleting, Verified in Code
Table Of Contents
- The Environment I Tested On
- How to Configure Eloquent
- The Basics of get()
- The Basics of first()
- findOrFail()
- The Basics of where()
- The Basics of orderBy()
- The Basics of count()
- The Basics of withCount()
- The Basics of exists()
- The Basics of has()
- The Basics of with()
- The Basics of load()
- The Basics of join()
- The Basics of update()
- The Basics of delete()
A hands-on tour of Eloquent on Laravel 13 + SQLite, building a support ticket board and checking the return value and the actual SQL for where, orderBy, get, first, count, exists, with, join, update and soft deletes.
I set up Laravel 13 + SQLite in Docker and built a "support ticket board" while going through the Eloquent methods I reach for most often.
Each section follows the same order: the screen I wanted to build, the code and its output, then the return value and the SQL that was actually issued.
By the way, Eloquent is usually pronounced the way it reads in English, "eloquent".
The Environment I Tested On
Every result below was measured in this environment.
| Item | Value |
|---|---|
| Laravel Framework | 13.31.0 |
| PHP | 8.3.33 (NTS) |
| Database | SQLite 3.46.1 |
The SQL comes from DB::enableQueryLog().
Test Data
6 rows in the tickets table
3 rows in the customers table
Only id = 4 in tickets has a NULL customer_id, meaning no customer is linked to it.
How to Configure Eloquent
This is where you decide which database to connect to, and with which settings.
Most of it lives in config/database.php, and you override the values from .env.
The demo has exactly one DB-related line in .env.
.env
DB_CONNECTION=sqliteconfig/database.php looks like this.
'default' => env('DB_CONNECTION', 'sqlite'),
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => null,
'journal_mode' => null,
'synchronous' => null,
],You can check the resolved values with php artisan config:show.
When a change doesn't seem to apply, it is almost always the config cache, and this command clears it.
php artisan config:clearTo confirm the connection itself, use php artisan db:show.
php artisan db:show
SQLite .............................................................. 3.46.1
Connection .......................................................... sqlite
Database .......................................... database/database.sqlite
Tables .................................................................. 11The Basics of get()
get() is what you use when you want to fetch multiple rows.
It renders the list of open tickets on the index screen.
The table part of this screen is built by get().
I called get() with a condition.
$tickets = Ticket::get();get() does not return an array. It returns a Collection, which behaves much like an associative array.
The Basics of first()
first() is what you use when you want a single row.
Think of the "next ticket to handle" card on the dashboard, which shows only the highest priority open ticket.
I called first() with sorting applied.
$t = Ticket::query()
->where('status', 'open') //rows whose status column is open
->orderByDesc('priority') //highest priority first
->orderBy('id') //then by id
->first(); //take one of themget() returns an empty Collection when nothing matches, but first() returns null.
That is the biggest difference from get(), so you need a null check before touching a property such as $t->subject!
findOrFail()
findOrFail() also fetches a single row, but it returns an HTTP 404 response instead of null.
It saves you from writing abort(404) by hand when someone passes an ID that doesn't exist, so
it is handy on detail screens
$t = Ticket::findOrFail($ticketId);The Basics of where()
where() is what you use to narrow down rows.
Say the search form lets you filter the list by status and subject.
I passed both the status and the subject.
$r = Ticket::query()
->where('status', 'open')
->where('subject', 'like', '%請求書%') //"invoice" in Japanese
->get();
echo $r->count(); // 2
echo $r->pluck('id')->implode(', '); // 2, 5SQL[1] select * from "tickets" where "status" = ? and "subject" like ? and "tickets"."deleted_at" is null
-- bindings: ["open","%請求書%"]Chaining where() combines the conditions with and.
With two arguments, it compares with =.
Ticket::query()->where('priority', 2)->get();With three arguments, the second one becomes the comparison operator.
Ticket::query()->where('priority', '>=', 2)->get();The Basics of orderBy()
orderBy() changes the order of the results.
To float urgent tickets to the top of the list, I sorted by priority in descending order.
$r = Ticket::query()->orderByDesc('priority')->get();The Basics of count()
count() returns just the number of rows.
To show "4 open" at the top of the screen, I called count() with the filter applied.
$c = Ticket::query()->where('status', 'open')->count();
echo get_debug_type($c); // int
echo $c; // 4The Basics of withCount()
withCount() returns the number of related rows.
I used it to count the tickets per customer.
$customers = Customer::query()->withCount('tickets')->get();
// A商事 tickets_count=int 2
// B工業 tickets_count=int 2
// C制作 tickets_count=int 1Each customer record gains a tickets_count column holding the count.
The Basics of exists()
exists() tells you whether at least one matching row exists, as a true or false value.
I used it to check whether there is any open ticket at all.
$e = Ticket::query()->where('status', 'open')->where('priority', 3)->exists();
echo get_debug_type($e); // bool
var_export($e); // trueYou can do something similar with count() or first(), but
count() counts every matching row and first() instantiates a model object for one row, while
exists() only reads back a boolean without pulling the data into memory, which makes it the lighter option.
doesntExist() is the opposite of exists(): it returns true when nothing matches.
The Basics of has()
has() narrows the query down to rows that have related records.
Use it when you want "customers with at least one ticket".
Customer::query()->has('tickets')->pluck('name');
// ["A商事","B工業","C制作"]To add a condition to the relation, use whereHas().
Customer::query()
->whereHas('tickets', fn ($q) => $q->where('priority', 3)) //customers with an urgent ticket
->pluck('name');
// ["A商事"]The SQL it issued was an exists subquery.
select "name" from "customers" where exists (select * from "tickets" where "customers"."id" = "tickets"."customer_id" and "priority" = ? and "tickets"."deleted_at" is null)There is no need to fetch every customer and loop over them in PHP.
The Basics of with()
with() fetches the related records together with the main query.
Use it when each row of the list also has to show the customer name.
$tickets = Ticket::query()
->with('customer') //loads the relation defined on the Ticket model
->get();To limit it to the columns you need, list them after the relation name.
Ticket::query()->with(['customer:id,name'])->get();This is called eager loading. Because everything is fetched up front, it avoids the N+1 problem.
The Basics of load()
load() is the same idea as with(), but for a Collection you have already fetched.
$tickets = Ticket::query()->orderBy('id')->get();
$tickets->load('customer');Both of these matter for keeping server response times down.
Laravel の表示速度をあげたいとき、実装コストに対して効果が高いものはコレ!
The Basics of join()
join() pulls columns from another table in a single SQL statement.
It is essentially the join you already know from SQL.
Use it when you want to filter or sort by the customer name.
$r = Ticket::query()
->leftJoin('customers', 'customers.id', '=', 'tickets.customer_id')
->select('tickets.*', 'customers.name as customer_name')
->orderBy('tickets.id')
->get();The difference from with is that the column is attached to the Ticket record itself.
You get flat columns side by side instead of a relation.
Unless you have a specific reason, I would recommend using with and the relation rather than a JOIN.
Without select(), the id gets overwritten by the customer's ID
When column names collide, one overwrites the other, and select() is how you avoid it.
->select('tickets.id', 'customers.id as customer_id')Renaming the duplicated column with as, as shown above, lets you use both.
join() drops tickets with no customer
id = 4 has a NULL customer_id, so join() (INNER JOIN) has nothing to match and the row disappears from the result.
Use leftJoin() when you want every row.
The Basics of update()
update() updates a record.
It backs the "mark as done" action on the list screen.
$ticket = Ticket::findOrFail(1);
$result = $ticket->update(['status' => 'done']);This only works when mass assignment is configured on the Ticket model.
#[Fillable(['customer_id', 'subject', 'body', 'priority','status'])] //UPDATE won't work unless status is listedAlternatively, enabling preventSilentlyDiscardingAttributes() turns the silent failure into an exception.
Model::preventSilentlyDiscardingAttributes();
Ticket::findOrFail(1)->update(['status' => 'done']);save() works too
For columns you don't want writable from outside, assign the property and call save().
$ticket = Ticket::findOrFail(1);
$ticket->status = 'done';
$ticket->save();The Basics of delete()
delete() deletes a record.
With soft deletes, the row stays in the database but disappears from the application, which is what "logical deletion" means.
In Laravel you enable it by adding use SoftDeletes; to the Ticket model.
The row is soft deleted once a timestamp lands in deleted_at.
$ticket = Ticket::findOrFail(2);
$deleted = $ticket->delete();Fetch the list again after deleting, and id = 2 is gone.
To include deleted rows use withTrashed(), and to count only the deleted ones use onlyTrashed().
To bring a row back, restore() puts null into deleted_at again.
Ticket::withTrashed()->findOrFail(2)->restore(); // trueWhen you really want the row gone, use forceDelete().
Ticket::findOrFail(6)->forceDelete(); // trueThat covers the Eloquent methods I use most, written up while actually building with them.




