← Other topics

Laravel error: Integrity constraint violation - foreign key constraint fails

Video Notes

Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails error in Laravel

The above error occurs when you attempt to delete a row in a table that has a foreign key connection with another row in another table.

To demonstrate this, imagine an application that catalogs books. Within this application, there is a One to Many relationship between books and authors, such that every book is connected to a single author.

Here’s the migration method to create the books table, with the author relationship established at the end:

public function up()
{
    Schema::create('books', function (Blueprint $table) {
        $table->id();
        $table->timestamps();
        $table->string('title');
        $table->text('description');

        # ⭐ Establish one-to-many relationship with authors ⭐ 
        $table->bigInteger('author_id')->unsigned()->nullable();
        $table->foreign('author_id')->references('id')->on('authors');
    });
}

Because of the above relationship, if you attempt to delete an author that is connected to a book, you will receive the Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails error. Example:

/**
* Deletes the author
* DELETE /authors/{id}/delete
*/
public function destroy($id)
{
    $author = Author::find($id);

    # If there are any books associated with this author, the following 
    # deletion will cause a "foreign key constraint fails" error
    $author->delete();

    return redirect('/authors')->with([
        'flash-alert' => '“' . $author->title . '” was deleted.'
    ]);
}

Solution 1 - Dissociate

One way to address this problem is to dissociate any books connected to an author before deleting the author:

/**
* Deletes the author
* DELETE /authors/{id}/delete
*/
public function destroy($id)
{
    $author = Author::find($id);

    # Before deleting the author, dissociate any books connected to this author
    foreach($author->books as $book) {
        $book->author()->dissociate();
        $book->save();
    }

    $author->delete();

    return redirect('/authors')->with([
        'flash-alert' => 'The author ' . $author->getFullName() . ' was deleted.'
    ]);
}

The above approach uses Eloquent’s dissociate method to break the connection between a book and the author about to be deleted. Behind the scenes, the dissociate method works by setting the book’s author_id field to null.

Note that this only works without error because our migration set the books table’s author_id field to be nullable:

$table->bigInteger('author_id')->unsigned()->nullable();

Solution 2 - Delete

Another way to solve the foreign key constraint error is to delete any books associated with an author before deleting that author:

/**
* Deletes the author
* DELETE /authors/{id}/delete
*/
public function destroy($id)
{
    $author = Author::find($id);

    # Delete any books attached to this author before deleting the author
    $author->books()->delete();

    $author->delete();

    return redirect('/authors')->with([
        'flash-alert' => 'The author ' . $author->getFullName() . ' was deleted.'
    ]);
}

Solution 3 - Cascade on delete

If you don’t want to worry about manually deleting books before deleting authors, you can include the cascadeOnDelete method when defining the author_id field in your books table migration. This will make it so that if a book’s author is deleted, it will also delete that book.

public function up()
{
    Schema::create('books', function (Blueprint $table) {
        $table->id();
        $table->timestamps();
        $table->string('title');
        $table->text('description');

        # Establish One to Many relationship with authors
        $table->bigInteger('author_id')->unsigned()->nullable();
        $table
            ->foreign('author_id')
            ->references('id')
            ->on('authors')
            ->cascadeOnDelete(); # ← ⭐ NEW: cascadeOnDelete method
    });
}

I’m not a fan of this approach because I don’t like it when my database automatically deletes rows. Instead, I prefer to have code within my features that does the deletion so it’s clear (to myself and collaborators) when data is being removed.

User experience

The above solutions avoid the foreign key error, but might not be the best approach in terms of user experience, as users would be deleting content they didn’t explicitly request to delete. To address this, at minimum, you should warn the user ahead of time:

Alternatively, you could prevent the deletion, forcing the user to first delete the corresponding books:

Many to Many relationships

The same foreign key constraint fails error described above can also apply to Many to Many connections. For example, in our book application, imagine users can add books to a favorite list. This relationship is established with a pivot table called book_user as seen in the following migration:

public function up()
{
    Schema::create('book_user', function (Blueprint $table) {
        $table->id();
        $table->timestamps();

        $table->bigInteger('book_id')->unsigned();
        $table->bigInteger('user_id')->unsigned();

        # Make foreign keys
        $table->foreign('book_id')->references('id')->on('books');
        $table->foreign('user_id')->references('id')->on('users');

    });
}

In this set up, you’ll see a foreign key connection error if you delete a book associated with any users or if you delete a user associated with any books.

To address this, we can use the detach method to remove any relationships before doing a deletion. Example:

/**
* Deletes the book
* DELETE /books/{slug}/delete
*/
public function destroy($slug)
{
    $book = Book::findBySlug($slug);

    # ⭐ Before deleting this book, detach any of the users that were connected to it ⭐
    $book->users()->detach();

    # Now we can delete the book
    $book->delete();

    return redirect('/books')->with([
        'flash-alert' => '“' . $book->title . '” was removed.'
    ]);
}

Of course, the same user experience considerations discussed above should thought of here. It could be frustrating to a user to have books disappear from their favorite list without understanding why, so it should either be avoided or they should be warned ahead of time.

One last issue...

We have one final foreign key related issue in our book application and that’s the impact of deleting authors when it comes to our favorite’s list.

The way we’ve currently set things up, if we delete authors, that could delete books, and if those books are on a user’s favorite list - we’ll face that same foreign foreign key constraint fails error when the books are deleted.

To address this, the order of operations for deleting an author should be:

  1. Remove author’s books from users’s favorite lists
  2. Delete author’s books
  3. Delete author
/**
* Deletes the author
* DELETE /authors/{id}/delete
*/
public function destroy($id)
{
    $author = Author::find($id);

    # 1. Remove author’s books from users’s favorite lists
    foreach($author->books as $book) {
        $book->users()->detach();
    }

    # 2. Delete author’s books
    $author->books()->delete();
    
    # 3. Delete author
    $author->delete();

    return redirect('/authors')->with([
        'flash-alert' => 'The author ' . $author->getFullName() . ' was deleted.'
    ]);
}

The above solution shows you technically how to solve foreign key errors when deleting data, but it doesn’t necessarily mean it’s the best approach because as you can see, it causes a domino effect of deletion.

In the context of this book application, I would make it so only administrators could delete books or authors, and I would make it clear in the interface about any of the cascading effects those deletions would have (E.g. ”deleting this author will also remove x books which are on y user’s lists - are you sure you want to continue?”).

If this info helped you out, toss a coin in the tip jar?
← Other topics