In Laravel projects, a lot of issues come from non-Laravel mistakes, database structure is one of those. In this tutorial, we will cover the most typical mistakes devs make when structuring DB in Laravel.
For example, have you ever encountered an issue with fields not being nullable? Or maybe you had problems with relationships being incorrect type?

Don't worry - you are not alone! Let's cover the most typical mistakes and how to solve them.
Mistake 1: Not Using Foreign Keys
When defining the relationships, some developers forget to add foreign key definitions to the database. Let's look at an example:
Schema::create('clients', function (Blueprint $table) { $table->id(); $table->string('first_name'); // ...});Schema::create('orders', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('client'); // ...});
As you can see, no ->foreign() here.
This works fine for the most part, but once we have some clients and orders in our system - deleting a client will cause an issue:

If we look at our database, we can see that we still have an order with our deleted client:

This issue happened because our database has no idea what to do in that scenario. All it thinks of - is that we have a number in our client column, which is not tied to anything.
You can solve this issue with foreign keys, as this will tell the database to check if the client exists before doing anything with the client_id column. Let's look at an example:
Schema::create('orders', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('client_id'); $table->foreign('client_id')->references('id')->on('clients'); // ...});
Now we will do the same action - attempting to delete our client:

This time, our deletion has failed, but our Orders table is still working:

Why did this happen? Well, in the database, we have a constraint added. This limits what action can happen with our parent resource.
In our case, we can only delete a Client after first deleting all their orders. Of course, you need to handle this error with a try-catch block, but this provides a better development experience.
Even if you forget about this - Database has your back! It might break the user experience, but this will prevent bigger collapses in your system.
Notice: Foreign Key Column Names
There is one small thing to note here - column naming. In our example, we used client, which is okay to use, but standard practice is to use the singular form of the table name suffixed with _id. So, in our case, it would be:
Schema::create('orders', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('client_id'); $table->foreign('client_id')->references('id')->on('clients'); // ...});
Or, shorter:
Schema::create('orders', function (Blueprint $table) { $table->id(); $table->foreignId('client_id')->constrained(); // ...});
While this is a personal preference - it does help you to understand what the column is for. If you have client - you might think it's some identifier for the client (especially in bigger databases), but if you have client_id - you know it's a foreign key to the clients table. A quick look at a database like this - points us in the right direction:

Mistake 2: Incorrect Column Types
Another common issue with Database design is wrong column types, like creating an int field but needing to add floats later on.
One specific case is quite common - varchar fields with comma-separated values instead of using relationships. Let's look at an example:
Schema::create('orders', function (Blueprint $table) { $table->id(); $table->string('client_id'); // ...});
This seems fine, right? Well, it probably is okay until you have something like this:

What do we have here? We have a list of related IDs in the format of a string. We have seen this used as a way to have one-to-many relationships.
For example, we would expect our order to have multiple Clients. But this is different from how it works. This is just a string, and if you try to use it as a relationship:
Model
// ... public function client(){ return $this->belongsToMany(Client::class);} // ...
You will get an error like this:

Even if you try to...
Premium Members Only
This advanced tutorial is available exclusively to Laravel Daily Premium members.
Already a member? Login here
Premium membership includes:
Comments & Discussion
I am currently recreating a massive (for me; 110+ Models) project that I inherited from other developers. In short they made nearly each of these mistakes.
They never used foreign keys (and simply used the ids in Integer columns; the project was on Laravel 6 when I got it); No index except the primary key index (by just indexing a single table, I was able to get a nearly 15x performance boost on the main queries); They also stored a bunch of important data in json format in a text column (while json queries can be run, they are very slow). The db normalization also was not great and adding new features always felt like adding a bandaid to hopefully not break anything. In addition to this, they also never created tests.
Knowing where this project is headed, I am thus recreating it from scratch, and I am happy that the way I build the database structure, is avoiding these mistakes. With the new project, the main query now runs within a few miliseconds for typical parameters (old query with typical parameters, prior to optimization could run for 36 seconds, and on some parameters, your browser might timeout before it completed).
Do we always have to normalize in 100%? A dummy example would be like users table with field cities. If we plan to have only 10 users, i do not see the point of creating separate table/model for cites.
That depends on what will happen. For example:
You have 10 users, they each have a free-type field. Then you end up with different names (New-York, NY, New York) in your table. If that's fine with the system - awesome. But if you want to group them by city or something else - normalized data helps there.
I would disagree on Case 2 for Mistake 10. I don't believe that it is a good thing, to change the DB structure to allow "bad"/incomplete data to be submitted from the user. Sometimes it is okay, but then the field but be nullable anyways as it's a chosen thing in the planning phase.
Issues like that usually just come up during implementation, and changing then is just mostly out of convienience.
Well, I must be doing things the right way. I am aware of all this. I totally agree with you: building the right DB schema is key for a succesfull project. Regards