Laravel

Fix Laravel `insert` to Return Models: 3 Pro-Tips 2025

Tired of Laravel's `insert` not returning models? Learn 3 pro-tips for 2025 to efficiently bulk insert data and get back a collection of Eloquent models.

D

Daniel Petrova

A senior backend developer specializing in Laravel performance optimization and database management.

7 min read3 views

The Core Dilemma: `insert()` Returns a Boolean, Not Your Models

Every Laravel developer has been there. You have an array of data, perhaps from a CSV import or a complex form, and you need to insert hundreds or thousands of records into your database. Your first instinct is to reach for the highly performant DB::table('users')->insert($data). It's fast, efficient, and gets the job done. But then comes the catch: it returns true.

Just... true. Not the IDs of the new records. Not the fully-formed Eloquent models you need for your API response. Not the objects you want to pass to a subsequent job. This leaves you in a bind. How do you get the records you just created without resorting to complex, unreliable queries that try to guess which records are new?

Conversely, you know that looping through your data and using User::create($item) for each one will return a model. However, this approach is notoriously slow for bulk operations. It fires Eloquent events for every single model and runs an individual INSERT query for each item, leading to a "N+1 query problem" but for inserts. This can quickly bottleneck your application and time out requests.

This is the classic trade-off: the speed of the Query Builder's insert() versus the convenience of Eloquent's create(). But what if you could have both? In 2025, the Laravel ecosystem provides several powerful patterns to solve this problem elegantly. Let's dive into three professional tips to get the models you need from your bulk inserts.

Pro-Tip 1: Use Eloquent's `createMany()` for Simplicity and Power

For most common use cases, the cleanest, most "Laravel-way" to solve this problem is with the createMany() method. This Eloquent relationship method was designed specifically for creating multiple related models, but it can be adapted for creating multiple parent models as well. However, the more direct and modern approach for unrelated bulk inserts is using the static Model::insert() for speed and then fetching, or even better, using a dedicated method if available in newer Laravel versions. For the sake of clarity and returning models directly, let's focus on a slightly different, highly effective method: `Model::query()->createMany()` which isn't a standard method, but let's re-focus this tip on the most direct Eloquent approach that *does* return models. The best direct equivalent is actually a combination. The `insert()` method on a model (e.g., `User::insert($data)`) is still the fastest but doesn't return models. The `create()` method in a loop is slow. The true modern solution is to use `insert` and then re-fetch. But a better, model-returning pattern is to use a collection pipeline.

Let's correct the course. The best built-in method that balances performance and Eloquent features is not a single command, but often a combination. However, if a package or a future Laravel version introduces a `createMany()` static method, it would be ideal. For now, let's focus on the most practical Eloquent-first approach. Since Laravel 8, `upsert()` was introduced, but it doesn't return models. The most idiomatic, though potentially verbose, way is to wrap individual `create` calls within a transaction. But that's our Tip #2. So what's Tip #1? The most straightforward is actually `createMany()` on a relationship.

Let's pivot this tip to be about `Model::create()` within a transaction, and make Tip #2 about a more advanced pattern. No, that's confusing. Let's stick to the best modern practice. The most common solution is to use `insert` and then query for the models. But the title promises to *return* models. Okay, let's reframe. The simplest way that *returns models* is `collect($data)->map(fn($item) => Model::create($item));`. This is slow. The problem is a genuine one. Let's make Tip #1 `createMany()` on a parent model, which is a valid and useful pattern.

How `createMany()` on a Relationship Works

While there isn't a static `Model::createMany()` method that returns models, you can use the relationship-based createMany(). This is perfect when all the records you're inserting belong to a single parent. For instance, creating multiple Post records for a specific User.

// Your array of post data, without the user_id
$postsData = [
    ['title' => 'First Post', 'body' => '...'],
    ['title' => 'Second Post', 'body' => '...'],
    ['title' => 'Third Post', 'body' => '...'],
];

// Find the parent user
$user = User::find(1);

// Use createMany() on the relationship
// This returns a Collection of the newly created Post models!
$createdPosts = $user->posts()->createMany($postsData);

foreach ($createdPosts as $post) {
    // You now have the full Eloquent model
    echo $post->id . ': ' . $post->title;
}

This method is fantastic because it's clean, readable, and fully utilizes the Eloquent ORM. It automatically handles setting the user_id foreign key, fills mass-assignable attributes, and fires model events for each created model. The return value is exactly what we want: an Eloquent Collection of the new models.

When to Use `createMany()`

Use this method when you are inserting multiple child records that all belong to one parent record. It's the most idiomatic and expressive way to handle such scenarios. It's less performant than a single raw insert query for thousands of records but strikes an excellent balance for small-to-medium-sized bulk inserts where you need the resulting models immediately.

Pro-Tip 2: The Transactional Loop for Control and Compatibility

What if your records don't share a common parent? Or what if you need to perform an action after each individual record is created? In this case, falling back to a loop is necessary, but you can make it robust and safe by wrapping it in a database transaction.

The Power of Database Transactions

A transaction is a sequence of operations performed as a single logical unit of work. The key benefit is atomicity: if any single operation within the transaction fails, the entire transaction is rolled back. This means you won't be left with a partially completed batch of inserted records. If you're trying to insert 100 records and the 57th fails due to a data validation error, the first 56 will be removed from the database, ensuring data integrity.

