Building Web Apps with Laravel Module 5 – Databases and Eloquent
Eloquent models, mass assignment, soft deletes
Set up Eloquent models for Pathshala's fees, turn on strict mode, upsert a thousand instalments in one statement, and soft-delete a row you can restore.
What you will learn
- Set up an Eloquent model with Fillable and switch on strict mode outside production
- Create, read, update and delete rows, with firstOrCreate and updateOrCreate
- Write one upsert that saves a thousand rows in one statement, and read the query count to prove it
- Use the SoftDeletes trait to soft-delete, restore and permanently delete rows
- Choose between an auto-increment, UUID or ULID key, and still authorize every access
Before you start
What you’ll learn · Before you start
What you will learn
- Set up an Eloquent model with Fillable and switch on strict mode outside production
- Create, read, update and delete rows, with firstOrCreate and updateOrCreate
- Write one upsert that saves a thousand rows in one statement, and read the query count to prove it
- Use the SoftDeletes trait to soft-delete, restore and permanently delete rows
- Choose between an auto-increment, UUID or ULID key, and still authorize every access
Before you start
On this page
Pathshala’s accountant imports fee instalments from a spreadsheet of 100 students at a time: 1,000 rows a term. If
each row runs its own insert, that is 1,000 round trips through the database driver; if one of those rows has a key
nobody declared on the model, the row is written anyway and the extra field is dropped in silence. This lesson
settles both problems: a model with a short Fillable list, a boot method that turns on strict mode outside
production, and an upsert call whose query count the probe prints as 1.
Along the way you will see what each strict-mode guard refuses, read a soft-deleted row with withTrashed, restore
it, and give one Pathshala table a ULID key while still writing the authorization that an unguessable id does not
replace.
A model is a row, and strict mode makes the slips loud
An Eloquent model: A PHP class that stands for one database table, each of its objects for one row: the Notice model queries the notices table, and saving a Notice object writes its row. More in “Your first CRUD feature with Eloquent” is a PHP class that stands for one database row. Pathshala’s fee
sheet needs two of them, kept in app/Models/: Student for one row of the students table, and FeeInstalment for
one row of the fee_instalments table. This lesson’s migration: A schema change written as a PHP class with an up method that applies it and a down method that undoes it, kept in database/migrations and run in file-name order. More in “Your first CRUD feature with Eloquent” gives the fee_instalments
table a unique index on (student_id, due_on), so no two instalments belong to the same student on the same date;
the model is what the rest of the code uses to read and write that 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('fee_instalments', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('student_id')->index();
$table->date('due_on');
$table->unsignedInteger('amount_paise');
$table->string('status', 10)->default('due');
$table->softDeletes();
$table->timestamps();
$table->unique(['student_id', 'due_on']);
});
}
public function down(): void
{
Schema::dropIfExists('fee_instalments');
}
}; <?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable(['name', 'phone'])]
class Student extends Model
{
public function instalments(): HasMany
{
return $this->hasMany(FeeInstalment::class);
}
} <?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
#[Fillable(['student_id', 'due_on', 'amount_paise', 'status'])]
class FeeInstalment extends Model
{
use SoftDeletes;
protected $casts = [
'due_on' => 'date',
];
public function student(): BelongsTo
{
return $this->belongsTo(Student::class);
}
} The #[Fillable([...])] attribute names the four columns a request array may write to a fee instalment, and nothing
else. That is the mass assignment: Creating or updating a model from a whole array of fields in one call, which is why a model declares the columns such an array is allowed to write. More in “Your first CRUD feature with Eloquent” boundary from the first-feature module: whatever
is not on the list is dropped before Eloquent builds the insert.
Dropped silently, by default. In development, three default behaviours of Eloquent hide bugs you want to see at
once: lazy loading a relationship inside a loop runs an extra query per row, which only shows up when the page is
slow; a request array with a stray key writes the row without reporting the key; and selecting a subset of columns
then reading one of the others returns null, which looks like an empty column. Laravel offers one method,
Model::shouldBeStrict(), that turns all three into exceptions.
<?php
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
//
}
public function boot(): void
{
Model::shouldBeStrict(! $this->app->isProduction());
}
} strict mode: A development-only Eloquent setting that turns three default behaviours into exceptions: lazy loading of a relationship, writing a column the model has not declared, and reading a column the query did not select. only runs when isProduction() is false, so a production site keeps its old
habit. The probe below
seeds two students and four instalments without the guards, turns them on, and provokes each in turn, catching the
exception each throws:
from session import run
# Model::shouldBeStrict() turns on three guards at once: no lazy loading, no silent discard of unfillable keys,
# and no reading of a column the query did not select. The probe below seeds two students and four instalments
# without those guards, then switches them on and provokes each in turn, catching the exception each throws.
run(
"""
php artisan migrate --no-interaction
php strict_mode_probe.php
""",
app=True,
overlay=("pathshala-model", "strict-mode"),
) Output
$ php artisan migrate --no-interaction INFO Running migrations. 2026_01_05_090000_create_students_table ............................... DONE 2026_01_05_091000_create_fee_instalments_table ........................ DONE $ php strict_mode_probe.php Guard 1: lazy loading inside a loop Illuminate\Database\LazyLoadingViolationException: Attempted to lazy load [instalments] on model [App\Models\Student] but lazy loading is disabled. Guard 2: an array key the model does not allow Illuminate\Database\Eloquent\MassAssignmentException: Add fillable property [written_off] to allow mass assignment on [App\Models\FeeInstalment]. Guard 3: a column the query did not select Illuminate\Database\Eloquent\MissingAttributeException: The attribute [name] either does not exist or was not retrieved for model [App\Models\Student].
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 strict_mode_session.py
Three exceptions arrived in order and named the model and the attribute each time:
LazyLoadingViolationException for the instalments relation accessed inside the loop,
MassAssignmentException for the written_off key the model does not declare, and MissingAttributeException for
the name column the select('id') query left out.
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- One line of boot. shouldBeStrict() turns on three guards at once. In production the line is skipped, so the running site keeps its old habit of fetching one row at a time, dropping unknown keys and reading NULL for a column the query did not select.
- A relation inside a loop. A loop over students that reads $student->instalments runs one extra query per student. With the first guard on, Eloquent throws LazyLoadingViolationException instead, and names the relation it refused to load.
- An extra array key. FeeInstalment::create() passed an extra key the Fillable list does not name. With the second guard on, the call throws MassAssignmentException and names the column, instead of writing the row and ignoring the extra silently.
- A column that was not selected. Student::select('id')->first() left out the name column, then the code read $student->name. With the third guard on, Eloquent throws MissingAttributeException with the model and the column, so the developer sees that the select left it out.
A column default is not an answer
Student::select('id')->first()->name returns null while the guard is off, and plenty of code takes that for the
value Eloquent held for the row. The third guard refuses it with an exception that names the model and the
attribute, so the fix (adding the column to the select() or asking for the row with find()) is a one-line
change you make at the point of the slip.
Checkpoint
Check: Strict mode guards
Create, read, update and delete, with firstOrCreate and updateOrCreate
Every row’s life passes through four actions, and Eloquent gives each a short name. create() builds a row from a
Fillable array and saves it; find() reads one row by its primary key, where()->first() by any column; a model
instance’s property setter plus save() or an array passed to update() writes the changed row back; delete()
removes it, or marks it as deleted when the model is soft-deletable.
Two more combined moves cover the common cases. firstOrCreate([keys], [defaults]) reads the row that matches the
first array and inserts it with the two arrays merged if there is no match. updateOrCreate([keys], [values]) is
the write version: it finds the matching row and sets values on it, or inserts the merged arrays. The
wasRecentlyCreated property says which side it took, so the controller after it knows whether to log a new row or
a changed one.
<?php
use App\Models\FeeInstalment;
// read or create: idempotent on the keys array alone.
$apr = FeeInstalment::firstOrCreate(
['student_id' => 7, 'due_on' => '2026-04-05'],
['amount_paise' => 500000, 'status' => 'due'],
);
// write or create: the values on the right are the write.
$may = FeeInstalment::updateOrCreate(
['student_id' => 7, 'due_on' => '2026-05-05'],
['amount_paise' => 550000, 'status' => 'due'],
);
if ($may->wasRecentlyCreated) {
// the row was inserted this call; log it to the audit trail.
} The keys of the first array must land on columns the database agrees are unique together, which is why the
migration gave (student_id, due_on) its unique index. Two accountants running firstOrCreate on the same month
produce one row, not two: the database refuses the second insert, and firstOrCreate then reads the row the first
one wrote.
Checkpoint
Check: Create, read, update, delete
Upsert changes a thousand rows in one statement
A spreadsheet of 1,000 instalments is 1,000 inserts if every row goes on its own, and 1,000 round trips through the
database driver. upsert() sends them as one statement: the first array is every row to write, the second names
the columns that make a row unique, and the third names the columns to overwrite when a matching row already
exists. Laravel’s own documentation calls it “a single, atomic operation” that updates or inserts in one call. The
probe below builds 1,000 rows (100 students over 10 months), upserts them twice and reads back the query log.
upsert: A single SQL statement that writes many rows: the database matches each row against a unique index and either inserts it or writes the given columns over the matching row. takes one statement whatever the number of rows:
from session import run
# upsert() sends every row of the array in one statement, matching on the unique columns of the
# second argument and updating only the columns of the third. The probe builds 1,000 instalments
# for 100 students over 10 months, upserts them twice, and prints the query count each time.
run(
"""
php artisan migrate --no-interaction
php upsert_fees_probe.php
""",
app=True,
overlay=("pathshala-model", "upsert-fees"),
) Output
$ php artisan migrate --no-interaction
INFO Running migrations.
2026_01_05_090000_create_students_table ............................... DONE
2026_01_05_091000_create_fee_instalments_table ........................ DONE
$ php upsert_fees_probe.php
Rows to upsert: 1000
Queries run by the first upsert: 1
Statement: insert into "fee_instalments" ("amount_paise", "created_at", "due_on", "status", "student_id", "updated_at") values ...
Rows in fee_instalments: 1000
Second upsert, with changed amounts:
Queries run: 1
Rows in fee_instalments: 1000
Updated amount of (student 1, due 2026-01-05): 500100 paise
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 upsert_fees_session.py
The query log printed Queries run by the first upsert: 1 and Rows in fee_instalments: 1000, which is the whole
promise: one statement, every row written. The second call changed every amount from 500000 to 500100 paise, and
still ran one query; the row of student 1 due on 2026-01-05 came back with 500100 paise, so the matching rows were
updated instead of duplicated.
Key idea
upsert($rows, $uniqueBy, $updateColumns) is for row sets that come from outside, like a payroll upload or a daily
sync: one statement a batch, the matching columns pinned by the migration’s unique index, the overwrite
controlled by naming exactly the columns the import may touch.
The database decides what “matching” means. The documentation notes that every driver except SQL Server needs the columns of the second argument to have a primary or unique index, and MySQL and MariaDB ignore the second argument and read every unique index of the table. Pin the match down with the index you declared.
Checkpoint
Check: Upsert in one statement
Soft deletes keep the row and let you restore it
An accountant who deletes an instalment should still be able to put it back; auditors want to see what was deleted
and when. Laravel’s SoftDeletes trait writes the current time into a deleted_at column instead of running a
DELETE: every default query of the model adds where deleted_at is null, so a trashed row drops out of it, and
withTrashed() and onlyTrashed() read the trashed rows when the code asks for them.
soft delete: Marking a row as deleted with a deleted_at time instead of removing it. Normal queries and route binding skip such rows, and they can be restored. More in “Controllers and route model binding” is on FeeInstalment from the top of this lesson: the migration
added softDeletes(), which is the Blueprint helper for a nullable deleted_at column, and the model used the
SoftDeletes trait. One row through its states:
from session import run
# The SoftDeletes trait keeps the row and sets a deleted_at timestamp instead of removing it;
# the default query ignores soft-deleted rows, withTrashed() reads them, restore() clears the
# timestamp, and forceDelete() removes the row for good.
run(
"""
php artisan migrate --no-interaction
php soft_delete_probe.php
""",
app=True,
overlay=("pathshala-model", "soft-delete"),
) Output
$ php artisan migrate --no-interaction INFO Running migrations. 2026_01_05_090000_create_students_table ............................... DONE 2026_01_05_091000_create_fee_instalments_table ........................ DONE $ php soft_delete_probe.php All rows at start: 3 After delete() on the April row: default query: 2 rows withTrashed(): 3 rows onlyTrashed(): 1 row deleted_at of the April row: 2026-04-10 09:15:00 After restore() on the April row: default query: 3 rows deleted_at of the April row: NULL After forceDelete() on the April row: default query: 2 rows withTrashed() still finds it: false
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 soft_delete_session.py
The default count fell from 3 to 2 after delete(), withTrashed()->count() kept 3 and onlyTrashed()->count()
returned 1. deleted_at of the April row read 2026-04-10 09:15:00, which is the test clock the probe set with
Carbon::setTestNow for a repeatable run. restore() wrote NULL back into the column and the row returned to
every default query; forceDelete() ran the real DELETE and even withTrashed() could no longer find it.
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The row at the start. The April instalment sits in the fee_instalments table with deleted_at set to NULL. The default query returns it and withTrashed() returns it too, because no row is trashed yet.
- delete() writes the timestamp. $instalment->delete() does not remove the row. It writes the current time into deleted_at, so the row stays in the table but every query that uses the default scope will skip it.
- Default query drops it. FeeInstalment::count() returns one row less, because the SoftDeletes trait adds WHERE deleted_at IS NULL to every default query it writes.
- withTrashed() still finds it. FeeInstalment::withTrashed()->count() returns the full set, including the April row. onlyTrashed() returns the April row by itself, which is what a trash bin page would read.
- restore() clears the timestamp. FeeInstalment::withTrashed()->find($id)->restore() writes NULL back into deleted_at. The row is in the default query again and withTrashed() sees it, with no history that it was ever gone.
- forceDelete() removes the row. $instalment->forceDelete() runs a DELETE statement for real. Neither query returns the April row now, and the audit history goes with it, so forceDelete is for rows a policy says must leave the database.
Three scopes for three audiences
A page for the office uses the default query and sees current rows only. A trash bin page for the office uses
onlyTrashed(). An auditor’s report uses withTrashed() and shows every row that ever existed.
Checkpoint
Check: Soft deletes and restore
UUID and ULID keys do not replace an authorization check
An auto-increment id in a URL is a page the next integer may reach. If a route serves /students/{student}/fees
and the controller trusts the id alone, a visitor with one legitimate link can edit the number and try the next
account. The insecure direct object reference (IDOR): Access to a record through a real identifier, such as an id in a URL, that the application loaded but failed to authorize for the current user. More in “Authorization: gates and policies” name is for that pattern, and OWASP’s cheat
sheet is a defensive one: the fix is a check on who may read each row, not a bigger number. A key that is hard to
guess stops a visitor from reaching the next row by adding one, and hides how many rows the table holds; it does not
refuse any request.
The HasUuids trait gives a model a 36-character UUIDv7 primary key, which is lexicographically sortable because
UUIDv7 starts with its timestamp. The HasUlids trait does the same with a 26-character
ULID: A 26-character identifier made of a 48-bit timestamp and 80 random bits, sortable in the order it was generated, used as a primary key when a sequential integer would make the next row too easy to guess., which is shorter in the URL and still sorts by time:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Model;
// A one-time code sent to a guardian when they open a session. The ULID key keeps the row's
// identifier out of the URL, so a URL on another device cannot be edited to reach another
// guardian's code. The authorization check in GuardianOtpController does the actual refusing.
#[Fillable(['guardian_id', 'code_hash', 'expires_at'])]
class OtpSession extends Model
{
use HasUlids;
} A ULID in the URL of a one-time code is a fence, not a lock. The second half of the pair is the controller: it calls an authorization policy before it reads anything about the row, and the policy answers from the authenticated user and the row, not the URL.
| Key | In a URL | Fit |
|---|---|---|
| Auto-increment | Digits; the next id is one more | Internal rows, behind a policy |
| UUIDv7 | 36 characters, sorts by time | Public rows, longer URL |
| ULID | 26 characters, sorts by time | Public rows, shorter URL |
Pin the fence to a lock
Put a ULID on the handful of models whose rows appear in URLs sent to visitors (invitations, reset tokens, one-time codes), and keep auto-increment keys everywhere else. Then write the policy that refuses the request whatever the key is: the gates and policies of this track’s authorization module are the tool for it.
Checkpoint
Check: UUID and ULID keys
Pruning old rows on a schedule
A guardian one-time code is useful for 10 minutes and then it is clutter. A table of codes two months old is a cost
and a liability. The Prunable trait tells a model which of its rows should leave the database over time, and the
model:prune Artisan command runs that query on the schedule the application sets.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
use Illuminate\Database\Eloquent\Builder;
#[Fillable(['guardian_id', 'code_hash', 'expires_at'])]
class OtpSession extends Model
{
use Prunable;
/**
* One-time codes that have been consumed, or have been sitting
* past their expiry for a day, can leave the table.
*/
public function prunable(): Builder
{
return static::query()->where('expires_at', '<=', now()->subDay());
}
} model pruning: A scheduled Artisan task that deletes rows a model marks as no longer needed, so a request that handles today's codes never also has to clean up last month's. runs on the schedule, not inside a request. One line in
routes/console.php sets the time of day:
<?php
use App\Models\OtpSession;
use Illuminate\Support\Facades\Schedule;
// routes/console.php: run model:prune every night. Behind the scenes the command walks
// the app/Models folder for Prunable models and calls each one's prunable() query. Only
// the models the --model option names are scheduled when they live outside app/Models.
Schedule::command('model:prune', [
'--model' => [OtpSession::class],
])->dailyAt('02:30'); model:prune --pretend reports how many rows would leave the table without touching them, which is the test to run
before the first scheduled night. Soft-deleted rows that match the prunable query are permanently removed with
forceDelete, so a trashed row scheduled for pruning does leave; a prunable method can also return a
whereNotNull('deleted_at') to prune only trashed rows.
Checkpoint
Check: Scheduled pruning
Practise: a soft-deletable guardian OTP log, with pruning
Give a copy of your Pathshala project a log of guardian OTPs. The six steps repeat the model, upsert, soft-delete and pruning moves of this page on a table the page never built:
- Write a migration for a
guardian_otp_logtable withid,guardian_id(indexed),code_hash,expires_at,unique(['guardian_id', 'expires_at']),softDeletes()andtimestamps(). Run it withphp artisan migrate. - Create an
App\Models\GuardianOtpLogmodel that listsguardian_id,code_hashandexpires_atunder#[Fillable([...])], usesSoftDeletesand castsexpires_atto a datetime. - Add
Model::shouldBeStrict(! $this->app->isProduction())toAppServiceProvider::boot(). In Tinker, read a row withselect('id')and then try to accesscode_hashto see the third guard fire. - Upsert a batch of 500 rows from a seed file, using
(guardian_id, expires_at)as the unique columns. EnableDB::enableQueryLog()around the call and readcount(DB::getQueryLog()): it should be 1. - Soft-delete one row, read it with
withTrashed(), restore it, thenforceDeleteit. Count rows at each step. - Add
Prunableto the model with aprunable()method that returns rows whoseexpires_atis more than a day old, and schedulemodel:prune --model=App\Models\GuardianOtpLogdaily inroutes/console.php.
The self-check below asks about strict mode, upserts, soft deletes, keys and pruning, starting with the idea you answer least well, and explains every answer:
Practice that adapts to you
Practise this lesson
12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): why does strict mode belong in development and not on the production site?
Because the guards are there to make a developer see a slip early, at the first slow page or the first odd row on
a laptop, not to break a live site that has run with a lazy-loaded relation or a stray array key for months. In
development each exception is a one-line fix: eager-load with with(), add the key to Fillable, or add the column
to the select(). In production the same exception would be an error page a visitor gets instead of the page they
asked for. So the usual line is Model::shouldBeStrict(! $this->app->isProduction()): on everywhere except
production. The test suite runs with the guards on as well, so most slips are found and fixed before a deploy, while
production keeps the default behaviour.
Two longer questions grow out of the Fillable list and the ULID key above. Sketch how you would open each answer before you read the opening step written for it:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A teammate sees protected $guarded = [] on a User model and a controller line User::create(request()->all()). Walk through why this pair is a mass-assignment hole, and the two small changes you would make to close it.
-
Mid to senior
A teammate replaces the primary key of an Invitation model with a ULID and closes a security ticket about an insecure direct object reference. How would you decide whether the hole is closed, and what would you add if not?
Flashcards · 5 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Which three slips does Model::shouldBeStrict() turn into exceptions?
Lazy loading of a relationship, writing an array key the model has not declared as fillable, and reading a column the query did not select. Turn it on in development only, so a live page never fails on a slip it has run with for months.
-
How do firstOrCreate, updateOrCreate and upsert differ?
firstOrCreate and updateOrCreate write one row each; upsert writes many rows in one statement. All three are repeatable without duplicates only when a unique index matches the columns they search by.
-
What do the three arguments of upsert($rows, $uniqueBy, $updateColumns) mean?
The rows to write, the columns that identify an existing row (backed by the migration's unique index), and exactly the columns an existing row may have overwritten. In the probe it wrote 1,000 rows in one query.
-
What does the SoftDeletes trait change, and which four methods work with deleted rows?
Deleting writes a time into deleted_at instead of removing the row, and normal queries skip it. withTrashed includes such rows, onlyTrashed reads only them, restore clears the mark, and forceDelete really deletes.
-
Does a UUID or ULID primary key make a record private?
No. It keeps the next id from being guessed from a URL, but only an authorization check refuses a visitor the row does not belong to.
Key takeaways
- A model names one row of a table, and its Fillable list is the column boundary for an array that came from outside, so a request can never write a column no form offered.
Model::shouldBeStrict()turns three default behaviours into exceptions in development: lazy loading inside a loop, writing an array key the model has not declared, and reading a column the query did not select.firstOrCreate,updateOrCreateandupsertwrite one row, one row and many rows respectively, and all three need a matching unique index to be idempotent.upsert($rows, $uniqueBy, $updateColumns)ran one query for 1,000 rows in the probe, which is why the import takes one trip through the driver instead of 1,000.SoftDeleteswrites adeleted_atcolumn instead of deleting the row;withTrashed,onlyTrashed,restoreandforceDeleteare the four methods that read, un-mark or really delete it.- A UUID or ULID key is a fence that keeps the next id out of a URL; the authorization check is the lock that refuses a request from a visitor the row does not belong to.
Terms in this lesson
- model pruning
- A scheduled Artisan task that deletes rows a model marks as no longer needed, so a request that handles today's codes never also has to clean up last month's.
- strict mode
- A development-only Eloquent setting that turns three default behaviours into exceptions: lazy loading of a relationship, writing a column the model has not declared, and reading a column the query did not select.
- ULID
- A 26-character identifier made of a 48-bit timestamp and 80 random bits, sortable in the order it was generated, used as a primary key when a sequential integer would make the next row too easy to guess.
- upsert
- A single SQL statement that writes many rows: the database matches each row against a unique index and either inserts it or writes the given columns over the matching row.
References
- Eloquent: Getting Started (Laravel (laravel.com))
- Eloquent: Serialization (Laravel (laravel.com))
- Release Notes (Laravel (laravel.com))
- Insecure Direct Object Reference Prevention Cheat Sheet (OWASP Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress