To understand how to structure the DB in Laravel or in whatever language, one of the most important concepts is relationships and understanding the differences between them. How do you choose whether it should be a belongsTo, belongsToMany, or polymorphic?
In this lesson, I will show three examples of all those relationships within the same project. The project is a typical blog or a portal of articles, and we will add additional tables to the articles with relationships. We will see which relationship is suitable for which situation.
Example 1: Photos
Scenario 1. BelongsTo Relationship
The first situation is about photos. The most typical scenario is that each article has many photos. In this case, we have a hasMany
and a belongsTo
relationship.
First, the articles are a simple table of title and text.
database/migrations/xxx_create_articles_table.php:
Schema::create('articles', function (Blueprint $table) { $table->id(); $table->string('title'); $table->string('body'); $table->timestamps();});
We have a foreign ID for the articles
table in the migration for the photos
table.
database/migrations/xxx_create_photos_table.php:
Schema::create('photos', function (Blueprint $table) { $table->id(); $table->string('filename'); $table->foreignId('article_id')->constrained(); $table->timestamps();});
Then, in the Model, every photo belongs to some article.
app/Models/Photo.php:
use Illuminate\Database\Eloquent\Relations\BelongsTo; class Photo extends Model{ protected $fillable = [ 'filename', 'article_id', ]; public function article(): BelongsTo { return $this->belongsTo(Article::class); }}
And every article has many photos.
app/Models/Article.php:
use Illuminate\Database\Eloquent\Relations\HasMany; class Article extends Model{ protected $fillable = [ 'title', 'body', ]; public function photos(): HasMany { return $this->hasMany(Photo::class); }}
This is the most simple and the most typical example.
Scenario 2. BelongsToMany Relationship
What if you want each photo to be used in multiple articles? For example, some photos are suitable for many different articles. Then you need a belongsToMany
relationship. A photo may belong to many articles, but each article still has many photos. Then, it's a two-way relationship.
You create a pivot table article_photo
with foreign key columns for both tables.
Laravel has a naming convention for creating a pivot table. It should be singular from both tables and in alphabetical order. So, not
photo_article
and notarticle_photos
.
database/migrations/xxx_create_article_photo_table.php:
Schema::create('article_photo', function (Blueprint $table) { $table->foreignId('article_id')->constrained(); $table->foreignId('photo_id')->constrained();});
Then, in the Photo
Model, instead of the belongsTo(Article)
relationship, we have belongsToMany(Article)
.
app/Models/Photo.php:
use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Photo extends Model{ protected $fillable = [ 'filename', 'article_id', ]; public function articles(): BelongsToMany { return $this->belongsToMany(Article::class); }}
In the Article
Model, it's also a belongsToMany
relationship instead of hasMany
.
app/Models/Article.php:
use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Article extends Model{ protected $fillable = [ 'title', 'body', ]; public function photos(): BelongsToMany { return $this->belongsToMany(Photo::class); }}
That's the second scenario. Do you feel the difference? Now, let's add a third scenario.
Scenario 3. Polymorphic Relationship
What if there's a separate table called videos
in addition to articles? The blog project has two types of entities: articles and videos. The photo and thumbnail may belong to either an article or a video.
One way of doing that in the photos
table is to add another foreign ID to video_id
. Both should be nullable
because only one will be present each time.
database/migrations/xxx_create_photos_table.php:
Schema::create('photos', function (Blueprint $table) { $table->id(); $table->string('filename'); $table->foreignId('article_id')->nullable()->constrained(); $table->foreignId('video_id')->nullable()->constrained(); $table->timestamps();});
Then, you should add a second belongsTo
relationship in the Photo
Model for the video.
app/Models/Photo.php:
use Illuminate\Database\Eloquent\Relations\BelongsTo; class Photo extends Model{ protected $fillable = [ 'filename', 'article_id', ]; public function articles(): BelongsTo { return $this->belongsTo(Article::class); } public function video(): BelongsTo { return $this->belongsTo(Video::class); }}
However, a polymorphic relationship should be considered for this type of scenario. Polymorphic relationships are used when an entity can belong to anything.
In the migration, you can use a morphs()
method, which will create two columns:
-
{column}_id
(the ID of the record) -
{column}_type
(the model likeApp\Models\Article
orApp\Models\Video
).
The name for the morph columns is the table name with an able
suffix, like photoable_id
and photoable_type
.
database/migrations/xxx_create_photos_tables.php:
Schema::create('photos', function (Blueprint $table) { $table->id(); $table->string('filename'); $table->foreignId('article_id')->nullable()->constrained(); $table->foreignId('video_id')->nullable()->constrained(); $table->morphs('photoable'); $table->timestamps();});
In the Photo
Model, you only need to define the morphTo
relation instead of a relationship for every Model.
app/Models/Photo.php:
use Illuminate\Database\Eloquent\Relations\MorphTo; class Photo extends Model{ protected $fillable = [ 'filename', 'article_id', ]; public function photoable(): MorphTo { return $this->morphTo(); }}
On the other side, in each parent model, you call morphMany
with the name of a relationship. In this case, it's photoable
.
app/Models/Article.php:
use Illuminate\Database\Eloquent\Relations\MorphMany; class Article extends Model{ protected $fillable = [ 'title', 'body', ]; public function photos(): MorphMany { return $this->morphMany(Photo::class, 'photoable'); }}
app/Models/Video.php:
use Illuminate\Database\Eloquent\Relations\MorphMany; class Video extends Model{ protected $fillable = [ 'title', 'video_url', ]; public function photos(): MorphMany { return $this->morphMany(Photo::class, 'photoable'); }}
When you want to get those child records by article, for example, in the ArticleController, you treat them like they would be a hasMany
relationship.
use App\Models\Article; class ArticleController extends Controller{ public function index(Article $article) { foreach ($article->photos as $photo) { echo $photo->filename; } }}
So that's the first example of belongsTo
, belongsToMany
, and Polymorphic relationships with photos.
Example 2: Comments
Let's say we have a comments
table in the same blog project, and comments belong to an article. However, in the future, comments may also belong to a video.
Again, for the comments table, there could be two foreign nullable columns for each model to which a comment belongs.
database/migrations/xxx_create_comments_table.php:
Schema::create('comments', function (Blueprint $table) { $table->id(); $table->foreignId('article_id')->nullable()->constrained(); $table->foreignId('video_id')->nullable()->constrained(); $table->string('body'); $table->timestamps();});
Or you may choose a polymorphic relationship.
database/migrations/xxx_create_comments_table.php:
Schema::create('comments', function (Blueprint $table) { $table->id(); $table->foreignId('article_id')->nullable()->constrained(); $table->foreignId('video_id')->nullable()->constrained(); $table->morphs('commentable'); $table->string('body'); $table->timestamps();});
From the practical point of view, I want you to understand the difference between a regular belongsTo
and a polymorphic belongsTo
.
Example 3: Tags
Finally, let's look at the belongsToMany
example with tags. Let's add tags to all those articles, videos, and photos in the same project. What would be their relationship to articles? It's probably belongsToMany
.
When deciding whether a relationship belongs to one article or many, my tip is to try to pronounce what makes more sense in your head. Does the tag belong to one article, or does it belong to many articles? If every tag belongs to only one article, then what's the point of the tags? The tagging system should have many tags for many articles.
database/migrations/xxx_create_tags_table.php:
Schema::create('tags', function (Blueprint $table) { $table->id(); $table->string('name'); $table->timestamps();});
In the Tag
Model, we have a belongsToMany
relationship to articles.
app/Models/Tag.php:
use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Tag extends Model{ protected $fillable = [ 'name', ]; public function articles(): BelongsToMany { return $this->belongsToMany(Article::class); }}
We also have a separate pivot table for the article_tag
.
database/migrations/xxx_create_article_tag_table.php:
Schema::create('article_tag', function (Blueprint $table) { $table->foreignId('article_id')->constrained(); $table->foreignId('tag_id')->constrained();});
What about the video situation? A tag may belong to many articles or many videos. Should it be a polymorphic relationship? In my opinion, it shouldn't. There is a way to have polymorphic relationships with many to many, but then it gets very complex.
Personally, I would add another belongsToMany
relationship for the videos and another tag_video
pivot table in the 'Tag' model.
I hope this short introduction to relationships makes it a bit clearer to you when to choose which. This is one of the typical tasks or jobs when trying to come up with a database structure: how to structure the relationships.
This is an introduction to most typical scenarios. Let's dive deeper with more examples in the following lessons.
I don't really understand why you went for "many to many" realtionships instead of "polymorphic relationship" for the tags example
Why would it be a polymorphic, if the tags are only used on the articles? It doesn't make sense. Polymorphic relations are slower than many to many and there is no need for anything else to be tagged
I think it's depends on your project. You need to feel it :)