Laravel duplicate key error despite unique validation

In a Laravel API, it’s really common to create users with an endpoint like this in a user controller:

public function store(Request $request): UserResource|JsonResponse
{
    $validator = Validator::make(
        $request->all(),
        [
            'email' => 'required|string|max:255|email|unique:users',
            'name'  => 'required|string|max:255',
        ],
        [
            'email.unique' => 'That email address already has an account.',
        ]
    );
    if ($validator->fails()) {
        return response()->json(
            [
                'error'   => true,
                'message' => $validator->errors()->all(),
            ],
            Response::HTTP_UNPROCESSABLE_ENTITY
        );
    }
    $user = User::create(
        $request->only(
            [
                'email',
                'name',
            ]
        )
    );Code language: PHP (php)

There’s a problem here though – that unique validation on the email field is subject to a race condition. If two requests are received very close together, both can pass validation, but then the second one will fail with a duplicate key error on the User::create call. While that sounds unlikely, it happens for real sometimes, and you’ll see something like this in your web logs when it does:

192.168.0.1 - - [06/Oct/2023:07:08:54 +0000] "POST /users/ HTTP/2.0" 201 1276 "-" "okhttp/4.9.2"
192.168.0.1 - - [06/Oct/2023:07:08:55 +0000] "POST /users/ HTTP/2.0" 500 17841 "-" "okhttp/4.9.2"Code language: JavaScript (javascript)

The 201 response is a successful creation, but it’s followed a second later by a 500 failure for the duplicate request. The Laravel log will then contain one of these:

[2023-10-06 07:08:55] staging.ERROR: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'user@example.com'
 for key 'users.users_email_unique' (Connection: mysql, SQL: insert into `users` (`email`, `name`) values (user@example.com, Test)Code language: JavaScript (javascript)

To deal with that we can trap the creation error, and return an error response that looks the same as the validation error:

try {
    $user = User::create(
        $request->only(
            [
                'email',
                'name',
            ]
        )
    );
} catch (QueryException $e) {
    //1062 is the MySQL code for duplicate key
    if ($e->errorInfo[1] !== 1062) {
        //Rethrow anything except a duplicate key error
        throw $e;
    }
    return response()->json(
        [
            'error'   => true,
            'message' => 'That email address already has an account.',
        ],
        Response::HTTP_UNPROCESSABLE_ENTITY
    );
}Code language: PHP (php)

This way, as far as the client is concerned, it was a straightforward validation failure with an appropriate 422 error code, and we don’t get spurious 500s clogging up our error logs.

Bridging Laravel Scout to Eloquent

Laravel Scout provides an interface to external search engines such as Algolia and Meilisearch. These are typically remote indexes of the same data that you’re storing locally, but with far faster and more powerful searching and ranking capabilities. However, they have some limitations on how you can express the search itself (allowing only a single string value), and what you can do with the search result (because it comes back as a Laravel Collection and not a Query Builder). This makes it difficult to do things like perform a complex search remotely, and then filter the result further via Eloquent operations, perhaps involving different parts of your database. A typical Scout search might be:

use App\Models\Order;
$orders = Order::search('Star Trek')->get();Code language: PHP (php)

This gives us a bunch of Order instances, but we had no opportunity to ask it to do things like load relations or filter the results while it was doing it.

Fortunately it’s not difficult to bridge these two worlds. The resulting Collection contains model instances, so we can extract their IDs and use them to construct an Eloquent search that selects the same records, but locally this time (Scout already did the heavy lifting of figuring out which ones we wanted):

$builder = Order::query()->whereIn('id', $orders->keys());Code language: PHP (php)

This isn’t ideal (because it will fetch those records a second time), but it will be reasonably efficient because the IDs it searches on are exact matches for primary keys in the database.

We now have a builder that will select the same records as the Scout search did, but we can continue adding to it before requesting the final results.

$result = $builder->where('orders.name', 'like', 'a%')
    ->with(['orderItems', 'customer'])
    ->get();Code language: PHP (php)

So that’s how we can get to use Eloquent features on top of a Scout search.


After finding out that this bridging wasn’t built-in, I submitted a PR to add it, but sadly it was rejected. With the PR code in place, the syntax would have looked like this:

use App\Models\Order;
$orders = Order::search('Star Trek')
    ->toEloquent()
    ->where('orders.name', 'like', 'a%')
    ->with(['orderItems', 'customer'])
    ->get();Code language: PHP (php)

To be fair, this isn’t much of a saving in the external syntax, but it is more efficient because it can get the record IDs directly from Scout without having to load the models from the database. I don’t think that efficiency gain can be obtained from outside Scout’s own code.

I hope that helps someone!