Why Use upsert() for CSV Imports in Laravel 12?

             

The upsert() method allows you to perform insert or update operations in a single database query per chunk — much faster than looping with create() or updateOrCreate().

Advantages:

  • Performance: One query instead of thousands
  • Atomicity: Safer for concurrent imports
  • Simplicity: Clean, readable code
  • Duplicate handling: Automatically updates existing records based on unique keys



How to Import CSV Files in Laravel 12 with Upsert – Complete 2025 Tutorial

Table Of Content

1 Prerequisites

Before starting, make sure you have:
  • PHP ≥ 8.2
  • Composer
  • MySQL / PostgreSQL / SQLite (or any supported DB)
  • Basic knowledge of Laravel routing, controllers, and Blade

2 Introduction

Importing CSV files is one of the most common and at the same time most performance-sensitive tasks in almost every Laravel project.

In this article you will learn:

  • The cleanest and fastest way to do CSV import in Laravel 12
  • How to properly use upsert() (the best choice in 2025)
  • How to handle very large files (50k – 500k+ rows) without crashing
  • All the most frequent mistakes + how to avoid them
  • Modern, readable code style

3 Create a Fresh Laravel 12 Project

3.1 Install Laravel Project

First, ensure Composer is installed on your system. Use the following command to install a new Laravel Project:

composer create-project laravel/laravel:^12.0 laravel12-csv-import

Navigate to your project directory:

cd laravel12-csv-import

3.2 Configure Your Database (.env)

Open the .env file and input the necessary database credentials:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=vcard
DB_USERNAME=root
DB_PASSWORD=

4 Create Migration for Users Table

In the migration file located at database/migrations, add the following code to define the users table structure:
    
<?php
// database/migrations/2025_03_01_000000_create_users_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('users', function (Blueprint $table) {
            $table->id();
            $table->string('email')->unique();
            $table->string('name');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('users');
    }
};


email is unique.
Run the migration:

php artisan migrate

Important: The email column must have a unique index — this is what upsert() uses to decide whether to insert or update.

5 Create Controller for CSV Import

Create a controller to handle the data loading functionality:

php artisan make:controller UserImportController

Edit app/Http/Controllers/UserImportController.php
    
<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Support\Collection;

class UserImportController extends Controller
{
    public function showForm()
    {
        return view('import-users');
    }

    public function import(Request $request)
    {
        $request->validate([
            'csv_file' => 'required|file|mimes:csv,txt|max:10240', // max 10MB
        ]);

        $file = $request->file('csv_file');
        $handle = fopen($file->getRealPath(), 'r');

        // Skip header row
        $headers = fgetcsv($handle);

        $data = collect();

        while (($row = fgetcsv($handle)) !== false) {
            if (count($row) !== count($headers)) {
                continue; // skip malformed rows
            }

            $data->push([
                'email' => trim($row[0]),
                'name'  => trim($row[1] ?? ''),
            ]);
        }

        fclose($handle);

        // Process in chunks of 500 rows (adjust as needed)
        $data->chunk(500)->each(function (Collection $chunk) {
            User::upsert(
                $chunk->toArray(),       // array of records
                ['email'],               // unique key(s)
                ['name']                 // columns to update if exists
            );
        });

        return redirect()
            ->back()
            ->with('success', 'CSV file imported successfully! ✓');
    }
}
?>

We use upsertOrInsert to avoid duplicate errors.
array_chunk for memory efficiency

Key Points:

  • We use collect() for easy chunking
  • upsert() takes 3 arguments: data, uniqueBy, update columns
  • Chunk size of 500–1000 is ideal for most servers

6 Create the Upload Form (Blade View)

create resources/views/import-users.blade.php for the main view:
    
   <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Import Users from CSV - Laravel 12</title>
    <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
    <div class="bg-white p-8 rounded-lg shadow-lg w-full max-w-md">
        <h1 class="text-2xl font-bold mb-6 text-center">Import Users from CSV</h1>

        @if (session('success'))
            <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-6">
                {{ session('success') }}
            </div>
        @endif

        <form method="POST" action="{{ route('users.import') }}" enctype="multipart/form-data">
            @csrf

            <div class="mb-6">
                <label for="csv_file" class="block text-gray-700 font-medium mb-2">
                    Select CSV File
                </label>
                <input type="file" name="csv_file" id="csv_file" accept=".csv,.txt"
                       class="w-full px-4 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500">
                @error('csv_file')
                    <p class="text-red-500 text-sm mt-1">{{ $message }}</p>
                @enderror
            </div>

            <button type="submit"
                    class="w-full bg-blue-600 text-white py-3 rounded hover:bg-blue-700 transition">
                Import CSV
            </button>
        </form>

        <div class="mt-6 text-center text-gray-600">
            <p>Sample CSV format:</p>
            <pre class="bg-gray-100 p-2 rounded mt-2 text-left">email,name
john@example.com,John Doe
jane@example.com,Jane Smith</pre>
            <a href="#" class="text-blue-600 hover:underline">Download sample.csv</a>
        </div>
    </div>
</body>
</html>
    

7 Define Routes

In routes/web.php, define the necessary routes:

use App\Http\Controllers\UserImportController;

Route::get('/import-users', [UserImportController::class, 'showForm'])->name('users.form');
Route::post('/import-users', [UserImportController::class, 'import'])->name('users.import');

8 Folder Structure

      =
      • =

9 Run & Test the Application


php artisan serve

Visit: http://127.0.0.1:8000/import-users
Upload your CSV and watch the magic!

Bonus: Handling Large CSV Files (100k+ Rows)

  • Increase chunk size only if your server has enough memory
  • Use Laravel Queues for background processing:

// In controller
use App\Jobs\ImportCsvJob;

ImportCsvJob::dispatch($filePath);

Add progress bar with Laravel Echo + Pusher (advanced)

10 Conclusion

You've now built a robust, production-ready CSV import system in Laravel 12 using upsert() — the modern, efficient way to handle bulk inserts and updates.

This approach scales well, prevents duplicates, and keeps your code clean.
Happy coding!

Revathi M - PHP and CodeIgniter Developer

Written by Revathi M

PHP Developer & Technical Writer · 10+ years building web applications with CodeIgniter and Laravel

Revathi specializes in PHP backend development, authentication systems, and REST API design. She writes practical, production-tested tutorials at Get Sample Code to help developers build secure applications faster.

Frequently Asked Questions

You need PHP 8.2 or higher, a fresh Laravel 12 installation via Composer, and a configured MySQL database.

Configure .env with MySQL details, create a migration for the users table with a unique email column, and run 'php artisan migrate'.

The file is validated as required, a file type, and with mimes: 'csv,txt'.

Use fopen to open the file, fgetcsv to read headers and rows, and array_combine to map headers to values without loading the entire file into memory.

upsertOrInsert is a custom or helper method that inserts new records or updates existing ones based on unique keys like email, preventing duplicate errors.

Data is processed in chunks of 1000 rows using array_chunk before inserting/updating.

The first row should be headers like 'email,name', followed by data rows with corresponding values.

Run 'php artisan serve', visit /import-users, upload a valid CSV file, and check the database for inserted/updated records.