Skip to content

Laravel error

Laravel: SQLSTATE[23000] Integrity constraint violation

The error

SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails

The database refused a write because it would have left a row pointing at nothing. Which of the four 23000 errors you have is written in the number after it, and each one means something different.

The error

Illuminate\Database\QueryException

SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a
child row: a foreign key constraint fails (`shop`.`orders`,
CONSTRAINT `orders_customer_id_foreign` FOREIGN KEY (`customer_id`)
REFERENCES `customers` (`id`))

Read the number, not the class

23000 is the SQL standard's code for "you broke an integrity constraint", and on its own it does not narrow anything down. The number after it does:

CodeWhat the database is refusing
1452A child row whose foreign key points at a parent that does not exist
1451Deleting a parent that still has children pointing at it
1062A duplicate value in a unique index
1048A null in a column declared not null

Everything below is about 1452. The others are different problems that happen to share a prefix, which is why searching for SQLSTATE[23000] alone returns four unrelated conversations.

What 1452 actually means

You asked to write a row whose foreign key column holds a value that has no match in the referenced table. The constraint name in the message tells you which column and which table, and it is worth reading rather than skipping: orders_customer_id_foreign is orders.customer_id referencing customers.

The causes, roughly in order of frequency

The value is null when the column is not nullable. A relation that was not loaded, a request field that was not sent, an optional() chain quietly producing null. The column takes it as "point at nothing", which is precisely what the constraint forbids.

The parent is in an uncommitted transaction. Create a customer and an order inside one transaction and it works. Create the customer in a queued job and the order in the request that dispatched it, and the job can run before the request's transaction commits - so the worker looks for a customer that does not exist yet. Laravel has a switch for exactly this:

// config/queue.php
'after_commit' => true,

The parent was deleted. Hard deletes are the obvious version. Soft deletes are the surprising one: SoftDeletes hides a row from Eloquent but leaves it in the table, so the constraint is satisfied by a record your application believes is gone - until somebody prunes.

Seeders and migrations in the wrong order. Children seeded before parents. The fix is ordering, not disabling checks.

The types do not match. A foreignId() column is an unsigned bigint. A parent whose id is a plain integer, or a uuid column with a different collation, will refuse values that look identical in a client.

// the parent
$table->id();                    // bigint unsigned
 
// the child - matches
$table->foreignId('customer_id')->constrained();
 
// the child - does not
$table->integer('customer_id');

Diagnosing it in one query

Before changing any code, ask the database which rows would fail:

SELECT o.id, o.customer_id
FROM orders o
LEFT JOIN customers c ON c.id = o.customer_id
WHERE o.customer_id IS NOT NULL AND c.id IS NULL;

Empty result and you are looking at a timing problem - a transaction or a job that ran too early. Rows come back and you have orphans already, and the constraint is the first thing that has been honest with you about it.

Handling it properly in the application

Catch it where you can say something useful, and let it through where you cannot:

try {
    $order->save();
} catch (QueryException $e) {
    if ($e->errorInfo[1] === 1452) {
        throw ValidationException::withMessages([
            'customer_id' => 'That customer no longer exists.',
        ]);
    }
 
    throw $e;
}

Validating that the parent exists before the write is better still - Rule::exists() turns a 500 into a field error - but keep the catch as well. Validation checks a moment in time; the constraint checks the moment of the write, and between those two moments somebody else can delete the row.

A customer_id that arrived through create($request->all()) raises the mass assignment exception when the column is guarded, and this one when it is not. A foreign key pointing at a table the migrations have not created yet fails as a missing table. And a child row whose parent is already gone turns up later, in a template, as a property read on null.

Where the constraint is missing altogether, adding one to a table with existing data means deciding what to do with the rows that would violate it. A soft-deleted parent is a third case: the constraint is satisfied and the application is still wrong, which is part of what soft deletes cost.

Related questions

Can I turn foreign key checks off to get the deploy through?
You can, and the row will be written, and the constraint will be there tomorrow describing a relationship the data no longer honours. Every later query that joins on that column now returns something nobody planned for. The check is the cheapest correctness guarantee in the system - it runs on every write and costs nothing.
Why does it pass in tests and fail in production?
Two common reasons. SQLite in memory does not enforce foreign keys unless told to, so a suite configured that way never exercises the constraint. And factories create their parents, so a test can never hit the case where the parent was deleted by somebody else between one request and the next.
The parent exists. I can select it.
Then compare the types and the collations of the two columns, and check whether you are inside a transaction that has not committed the parent yet. A bigint child column pointing at an int parent, or utf8mb4_unicode_ci against utf8mb4_general_ci, will refuse a value that plainly exists.
Is 1451 the same problem?
It is the mirror image. 1452 is a child pointing at a parent that is not there; 1451 is a parent being deleted while children still point at it. The first is usually a bug in what you are writing, the second is usually a missing decision about what should happen to the children.
Call us+1 848 272 7583WhatsApp+90 850 308 5436Emailinfo@codefacture.comContact page