Khi học về csdl (mysql hay sql server) thì hầu hết chúng ta cũng biết về mối quan hệ. Thì trong Laravel cũng vậy, đề làm việc giữa các model ta cũng phải khai báo những quan hệ trong đó. Cùng mình bắt tay tìm hiểu ngay bây giờ nào.
Giới thiệu
Khi học về cơ sở dữ liệu có những mối quan hệ nào thì Laravel cũng hỗ trợ hầu hết các mối quan hệ đó. Nó giúp chúng ta dễ dàng thao tác dữ liệu trên nhiều bảng vô cùng dễ dàng và nhanh chóng.
Defining Relationships
One to One (1-1)
Đây là kiểu quan hệ đơn giản nhất, ta hiểu rằng 1 cái này chỉ phụ thuộc vào một cái khác và ngược lại. Ví dụ, ta có bảng Users
và Avatar
, ở đây có nghĩa một user chỉ có đúng một avatar và avatar này chỉ đại diện cho user đó. Để biểu diễn mối quan hệ này trong models
ta sử dụng method hasOne
:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use App\Models\Avatar;
class User extends Authenticatable
{
...
public function avatar()
{
return $this->hasOne('App\Avatar');
// or
return $this->hasOne(Avatar::class);
// or
return $this->hasOne(Avatar::class, 'user_id', 'id');
}
}
Ở trên ta thấy tham số truyền vào đầu tiên trong method hasOne
là tên của model liên quan đến bảng đó. Có 2 cách truyền vào, 1 là truyền namespace
của model đó vào, 2 là truyền trực tiếp class của model đó vào. Tham số thứ 2 và thứ 3 lần lượt là khóa ngoại và khóa chính.
Đi sâu vào một chút xíu, vì sao cách khai báo đầu tiên và thứ 2 được sử dụng mặc dù không truyền khóa ngoại và khóa chính vào. Nó chỉ hoạt động được khi khóa ngoại là user_id
trong bảng Avatar
và khóa chính là id
trong bảng Users
.
Mặc định nó sẽ lấy tên class ghép với khóa chỉnh của model làm khóa ngoại, và lấy primaryKey
làm khóa chính .
public function hasOne($related, $foreignKey = null, $localKey = null)
{
$instance = $this->newRelatedInstance($related);
$foreignKey = $foreignKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
return $this->newHasOne($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
}
public function getForeignKey()
{
return Str::snake(class_basename($this)).'_'.$this->getKeyName();
}
public function getKeyName()
{
return $this->primaryKey;
}
Sau khi khai báo, để lấy được avatar của 1 user nào đó, đơn giản chỉ làm như sau:
$avatar = User::find(1)->avatar;
Inverse
Tương tự khai báo phương thức user
trong model Avatar
:
<?php
namespace App;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
class Avatar extends Model
{
protected $table = 'avatar';
public function user()
{
return $this->belongsTo(User::class);
// or
return $this->belongsTo(User::class, 'user_id', 'id');
}
}
One to Many (1-n)
Mối quan hệ này để biểu thị mối quan hệ cha-con. Tức là một cha có nhiều con nhưng một con chỉ có một cha (tính ca ruột thôi nha).
Ví dụ dưới đâu nói về mối quan hệ giữa users
có nhiều bài posts
:
<?php
namespace App;
use App\Models\Post;
use Illuminate\Database\Eloquent\Model;
class User extends Authenticatable
{
public function posts()
{
return $this->hasMany(Post::class);
}
}
Tương tự như quan hệ One-One, để lấy những bài biết của user ta thử như sau:
$posts = App\User::find(1)->posts;
Trả về 1 collection
của Post
và ngược lại trên model Post
:
namespace App;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}
Many to Many (n-n)
Quan hệ này thì nó phức tạp hơn so với hai quan hệ trước đó. Khi học csdl thì ta đã hiểu quan hệ nhiều nhiều thì gồm 3 bảng. Thì đây cũng vậy. Ví dụ một product
sẽ thuộc nhiều orders
và một order
lại cho nhiều products
. Để biểu diễn được quan hệ này thì chúng ta phải nhờ sự trợ giúp của một bảng thứ 3, bảng trung gian giúp tạo quan hệ cho 2 bảng kia, mình đặt tên là order_product
và đồng thời chưa 2 cột order_id
và product_id
và chúng ta không cần tạo model cho bảng trung gian này. Theo dõi ví dụ bên dưới:
namespace App;
use App\Models\Order;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
public function orders()
{
return $this->belongsToMany(Order::class);
}
}
Muốn lấy ra được một product
có bao nhiêu order
chỉ cần:
$orders = App\Product::find(1)->orders;
Tương tự như One-One, belongsToMany
cũng đã tạo những tham số mặc định, Eloquent sẽ tự động tìm đến bảng trung gian đặt tên theo thứ tự alphabet, trong trường hợp này là order_product
. Tuy nhiên đời đâu như là mơ, đấu phải lúc nào cx được đặt tên bảng theo ý mình thích được, giả dụ là product_order
thì cần phải truyền tham số thứ 2 vào:
return $this->belongsToMany(Order::class, 'product_order');
Đi sâu vào thì Laravel cũng đã định nghĩa những giá trị mặc định nếu chúng ta không truyền các tham số vào.
/**
* Define a many-to-many relationship.
*
* @param string $related
* @param string|null $table
* @param string|null $foreignPivotKey
* @param string|null $relatedPivotKey
* @param string|null $parentKey
* @param string|null $relatedKey
* @param string|null $relation
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function belongsToMany($related, $table = null, $foreignPivotKey = null, $relatedPivotKey = null,
$parentKey = null, $relatedKey = null, $relation = null)
{
...
}
Diễn tả một xíu về các tham số truyền vào:
$related
: là class mà mình muốn tạo quan hệ.$table
: là bảng trung gian.$foreignPivotKey
: là khóa ngoại của bảng đang định nghĩa quan hệ (product_id trên bảng order_product).$relatedPivotKey
: là khóa ngoại của bảng mà chúng ta tạo quan hệ (order_id trên bảng order_product).$parentKey
: là khóa chính của bảng đang định nghĩa quan hệ (mặc định là id).$relatedKey
: là khóa chính của bảng quan hệ (mặc định là id).$relation
: là tên của quan hệ (không quan trọng).
Ngược lại, đối với model Order
ta định nghĩa tương tự như model Product
:
namespace App;
use App\Models\Product;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
public function products()
{
return $this->belongsToMany(Product::class);
}
}
Lấy giá trị bảng trung gian
Để làm việc với mối quan hệ Many to Many này thì chúng ta cần sử dụng đến một bảng trung gian. Eloquent cũng hỗ trợ giúp chúng ta lấy được các giá trị của bảng này. Để truy cập đến các cột của bảng trung gian chúng ta sẽ sử dụng thuộc tính pivot
. Ví dụ :
$product = App\Product::find(1);
foreach($product->orders as $order)
{
echo $order->pivot->created_at;
}
Theo mặc định thì Eloquent chỉ lấy các trường trung gian là created_at, update_at
nếu chúng ta muốn lấy ra giá trị của một cột khác thì cần khai báo thêm như sau, giả sử chúng ta cần lấy thêm trường address
return $this->belongsToMany(Product::class)->withPivot('address');
Hoặc là khi bạn muốn hai trường created_at và update_at
của bảng trung gian tự động cập nhật giá trị thì khai báo thêm
return $this->belongsToMany(Product::class)->withTimestamps();
Đôi khi người dùng lại muốn thay đổi tên của thuôc tính pivot
thì phải làm như thế nào, chỉ cần sử dụng method as
được khai báo trong model là xong. Ví dụ
return $this->belongsToMany(Product::class)
->as('newname')
->withTimestamps();
giờ muốn truy cập các thuộc tính của bảng trung gian thay thế pivot
thành newname
là được.
$product = App\Product::find(1);
foreach($product->orders as $order)
{
echo $order->newname->created_at;
}
Một câu hỏi nữa được đặt ra là nếu muốn lấy các sản phẩm với điều kiện của bảng trung gian là hợp lệ thì sẽ như thế nào, rất đơn giản, Laravel cũng hỗ trợ chúng ta trong vấn đề này.
public function products()
{
return $this->belongsToMany(Product::class)->wherePivot('price', '>', 20000);
}
Ở đây sẽ lấy ra các Order có giá lớn hơn 20000.
Insert & Update
Ở các mối quan hệ phía trên, đơn giản chỉ cần lưu id
của quan hệ mình muốn tạo vào trong model là xong, nhưng ở quan hệ n-n
thì có đôi chút khác.
Vì bảng trung gian không có model để quản lý nên ta sẽ quản lý nó thông qua model mà chúng ta cần handle.
Ví dụ một User
có nhiều quyền, và chúng ta thay đổi nó. Thì đầu tiên chúng ta cần có user
và role
sẵn thì mới có thể tạo quan hệ được.
Để thêm quyền cho các user chúng ta sử dụng attach
use App\Models\User;
$user = User::find(1);
$user->roles()->attach($roleId);
Bảng trung gian có những cột giá trị khác, thì khi thêm vào có thể kèm theo:
$user->roles()->attach($roleId, ['expires' => $expires]);
Ngược lại, để xóa một role
ra khỏi user
// Detach a single role from the user...
$user->roles()->detach($roleId);
// Detach all roles from the user...
$user->roles()->detach();
Ngoài ra chúng ra có thể thêm nhiều role
vào 1 lần:
$user = User::find(1);
$user->roles()->detach([1, 2, 3]);
$user->roles()->attach([1, 2, 3]);
$user->roles()->attach([
1 => ['expires' => $expires],
2 => ['expires' => $expires],
]);
Sync
Một số trường hợp chúng ta muốn cập nhật lại toàn bộ role
của thằng user
thì ta sử dụng phương thức sync
$user->roles()->sync([1, 2, 3]);
$user->roles()->sync([1 => ['expires' => true], 2, 3]);
Sau khi đồng bộ thì thằng user
chỉ có đúng 3 role
truyền vào.
Has One Of Many
Đôi khi giữa 2 bảng có quan hệ 1-n nhưng ta chỉ muốn lấy 1 recore mới nhất, thì ta khai báo như sau:
public function latestOrder()
{
return $this->hasOne(Order::class)->latestOfMany();
}
Hoặc cũ nhất
public function latestOrder()
{
return $this->hasOne(Order::class)->oldestOfMany();
}
hoặc lấy order nào có giá trị lớn nhất
public function latestOrder()
{
return $this->hasOne(Order::class)->orderBy('total', 'desc')
}
Has One Through
Đây là mối quan hệ liên kết các bảng với nhau thông qua một bảng trung gian.
Giả sử ta có 3 bảng như sau:
users
id - integer
supplier_id - integer
suppliers
id - integer
history
id - integer
user_id - integer
Mặc dù bảng history
không chứa supplier_id
nhưng chúng ta vẫn có thể truy cập đến history
từ suppliers
bởi mối quan hệ hasOneThrough
như sau
<?php
namespace App;
use App\Models\History;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
class Supplier extends Model
{
public function userHistory()
{
return $this->hasOneThrough(History::class, User::class);
}
}
Với tham số thứ nhất được truyền vào là tên của model mà chúng ta muốn truy cập, tham số thứ 2 là model trung gian. Chúng ta cũng có thể custom các key liên quan đến mối quan hệ này lần lượt là các tham số sau vào hàm định nghĩa quan hệ.
class Supplier extends Model
{
public function userHistory()
{
return $this->hasOneThrough(
History::class,
User::class,
'supplier_id', // Khóa ngoại của bảng trung gian user
'user_id', // Khóa ngoại của bảng chúng ta muốn truy cập đến
'id', // Khóa mà chúng ta muốn liên kết ở bảng supplier
'id' // Khóa mà chúng ta muốn liên kết ở bảng user
);
}
}
Ở model User
và History
chúng ta định nghĩa như bình thường
namespace App;
use App\Models\Supplier;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public function supplier()
{
return $this->belongsTo(Supplier::class);
}
}
namespace App;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
class Supplier extends Model
{
public function user()
{
return $this->hasOne(User::class);
}
}
Has Many Through
Mối quan hệ này cung cấp cho chúng ta cách truy cập bảng liên kết dễ dàng thông qua bảng trung gian. Khác với n-n, bảng trung gian trong quan hệ many to many thì không cần khai báo model và cũng như không sử dụng nhiều.
Ví dụ Team
có nhiều bài Post
thông qua bảng trung gian là User
teams
id - integer
name - string
users
id - integer
team_id - integer
name - string
posts
id - integer
user_id - integer
title - string
Chúng ta biểu diễn quan hệ như sau
<?php
namespace App;
use App\Models\User;
use App\Models\Post;
use Illuminate\Database\Eloquent\Model;
class Team extends Model
{
public function posts()
{
return $this->hasManyThrough(Post::class, User::class);
}
}
Tương tự như hasOneThrough
, chúng ta cũng có thể thay đổi khóa chính và khóa ngoại ràng buộc
class Team extends Model
{
public function posts()
{
return $this->hasManyThrough(
Post::class,
User::class,
'team_id', // khóa ngoại của bảng trung gian
'user_id', // khóa ngoại của bảng mà chúng ta muốn gọi tới
'id', //trường mà chúng ta muốn liên kết ở bảng đang sử dụng
'id' // trường mà chúng ta muốn liên kết ở bảng trung gian.
);
}
}
Polymorphic Relationships
Đây là mối quan hệ đa hình trong Laravel cho phép 1 Model có thể belongsTo nhiều Model khác mà chỉ cần dùng 1 associate.
One to One
Mối quan hệ này tương tự quan hệ One to One
mình đã giới thiệu phía trên, tuy nhiên mục đích của mối quan hệ này là 1 model cso thể belongsTo 1 hay nhiều model khác. Ví dụ một bài post
có một image
và một product
cũng có một image
, nếu như bình thường phải tạo một bảng để lưu ảnh của post
và một bảng để lưu ảnh của product
, giả sử có nhiều bảng cần đến image thì lại phải tạo nhiều bảng để lưu trữ hình ảnh. Vậy nên mối quan hệ Polymorphic
được sinh ra:
posts
id - integer
name - string
products
id - integer
name - string
images
id - integer
url - string
imageable_id - integer
imageable_type - string
Đây là cách để xây dừng mối quan hệ polymorphic này. Với imageable_id
sẽ lưu id
của bảng post
và product
, còn trường imageable_type
sẽ lưu tên class hai model đó. Theo convention của laravel thì bảng lưu trung gian sẽ bắt buộc phải có 2 trường id và type nhưng để rõ ràng hơn thì sẽ lưu thêm tiền tố tên_bảng_bỏ_s + able_
.
Trong migrate
đơn giản chỉ cần khai báo như sau:
public function up()
{
Schema::create('image', function (Blueprint $table) {
$table->bigIncrements('id');
...
$table->morphs('imageable');
$table->timestamps();
});
}
morphs
sẽ tự sinh ra 2 cột như trên.
Tiếp theo cần khai báo vào model
<?php
class Image extends Model
{
public function imageable()
{
return $this->morphTo();
}
}
class Post extends Model
{
public function image()
{
return $this->morphOne(Image::class, 'imageable');
}
}
class Product extends Model
{
public function image()
{
return $this->morphOne(Image::class, 'imageable');
}
}
Để lấy image thuộc posts
thì trỏ đến image
$post = App\Post::find(1);
$image = $post->image;
và ngược lại, nhưng mình không khuyến khích dùng như này, mất công mình phải check nó là model gì để không bị lỗi data
$image = App\Image::find(1);
$imageable = $image->imageable;
Insert & Update
$image = new Image([
...
])
$post = App\Post::find(1);
$post->image()->save($image);
// or
$post = Post::find(1);
$comment = $post->image()->create([
'name' => 'helloworld',
]);
One to Many
Mối quan hệ này cũng gần giống với quan hệ One to Many
. Ví dụ một User
có thể comment ở cả Post
lẫn Video
thì chỉ cần 1 bảng comments
trong trường hợp này
posts
id - integer
title - string
body - text
videos
id - integer
title - string
url - string
comments
id - integer
body - text
commentable_id - integer
commentable_type - string
Cấu trúc model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
public function commentable()
{
return $this->morphTo();
}
}
class Post extends Model
{
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
class Video extends Model
{
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
Lấy comment ra
$post = App\Post::find(1);
foreach ($post->comments as $comment) {
echo $comment->content;
}
Và ngược lại, cũng như phía trên mình cũng không khuyến khích truy vấn ngược
$comment = App\Comment::find(1);
$commentable = $comment->commentable;
Insert & Update
Tương tự như one-one, việc tạo các records vô cùng đơn giản
//project message add
$project = Project::find(1);
$message = new Message;
$message->body = "Hi nqdat.com";
$project->messages()->save($message);
//video message add
$video = Video::find(1);
$message = new Message;
$message->body = "Hi nqdat.com";
$video->messages()->save($message);
Many to Many
Quan hệ này phức tạp hơp một chút. Ví dụ một post
hay là video
có thể có nhiều tags
. Sử dụng mối quan hệ many to many polymorphic
cho phép bạn truy vấn lấy ra các tags
thuộc về một post
hay video
posts
id - integer
name - string
videos
id - integer
name - string
tags
id - integer
name - string
taggables
tag_id - integer
taggable_id - integer
taggable_type - string
Cấu trúc model
//post.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
}
//tag.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
public function posts()
{
return $this->morphedByMany(Post::class, 'taggable');
}
public function videos()
{
return $this->morphedByMany(Video::class, 'taggable');
}
}
Xong rồi, muốn lấy các tag
của 1 post
cũng làm tương tự như các mối quan hệ khác
$post = App\Post::find(1);
foreach ($post->tags as $tag) {
//
}
Và ngược lại
$tag = App\Tag::find(1);
foreach ($tag->videos as $video) {
//
}
Insert & Update
$post = Post::find(1);
$tag = new Tag;
$tag->name = "nqdat.com";
$post->tags()->save($tag);
$video = Video::find(1);
$tag = new Tag;
$tag->name = "ItSolutionStuff.com";
$video->tags()->save($tag);
// or
$post->tags()->saveMany([$tag1, $tag2]);
$video->tags()->saveMany([$tag1, $tag2]);
Nếu như đã có tag sẵn, thì ta có thể sử dụng attach
$video = Video::find(1);
$tag1 = Tag::find(3);
$tag2 = Tag::find(4);
$video->tags()->attach([$tag1->id, $tag2->id]);
Hoặc đồng bộ lại
$post->tags()->sync([$tag1->id, $tag2->id]);
Nghĩa là nếu không có tag1 tag2 sẽ thêm vào, cái nào khác thì sẽ bỏ ra.
Cuối cùng là detach
$post->tags()->detach([$tag1->id, $tag2->id]);
Kết luận
Qua bày này mình giới thiệu tới các bạn các chứng năng cơ bản và phổ biến sử dụng nhiều trong các ứng dụng. Có thể chưa đáp ứng nhu cầu của các bạn, bạn có thể tham khảo thêm trên trang chủ của Laravel.
Ngoài ra bạn nên tìm hiểu thêm về property
hay method
để có thể nắm model
trong lòng bàn tay nha
Nguồn tham khảo:
Rất mong được sự ủng hộ của mọi người để mình có động lực ra những bài viết tiếp theo.
{\__/}
( ~.~ )
/ > ♥️ I LOVE YOU 3000
JUST DO IT!