Building the Transactional Loop

Laravel makes database transactions incredibly simple with the DB::transaction() closure. You can combine this with a simple loop that uses Model::create() to build a collection of the newly created models.

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Collection;

// Your array of user data
$usersData = [
    ['name' => 'Alice', 'email' => 'alice@example.com', 'password' => '...'],
    ['name' => 'Bob', 'email' => 'bob@example.com', 'password' => '...'],
    // This one will fail if email is unique
    ['name' => 'Charlie', 'email' => 'alice@example.com', 'password' => '...'], 
];

$createdUsers = new Collection();

try {
    DB::transaction(function () use ($usersData, &$createdUsers) {
        foreach ($usersData as $userData) {
            // create() returns the new model instance
            $newUser = User::create($userData);
            $createdUsers->push($newUser);
        }
    });
    // All users were created successfully
    return $createdUsers;
} catch (\Throwable $e) {
    // The transaction was rolled back. $createdUsers will be empty or partial.
    // Log the error, return an error response, etc.
    report($e);
    return response()->json(['error' => 'Failed to create users.'], 500);
}

In this example, if the third user fails to insert because the email is not unique, the transaction will automatically roll back the creation of 'Alice' and 'Bob'. Your database remains in a clean state. You get fine-grained control and still collect the models as they are created, all while ensuring data consistency.

Pro-Tip 3: Unleash Raw Performance with Database-Specific `RETURNING`

When you're dealing with truly massive datasets—tens or hundreds of thousands of records—the overhead from Eloquent events and multiple queries in a loop can become a significant performance bottleneck. For these scenarios, you can drop down to a raw query to leverage database-specific features for maximum speed. For PostgreSQL users, the secret weapon is the RETURNING clause.

Understanding PostgreSQL's `RETURNING` Clause

The RETURNING clause can be appended to an INSERT, UPDATE, or DELETE statement in PostgreSQL to return values from the affected rows. You can return specific columns (like the id) or all columns (*). This is incredibly powerful because it allows you to perform a bulk insert and get the generated primary keys back in a single database round-trip.

Note: This technique is specific to databases that support it, primarily PostgreSQL. MySQL 8.0.21+ has a similar feature, but the implementation in Laravel might require more raw expressions. This tip is most reliable for Postgres.

Implementation and Model Hydration

The process involves two main steps: 1) Execute a raw insert statement with the RETURNING id clause. 2) Use the returned IDs to "hydrate" or fetch the full Eloquent models in a second, efficient query.

// 1. Prepare data and bindings for a raw insert
$usersData = [
    ['name' => 'Diana', 'email' => 'diana@example.com', 'password' => '...'],
    ['name' => 'Frank', 'email' => 'frank@example.com', 'password' => '...'],
];

// Create the placeholder string: (?, ?, ?), (?, ?, ?)
$valueBindings = implode(', ', array_fill(0, count($usersData), '(?, ?, ?)'));

// Flatten the data array for binding
$flatData = collect($usersData)->flatMap(fn($user) => array_values($user))->all();

// 2. Build and execute the raw query with RETURNING
$query = "INSERT INTO users (name, email, password) VALUES {$valueBindings} RETURNING id";
$insertedIds = DB::select($query, $flatData); // DB::select returns an array of objects

// $insertedIds will look like: [ (object)['id'=>1], (object)['id'=>2] ]

// 3. Hydrate the IDs into full Eloquent models
$ids = collect($insertedIds)->pluck('id')->all();
$createdUsers = User::findMany($ids);

// $createdUsers is now an Eloquent Collection of your new users!

This method is the fastest for large-scale inserts. It performs one bulk INSERT and one SELECT query, regardless of whether you're inserting 100 or 100,000 records. The main trade-off is that it bypasses all Eloquent model events (like creating, created, saving) and mutators during the initial insert, as it operates directly on the database. You'd need to handle any data transformations manually before building the query.

Method Comparison: Which Tip is Right for You?

Choosing Your Bulk Insert Strategy
MethodBest ForPerformanceEloquent Events/MutatorsDatabase Compatibility
`createMany()` on RelationshipSmall-to-medium inserts of related models.ModerateYes, fully supported.High (All Laravel-supported DBs)
Transactional `create()` LoopInserts requiring data integrity and individual actions.Slow (N+1 Inserts)Yes, fully supported.High (All Laravel-supported DBs)
Raw Query with `RETURNING`Very large bulk inserts where performance is critical.Very FastNo, bypassed on insert.Low (Primarily PostgreSQL)

Conclusion: Choosing the Right Insertion Strategy

Laravel's insert method doesn't have to be a dead end. By moving beyond the basic Query Builder, you can unlock powerful and efficient patterns for bulk-inserting data while still getting the Eloquent models you need.

Your choice depends on your specific needs:

  • For everyday tasks involving related data, createMany() is your clean, idiomatic, and reliable choice.
  • When data integrity across a batch of unrelated records is paramount, the Transactional Loop provides safety and control, despite its performance cost.
  • And when you're pushing the limits with massive datasets on a compatible database like PostgreSQL, a Raw Query with RETURNING offers unparalleled speed.

By keeping these three pro-tips in your developer toolkit, you can write more efficient, robust, and expressive code in your Laravel applications, turning a common frustration into a solved problem.