Eloquent Relationships: The Ultimate Guide
Founder of QuickAdminPanel
Eloquent is a powerful Laravel ORM, and it allows to define relationships pretty easily. But do you know all about its functionality? Let’s check it in this ultimate guide, which will cover all types – one-to-one, one-to-many, many-to-many, and polymorphic relations.
In addition to the article, each section will have a mini demo-project with link to GitHub and video explanation.
1. Intro: DB and Foreign Keys
Let’s start with basic database theory, almost unrelated to Laravel.
To have relationships between database tables, first you still need to take care of database fields and foreign keys. Usually, in database migration statement it looks something like this:
Schema::table('posts', function (Blueprint $table) { $table->integer('user_id')->unsigned(); $table->foreign('user_id')->references('id')->on('users'); });
In this example we’re defining posts.user_id field with foreign key to users.id field.
In pure MySQL, it would look like this:
ALTER TABLE posts ADD FOREIGN KEY (user_id) REFERENCES users(id);
Important thing: we also may specify the behavior for the delete and update actions on related tables. In other words, if we delete a user, what should happen to their posts?
Here are the main options:
- CASCADE: deletes/updates child entry along with the parent entry
- RESTRICT: throws error and doesn’t delete/updates parent entry
- SET NULL: deletes parent entry and sets child foreign key entry to NULL
Notice: there are more options but they depend on database system and settings.
Here’s how it looks in Laravel migrations:
$table->foreign('user_id') ->references('id')->on('users') ->onDelete('cascade');
2. One-to-one Relationships
Ok, enough database theory, let’s go to the first type of relationships.
We will take a look at the most often example – when User has a Profile but they are in separate database tables.
So then they are related with one-to-one relationship – one user has only one profile:
In Laravel it would work with models User and UserProfile. Let’s see how it’s defined.
hasOne function
User model would look like this:
class User extends Model { function profile() { return $this->hasOne('App\UserProfile'); } }
As you can see, we have hasOne() method with only one parameter – related model’s class name with namespace.
Laravel automatically “knows” that there is a relationship on main table’s id field and related table’s user_id – it is formed by main table’s name, putting it to singular and adding _id.
But if in your case fields are different, you can add them as parameters.
For example, if you had a field users.user_id and then user_profiles.profile_user_id, relationship would look like this:
return $this->hasOne('App\UserProfile', 'profile_user_id', 'user_id');
So second parameter is a related table’s field name, and third parameter is a main table’s field.
With our relationship defined, we can use it like this.
Here’s our Controller:
class ProfileController extends Controller { public function index($user_id) { $user = User::find($user_id); return view('profile', compact('user')); } }
Then in our profile.blade.php view file we can use something like this:
Address: {{ $user->profile->address }}
belongsTo function
On the other hand, we may also have a relationship from UserProfile to User model, a reverse from hasOne. This one is called belongsTo.
class UserProfile extends Model { function user() { return $this->belongsTo('App\User'); } }
Again, Laravel automatically decides what fields are used in relationship – but you can override them with additional parameters:
return $this->belongsTo('App\User', 'profile_user_id', 'user_id');
Then we will be able to do something like this:
{{ $profile->user->email }}
Create and delete
Relationships help us not only to view information in a convenient way, we can also manage it.
For example, a usual way how you would store a UserProfile entry:
$user = User::find(1); $profile = new UserProfile; $profile->user_id = $user->id; $profile->address = "Some address in New York"; $profile->save();
But there’s a shorter way to attach a profile to the user – there’s no need to define user_id field manually.
$user = User::find(1); $profile = new UserProfile; $profile->address = "Some address in New York"; $user->profile()->save($profile);
You can also delete related relationship entry:
$user = User::find(1); $user->profile()->delete();
Eager Loading, and How does this whole "magic" works
It’s really convenient to have these relationships defined and then only access data by calling parent or child object. But it’s not that simple – you may bump into performance issues.
See this code:
class Movie extends Model { public function director() { return $this->belongsTo('App\Director'); } }
And then, for example, you want to list all movies with their directors:
$movies = Movie::all(); foreach ($movies as $movie) { echo $movie->title . ' (' . $movie->director->name . ')'; }
It will work ok, but under the hood it will perform many SQL queries.
This code will be executed like this: “For each movie record please give me the director”.
Which in reality means this:
select * from directors where id = [director_id of the first movie]; select * from directors where id = [director_id of the second movie]; select * from directors where id = [director_id of the third movie]; // ...
So one additional query for each movie. And if you view the table with 100 movies, it may significantly impact the page load.
To avoid that, you can load the relationship data within one call – it’s called eager loading.
Instead of:
$movies = Movie::all();
We do:
$movies = Movie::with('director')->get();
See the with() function here? This will perform only one additional query for all movies.
select * from directors where id in ([array of all movies director_id's])
There are more usages for eager loading, which we will discuss for more complex relationships.
Generally talking about performance, I always advice to have Laravel Debugbar package turned on locally, and to check Queries tab.
One-to-One: Demo project
Here’s a short video with demo project I’ve made.
GitHub repository for the project: https://github.com/LaravelDaily/Laravel-one-to-one-demo
3. One-to-Many Relationships
The next type of relationships is one-to-many. It is used when entry in one database table can have many related entries in another table.
In terms of database schema, nothing really changes from one-to-one – a foreign key from child table to the parent table:
In fact, one-to-many is really similar to one-to-one, you will see now.
We take a popular example: Books and their Authors. So every book belongs to one author, and one author can have many books written.
In Laravel it would work with models Author and Book. We can define relationship in both models. Let’s start with Book – guess what, we will use the same belongsTo, which we already know from one-to-one relationship. It has absolutely identical syntax:
class Book extends Model { function author() { return $this->belongsTo('App\Author'); } }
Same as one-to-one, Laravel automatically “knows” that there is a relationship on main table’s id field and related table’s author_id – it is formed by main table’s name, putting it to singular and adding _id.
In this case we have books.author_id = authors.id. Then, with this relationship defined, we can do this:
$book = Book::find(1); echo $book->author->name;
Again, as in one-to-one case, you can override the default fields with additional parameters.
For example if you want to define books.book_author_id = authors.author_id:
return $this->belongsTo('App\Author', 'book_author_id', 'author_id');
hasMany function
Inverted function of a belongsTo() is called hasMany() and is defined in the parent’s model.
In our case, Author model would look like this:
class Author extends Model { function books() { return $this->hasMany('App\Book'); } }
Again, with possibility to re-define additional columns, see above example of books.book_author_id = authors.author_id:
return $this->hasMany('App\Book', 'book_author_id', 'author_id');
So second parameter is a related table’s field name, and third parameter is a main table’s field.
Then we can use our relationship, like this:
$books = Author::find(1)->books; foreach ($books as $book) { // }
Also we can use relationships to create the data, not only query it.
$author = Author::find(1); // Saving one book $author->books()->create([ 'title' => 'Harry Potter', ]); $author->books()->createMany([ [ 'title' => 'Harry Potter' ], [ 'title' => 'Harry Potter Returns' ] ]);
Levels of relationships
Let’s imagine we have more than one level deep. So one Author has many Books, and each Book has many Chapters.
class Author extends Model { function books() { return $this->hasMany('App\Book'); } }
class Book extends Model { function chapters() { return $this->hasMany('App\Chapter'); } }
Then you can load several relationships in one sentence like this:
$author = Author::with('books.chapters')->find(1);
The result will contain all chapters for every book, which you can loop through, without any additional SQL queries.
Querying relationships
This is where real power of using relationships shows benefits.
Relationships also serve as query builders, so you can do something like this:
$book = Author::find(1)->books()->where('title', 'Harry Potter')->first();
Or this:
$bookCount = $author->books()->count();
Also, you can check if the parent model even has children entries or not, with method has():
$authors = Author::has('books')->get();
Remember our example above with two levels of relationships? You can then write this:
// Authors with at least one book and with at least one chapter $authors = Author::has('books.chapters')->get();
Also we can query more productive authors:
// All authors with 5 books or more $authors = Author::has('books', '>=', 5)->get();
It gets even more complex – function whereHas() allows you to query the child entries:
// All authors of Harry Potter books $authors = Author::whereHas('books', function ($query) { $query->where('title', 'like', 'Harry Potter%'); })->get();
And it’s not only about querying outside of the relationship.
You can define additional clauses in relationship itself. For example, you want to have separate relationships to books and published books.
class Author extends Model { function books() { return $this->hasMany('App\Book'); } function publishedBooks() { return $this->hasMany('App\Book')->where('published', 1); } }
Or if you want to always order them by title:
function books() { return $this->hasMany('App\Book')->orderBy('title'); }
Basically, relationships open a lot of possibilities to avoid writing pretty complex SQL queries, just by using relationships, and it also makes the code more readable.
HasManyThrough Relationships
Another sub-type of relationships allow you to go more than one level deep, it’s kind of a has-many within has-many (yes, “Inception”).
Imagine this database structure:
authors
- id
- name
books
- id
- author_id
- title
chapters
- id
- book_id
- title
Then we can define this:
class Author extends Model { /** * Get all of the chapters for the author. */ public function chapters() { return $this->hasManyThrough('App\Chapter', 'App\Book'); } }
This will allow us to access the chapters like this: $author->chapters.
One-to-Many Demo project
Here’s a short video with demo project I’ve made.
GitHub repository for the project: https://github.com/LaravelDaily/Laravel-one-to-many-demo
4. Many-to-Many Relationships
In this chapter we will talk about more difficult concept of many-to-many relationships, also pivot tables. This structure is used when one entry from table X can have many entries from table Y, but also the other way around – Y may have many entries in X. For that, we need a separate table that links both X and Y via foreign keys.
Official Laravel documentation takes the most common example – a user can have many roles, and a role can have many users. But we will take another often-used example – Posts and Categories. So each post can have one or more category, and each category can have one or more posts.
Database schema looks like this:
As you can see, the middle table (called pivot table) has only two fields – foreign keys to both tables. Then, basically, we can query all the categories by the post, and vice versa.
To use that effectively in Laravel, we need to define relationships with belongsToMany() function. Since there is no parent or child table here, relationship can be defined in both models, depending on what you would need to query later.
class Post extends Model { function categories() { return $this->belongsToMany('App\Category'); } }
And the other way around:
class Category extends Model { function posts() { return $this->belongsToMany('App\Post'); } }
And then we can do this “magic”:
$post = Post::find(1); foreach ($post->categories as $category) { echo $category->title; }
Or this:
$categories = Post::find(1)->categories()->orderBy('title')->get();
Have you noticed we don’t specify any fields or table names? By default, Laravel makes a few assumptions:
1. Pivot table name is a lowercase singular form of both tables, ordered alphabetically and divided with “_” symbol. So it should be category_post (not “categories_posts” and not “post_category”) or “role_user” (not “roles_users” and not “user_role”).
2. Foreign keys are assumed to be [pivot_table].[table]_id = [table].id, but you can override it, as well as pivot table name, with additional parameters:
return $this->belongsToMany('App\Category', 'posts_categories', 'post_id', 'category_id');
3. There is no need to define a separate model for pivot table, you don’t need to create CategoryPost.php model.
Creating / editing data with pivot tables
Ok, so we have a many-to-many structure, now if we need to add a category to the post, we need to add an entry to the pivot table with category_id and post_id, right? Well, Eloquent takes care of it, all we need to write is this:
$post = Post::find(1); $post->categories()->attach($category_id);
And that’s it – function attach() accept the ID and saves the data.
A convenient thing is that you can pass one ID, or array of IDs:
$post->categories()->attach([1,2,3]);
Now, if we need to remove the category, it’s also simple – with detach() function:
$post->categories()->detach($category_id); $post->categories()->detach([1,2,3]);
Or remove all categories, then just don’t pass any parameter:
$post->categories()->detach();
Another great function is sync(). Imagine that you need to save the post and user removed checkboxes from some categories, and ticked other categories – now you need to check all of them and delete some entries? No, just pass the array of final result to sync() and Eloquent will take care of it:
$post->categories()->attach([1,2,3]); // Now post has categories 1,2,3 $post->categories()->detach([2]); // Now post has categories 1,3 $post->categories()->sync([1,2]); // Now post has categories 1,2 $post->categories()->syncWithoutDetaching([1,2]); // Won't detach ID 3 from the past example
There is one more function toggle(), it will invert the presence in pivot table. All the IDs here will be inverted – attach what wasn’t attached, and then detach what was attached.
$post->categories()->toggle([1, 2, 3]);
Additional columns in pivot tables
There are cases where you want more information for the pivot table. Like, for example, timestamps – which in Laravel is represented by fields created_at and updated_at.
There’s a function for that – use it when defining a relationship:
return $this->belongsToMany('App\Category')->withTimestamps();
Then you can access those fields via ->pivot object:
foreach ($post->categories as $category) { echo $category->pivot->created_at; }
If you want to define more fields to be access via ->pivot, add them like this:
return $this->belongsToMany('App\Category')->withPivot('column1', 'column2');
It also gives you the possibility of filtering those fields:
function approvedCategories() { return $this->belongsToMany('App\Category')->wherePivot('approved', 1); } function someThreeCategories() { return $this->belongsToMany('App\Category')->wherePivotIn('some_field', [1,2,3]); }
You can also perform saving operations and define those extra fields, like this:
$post->categories()->attach([ 1 => ['approved' => 1], 2 => ['approved' => 0] ]); $attributes = ['approved' => 1]; $post->categories()->updateExistingPivot($category_id, $attributes);
Many-to-Many Demo project
Here’s a short video with demo project I’ve made.
GitHub repository for the project: https://github.com/LaravelDaily/Laravel-many-to-many-demo
5. Polymorphic relationships
One of the most difficult types of relationships are polymorphic. They mean that table may be linked to as many tables as possible. Imagine this database structure:
posts
- id
- title
images
- id
- filename
categories
- id
- name
And now you need to capture the logs of every action on any table. With Model Observers, for example.
So you would have:
logs
- id
- post_id
- image_id
- category_id
- old_value
- new_value
So one field for each table – it may be post/image/category. But what if you have dozens of tables? It’s not that convenient to have many fields in the logs then. Here’s where polymorphic relations help.
logs
- id
- loggable_id (integer)
- loggable_type (varchar)
- old_value
- new_value
So these two columns loggable_id and loggable_type are the most important.
- loggable_id column will contain the ID value of the post/image/category;
- loggable_type column will contain the class name of the related model – in our case value may be App\Post, App\Image or App\Category.
And in the model you will define it like this:
class Log extends Model { /** * Get all of the owning loggable models. */ public function loggable() { return $this->morphTo(); } } class Post extends Model { /** * Get all of the post's logs. */ public function logs() { return $this->morphMany('App\Log', 'loggable'); } } class Image extends Model { /** * Get all of the image's logs. */ public function logs() { return $this->morphMany('App\Log', 'loggable'); } } class Category extends Model { /** * Get all of the category's logs. */ public function logs() { return $this->morphMany('App\Log', 'loggable'); } }
And then you can retrieve the relationships similar to hasMany:
$post = Post::find(1); foreach ($post->logs as $log) { // }
Also the other way around – you can get an instance of related parent model:
$log = Log::find(1); $loggable = $log->loggable; // will return Post/Image/Category
Notice that there’s no direct foreign key in the database – relationship is kind of “artificial”.
If you don’t like storing full model path in loggable_type field, instead of App\Post you can save only post, but you need to define the “morph map”, attaching those models to your strings.
Inside of the boot() function of your AppServiceProvider you may add this:
use Illuminate\Database\Eloquent\Relations\Relation; Relation::morphMap([ 'posts' => 'App\Post', 'images' => 'App\Image', 'categories' => 'App\Category', ]);
Many-to-many Polymorphic Relations
Yes, these relationships can also be many-to-many.
Imagine this scenario: DB tables posts, pages and categories. And you need to attach categories to either posts or pages. And you would have to create two separate many-to-many tables, right?
- category_post
- category_page
But you can also do it with polymorphic relations, like this: categoriables
- category_id
- categoriable_id (meaning post_id or page_id)
- categoriable_type (model, like App\Post or App\Page)
And then define the relationships in the model:
class Post extends Model { /** * Get all of the categories for the post. */ public function categories() { return $this->morphToMany('App\Category', 'categoriable'); } }
And vice versa:
class Category extends Model { /** * Get all of the posts that are assigned this category. */ public function posts() { return $this->morphedByMany('App\Post', 'categoriable'); } /** * Get all of the pages that are assigned this category. */ public function pages() { return $this->morphedByMany('App\Page', 'categoriable'); } }
Finally, this is how you use this relationship:
$post = Post::find(1); foreach ($post->categories as $category) { // }
Or we can get posts by category:
$category = Category::find(1); foreach ($category->posts as $post) { // }
Polymorphic Demo project
Here’s a short video with demo project I’ve made.
GitHub repository for the project: https://github.com/LaravelDaily/Laravel-polymorphic-demo
So, that’s all about Eloquent relationship for now.
If you want to try it out in our Laravel adminpanel generator, we support belongsTo and belongsToMany relationships, so you don’t need to add code there, here’s a short video:
Any questions? Anything to add? Please add to the comments.