Laravel 8 Eloquent upsert inserting new records
Stefan Bogdanescu
Founder & Senior Architect · 2026-06-29
# Mastering Laravel Eloquent `upsert`: Solving the Mystery of Updates
Laravel 8 introduced the incredibly useful `upsert` method, designed to simplify the process of either inserting new records or updating existing ones based on a set of criteria. As a senior developer, understanding how this feature interacts with the underlying database structure is crucial. If you are running into issues where your `upsert` operation only inserts new rows and fails to update existing ones, it almost always points to an issue with database constraints, not the Eloquent syntax itself.
This post will dive deep into why your `upsert` isn't working as expected and provide the robust solutions for achieving true upsert functionality in Laravel.
## The Mechanics of Database Upserts
The `upsert` method delegates the logic to the underlying database engine (like MySQL or PostgreSQL). For an operation to successfully perform both insertion *and* conditional updating, the database needs a clear rule: a mechanism to identify which rows match the input data for the update portion.
When you call:
```php
Flight::upsert([
['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
// ... more data
], ['departure', 'destination']);
```
Laravel tells the database: "Try to insert these rows. If a conflict occurs based on the specified columns (`departure` and `destination`), execute an update instead."
The fundamental requirement for this "if conflict, then update" logic to function is that there must be a **unique index or primary key** defined on the combination of the columns you are using to match records. Without this constraint, the database sees no unique identifier to target for the update, so it defaults purely to insertion.
## The Core Problem: Missing Uniqueness Constraints
In your sample scenario, where `departure` and `destination` are not unique fields (i.e., multiple flights can depart from Oakland, and multiple flights can go to San Diego), the database cannot reliably determine which existing record should be updated when a conflict occurs. Therefore, it executes the insert operation without modification.
To fix this, you must explicitly define the composite key that determines uniqueness in your database schema.
## Solution 1: The Database-First Approach (Recommended)
The most robust and performant solution is to enforce data integrity at the database level using migrations. This ensures that the dataânot just the application logicâguarantees correctness.
You need to add a unique index spanning both `departure` and `destination`.
### Implementing the Migration
When you create or modify your table migration, define a composite unique index:
```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::table('flights', function (Blueprint $table) {
// Add a unique index on the combination of departure and destination
$table->unique(['departure', 'destination']);
});
}
public function down(): void
{
Schema::table('flights', function (Blueprint $table) {
// Remove the index when rolling back
$table->dropUnique(['departure', 'destination']);
});
}
};
```
After running this migration, your database will now recognize the `(departure, destination)` pair as unique. When you run your Eloquent `upsert`, the database can successfully match existing records and perform the update operation as intended. This practice is central to maintaining data integrity across all Laravel applications, including those leveraging features like those found on [laravelcompany.com](https://laravelcompany.com).
## Solution 2: The Eloquent Alternative (When Constraints are Impossible)
If, for some reason, you absolutely cannot modify the database schema to enforce unique constraints (e.g., if this relationship is inherently many-to-many without a natural key), you must rely on an alternative Eloquent pattern. In this case, `upsert` is not the right tool, and you should use the more explicit `updateOrCreate` method within a transaction for safety:
```php
use App\Models\Flight;
use Illuminate\Support\Facades\DB;
// Example data to process
$data = [
['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150]
];
DB::transaction(function () use ($data) {
foreach ($data as $flightData) {
$flight = Flight::updateOrCreate(
[
'departure' => $flightData['departure'],
'destination' => $flightData['destination'],
],
[
'price' => $flightData['price'],
]
);
}
});
```
While this approach avoids the constraint problem, it is generally less performant for bulk operations than a native database `upsert`, especially with very large datasets. For high-volume data synchronization, always strive to use the optimized database features enabled by correct indexing.
## Conclusion
The confusion surrounding Laravel's `upsert` method stems from the necessary interaction between application logic and database constraints. To make `upsert` work reliably for updating records, you must ensure that the criteria used for matching existing rows are enforced by a unique index in your database schema. Always favor defining clear structural rules via migrations over relying solely on application-level logic when dealing with data integrity.