Laravel Eloquent ORM phần 2: Xử lý database relationship

Trong thế giới thực tại, mọi vật đều có những kết nối với nhau, ví dụ một ngôi nhà phải có người sở hữu, một cuốn sách phải có tác giả (một hoặc nhiều tác giả), một đơn hàng phải liên quan đến một hoặc nhiều sản phẩm… Cơ sở dữ liệu cũng giống như vậy, mỗi bảng là một cá thể và các bảng có thể có các mối quan hệ với nhau do các bảng này cũng đều đại diện cho một vật nào đó. Laravel Eloquent ORM xử lý các mối quan hệ dựa trên các Eloquent Model rất dễ dàng, nó hỗ trợ rất nhiều các mối quan hệ khác nhau trong database như một – một, một – nhiều, nhiều – nhiều, quan hệ đa ngôi, quan hệ đa hình…

1. Định nghĩa các mối quan hệ trong Eloquent Model

Eloquent Model định nghĩa các quan hệ bằng các phương thức trong Model, một ưu điểm là có thể sử dụng chuỗi phương thức và tăng cường khả năng truy vấn dữ liệu. Ví dụ dưới đây giúp bạn thấy được sức mạnh của khai báo relationship trong Eloquent Model:

$user = User::where('name', 'FirebirD')->first();
$posts = $user->post()->where('active', 1)->get();

biến $posts sẽ chứa tập hợp các bài viết của một user đang được đăng. Nếu như không có các thiết lập quan các quan hệ, đoạn code trên phải thực hiện như sau:

$user = User::where('name', 'FirebirD')->first();
$posts = Post::where('user_id', $user->id)->where('active', 1)->get();

Đây chỉ là một ví dụ đơn giản nhất, bạn đã thấy việc khai báo relationship trong Eloquent Model giúp cho code ngắn gọn nhưng cũng rất tường minh. Chúng ta cùng bắt đầu xem xét các mối quan hệ trong cơ sở dữ liệu sẽ được xử lý như thế nào trên Model.

1.1 Quan hệ 1 – 1

1.1.1 Định nghĩa quan hệ 1 – 1

Quan hệ một – một (còn gọi là quan hệ 1-1 hay one to one) là mối quan hệ cơ bản nhất, là mối quan hệ giữa hai tập thực thể mà mỗi thực thể thuộc tập này chỉ có duy nhất một mối quan hệ với một thực thể thuộc tập kia. Ví dụ User chỉ có một Phone, để định nghĩa mối quan hệ này, chúng ta sẽ đưa phương thức phone() vào model User. Trong phương thức phone() sẽ sử dụng phương thức hasOne() để trả về kết quả cho mối quan hệ.

<?php

namespace App;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public function phone()
    {
        return $this->hasOne('App\Phone');
    }
}

Phương thức hasOne() có nhiều tham số:
Tham số thứ nhất chính là tên của Model có quan hệ, khi quan hệ được định nghĩa, chúng ta có thể lấy các bản ghi liên quan thông qua các thuộc tính động của Eloquent. Các thuộc tính động cho bạn truy xuất vào phương thức quan hệ.

$phone = User::find(68)->phone;

Tham số thứ hai là khóa ngoại của quan hệ, trong ví dụ trên model Phone giả sử rằng khóa ngoại là user_id, nếu tên trường khóa ngoại khác, bạn có thể đưa nó vào tham số thứ hai này.

return $this->hasOne('App\Phone', 'foreign_key');

Tham số thứ ba là trường khóa chính, mặc định sẽ sử dụng trường id hoặc trường được thiết lập trong biến $primaryKey của Model. Nếu bạn sử dụng một giá trị khác với trường id, bạn có thể đưa vào tham số thứ ba này.

return $this->hasOne('App\Phone', 'foreign_key', 'local_key');

1.1.2 Định nghĩa chiều ngược lại trong quan hệ 1-1

Quan hệ 1-1 là quan hệ có tính khả chuyển (transferable) nếu thực thể con có thể liên kết lại với thực thể cha. Trong ví dụ trên, nếu từ Phone chúng ta có thể tìm ra được User thì quan hệ 1-1 này có tính khả chuyển, Eloquent định nghĩa quan hệ 1-1 theo chiều ngược này bằng cách đưa phương thức user() vào trong Model Phone và sử dụng phương thức belongsTo() để thực hiện:

<?php
namespace App;
use Illuminate\Database\Eloquent\Model;

class Phone extends Model
{
    public function user()
    {
        return $this->belongsTo('App\User');
    }
}

Tương tự như phương thức hasOne(), phương thức belongsTo() cũng có ba tham số, tham số thứ nhất là tên Model có quan hệ ngược, tham số thứ hai là tên trường dùng làm khóa ngoại và tham số thứ ba là tên trường dùng làm khóa chính.

1.2. Quan hệ một – nhiều

1.2.1 Định nghĩa quan hệ một -nhiều

Quan hệ 1-n (còn gọi là quan hệ một – nhiều hay one to many) là quan hệ giữa hai tập thực thể mà mỗi thực thể thuộc tập này có quan hệ với nhiều thực thể thuộc tập kia. Ví dụ, một bài viết có thể có rất nhiều các bình luận. Eloquent cho phép định nghĩa mối quan hệ này bằng cách đưa phương thức comment() vào Model Post và sử dụng phương thức hasMany():

<?php

namespace App;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    public function comments()
    {
        return $this->hasMany('App\Comment');
    }
}

Phương thức hasMany() cũng như hasOne() sẽ có ba tham số, nếu không đưa vào các tham số 2 và 3 thì nó sẽ sử dụng các giá trị mặc định.

return $this->hasMany('App\Comment', 'foreign_key', 'local_key');

Khi quan hệ giữa Post và Comment được định nghĩa, bạn có thể lấy các Comment của một Post dễ dàng như sau:

$comments = App\Post::find(68)->comments;

foreach ($comments as $comment) {
    // Xử lý từng comment của post
}

1.2.2 Định nghĩa chiều ngược lại trong quan hệ một – nhiều

Trong phần định nghĩa quan hệ thuận 1-n, mỗi Post chúng ta có thể lấy được tất cả các Comment, quan hệ ngược 1-n giúp mỗi Comment có thể truy xuất được Post mà nó thuộc về. Mối quan hệ này cũng được định nghĩa bằng phương thức belongsTo() như ở phần quan hệ ngược 1-1.

<?php

namespace App;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    public function post()
    {
        return $this->belongsTo('App\Post');
    }
}

Khi đó, để lấy các thuộc tính của Post khi biết Comment cụ thể chúng ta chỉ việc thực hiện như sau:

$comment = App\Comment::find(1);

echo $comment->post->title;

1.3 Quan hệ nhiều – nhiều

1.3.1 Định nghĩa quan hệ nhiều – nhiều

Quan hệ n-n (còn gọi là quan hệ nhiều – nhiều, hay quan hệ many to many) là mối quan hệ giữa hai tập thực thể mà mỗi thực thể thuộc tập này có thể có quan hệ với nhiều thực thể của tập kia và ngược lại. Ví dụ, một Product có thể nằm trong nhiều Order khác nhau và một Order có thể có nhiều Product khác nhau, do đó mối quan hệ Product-Order là quan hệ n-n. Định nghĩa quan hệ này trong Eloquent sử dụng phương thức belongsToMany().

<?php

namespace App;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    public function order()
    {
        return $this->belongsToMany('App\Order');
    }
}

Khi đó muốn lấy được các Order có liên quan đến Product này, chúng ta chỉ việc thực hiện như sau:

$product = App\Product::find(1);

foreach ($product->order()->where('payment', 1) as $order) {
    // In số tiền các đơn hàng đã được thanh toán liên quan đến sản phẩm 1.
    echo $order->totalAmount;
}

Chúng ta tìm hiểu thêm về phương thức belongsToMany(), phương thức này có 4 tham số

Tham số thứ nhất là tên Model có quan hệ giống như các phương thức hasOne(), hasMany(), belongsTo().

Tham số thứ hai là tên bảng dữ liệu giao nhau do quan hệ n-n phải có tập các dữ liệu giao nhau giữa hai tập thực thể, nếu tên bảng này được đặt theo quy tắc product_order thì không cần khai báo, nếu dùng tên khác bạn cần khai báo vào tham số thứ hai này.

Tham số thứ ba là khóa ngoại của Model mà đang được định nghĩa quan hệ, tham số thứ 4 là khóa ngoại của Model sử dụng để join.

1.3.2 Định nghĩa chiều ngược lại trong quan hệ nhiều – nhiều

Vì quan hệ n-n có tính đối xứng nên định nghĩa quan hệ n-n với quan hệ ngược n-n giống nhau, chỉ cần để ý là đang định nghĩa quan hệ cho model nào:

<?php

namespace App;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    public function product()
    {
        return $this->belongsToMany('App\Product');
    }
}

1.3.3 Lấy dữ liệu bảng trung gian trong quan hệ nhiều – nhiều

Như chúng ta đã biết, quan hệ n-n cần có bảng trung gian, trong ví dụ trên đó chính là bảng product_order. Eloquent ORM cung cấp sẵn một số phương thức hữu hiệu để làm việc với các bảng trung gian này. Để truy xuất các cột trong bảng trung gian chúng ta sử dụng thuộc tính pivot trên đối tượng.

$product = App\Product::find(1);

foreach ($product->order as $order) {
    // In ngày tạo đơn hàng trong cột created_at nằm trong bảng product_order
    echo $order->pivot->created_at;
}

Mặc định, chỉ các khóa của Model có trong đối tượng pivot, nếu bảng pivot của bạn chứa các thuộc tính khác nữa, bạn cần khai báo chúng khi định nghĩa quan hệ.

return $this->belongsToMany('App\Role')->withPivot('column1', 'column2');

Với các cột created_at, updated_at nếu muốn pivot tự động quản lý sử dụng phương thức withTimestamps():

return $this->belongsToMany('App\Role')->withTimestamps();

1.3.4 Lọc kết quả quan hệ trong qua bảng trung gian

Đôi khi chúng ta muốn trả về các bản ghi trong mối quan hệ hạn chế bởi việc lọc qua một số cột trong bảng trung gian có thể sử dụng các phương thức wherePivot() hoặc wherePivotIn() khi định nghĩa quan hệ.

return $this->belongsToMany('App\Product')->wherePivot('review', 1);

return $this->belongsToMany('App\Product')->wherePivotIn('priority', [1, 2]);

1.4 Quan hệ 3 ngôi

Quan hệ 3 ngôi (Has many through) là quan hệ giữa 3 tập thực thể mà mỗi thực thể tập A quan hệ với thực thể tập C thông qua mối quan hệ của thực thể tập B. Ví dụ sau giúp bạn hiểu rõ hơn về mối quan hệ 3 ngôi, một Country có nhiều Post thông qua User. Như vậy User sẽ là trung gian của mối quan hệ giữa Country và Post.

Quan hệ 3 ngôi

Khi đó muốn lấy tất cả các Post của một Country cho trước, chúng ta cần định nghĩa mối quan hệ 3 ngôi này.

<?php

namespace App;
use Illuminate\Database\Eloquent\Model;

class Country extends Model
{
    public function posts()
    {
        return $this->hasManyThrough('App\Post', 'App\User');
    }
}

Vì Post không có cột country_id nên phải thông quan mối quan hệ 3 ngôi với User làm trung gian. Sau khi định nghĩa, để lấy các post của một country chúng ta thực hiện đơn giản như sau:

$country = Country::find('VN');
foreach($country->post as $post){
    echo $post->title;
}

Chú ý, phương thức hasManyThrough() cũng có thêm các tham số để xác định tên cột làm khóa ngoại nếu như các bảng được đặt tên không theo chuẩn.

return $this->hasManyThrough('App\Post', 'App\User', 'country_id', 'user_id', 'id');
  • Tham số thứ nhất là Model cần quan hệ.
  • Tham số thứ hai là Model trung gian.
  • Tham số 3 là khóa ngoại của Model trung gian.
  • 4 là khóa ngoại của Model cần quan hệ.
  • 5 là khóa chính của Model đang định nghĩa quan hệ.

1.5 Quan hệ đa hình – Polymorphic Relations

Quan hệ đa hình cho phép một thực thể của một tập hợp phụ thuộc vào một hoặc nhiều thực thể của các tập hợp khác. Ví dụ, một người dùng trong ứng dụng có thể bình luận cả các bài viết và sản phẩm. Nếu sử dụng quan hệ đa hình chúng ta có thể sử dụng duy nhất bảng comment cho tình huống này.

Quan hệ đa hình

Trong bảng Comment sẽ có hai cột là object_id và object_type: object_id chứa id của bảng Post và Product, trong khi object_type chứa tên hai bảng này. Chúng ta sẽ định nghĩa mối quan hệ này như sau:

<?php

namespace App;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    public function object()
    {
        return $this->morphTo();
    }
}

class Post extends Model
{
    public function comments()
    {
        return $this->morphMany('App\Comment', 'object');
    }
}

class Product extends Model
{
    public function comments()
    {
        return $this->morphMany('App\Comment', 'object');
    }
}

Khi đó, chúng ta dễ dàng lấy các bình luận của một bài viết hay một sản phẩm như sau:

$post = App\Post::find(1);
foreach ($post->comments as $comment) {
    //
}

$product= App\Product::find(2);

foreach ($product->comments as $comment) {
    //
}

Và từ comment chúng ta cũng dễ dàng lấy lại các đối tượng mà comment thuộc về:

$comment = App\Comment::find(1);

$object = $comment->object;

$object này có thể là một instance của Post hoặc Product.

1.5.1 Quan hệ đa hình nhiều – nhiều

Quan hệ đa hình nhiều nhiều

Chúng ta cùng xem ví dụ sau để hiểu về quan hệ đa hình nhiều nhiều, Post quan hệ n-n với Tag, Product quan hệ n-n với Tag, khi đó nếu sử dụng quan hệ đa hình n-n, chúng ta chỉ cần duy nhất một bảng tag cho cả hệ thống. Khi đó để định nghĩa mối quan hệ này chúng ta thực hiện như sau:

<?php

namespace App;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Get all of the tags for the post.
     */
    public function tags()
    {
        return $this->morphToMany('App\Tag', 'taggable');
    }
}

class Product extends Model
{
    /**
     * Get all of the tags for the product.
     */
    public function tags()
    {
        return $this->morphToMany('App\Tag', 'taggable');
    }
}

1.5.2 Định nghĩa chiều ngược lại trong quan hệ đa hình n-n

Trong chiều ngược lại, chúng ta muốn định nghĩa quan hệ của Tag với Post và Product:

<?php

namespace App;
use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
    public function posts()
    {
        return $this->morphedByMany('App\Post', 'taggable');
    }

    public function products()
    {
        return $this->morphedByMany('App\Product', 'taggable');
    }
}

1.5.3 Lấy dữ liệu trong quan hệ đa hình n-n

Khi các mối quan hệ đã được định nghĩa, việc xử lý dữ liệu trong các quan hệ đa hình n-n sẽ rất đơn giản. Ví dụ lấy tất cả các thẻ được gắn cho bài viết:

$post = App\Post::find(1);

foreach ($post->tags as $tag) {
    //
}

Hoặc lấy tất cả các sản phẩm được gắn một thẻ nào đó:

$tag = App\Tag::find(1);

foreach ($tag->products as $product) {
    echo $product->name;
}

2. Truy vấn dữ liệu trong Eloquent Relationship

Sau khi các dạng quan hệ được định nghĩa trong Eloquent thông qua các phương thức, bạn có thể gọi các phương thức để lấy dữ liệu về như các phương thức sử dụng trong query builder, nó cho phép bạn gọi các phương thức theo chuỗi trước khi thực thi câu lệnh SQL. Ví dụ, bạn muốn lấy về các bài viết của một user đã được đăng:

$user = App\User::find(1);

$user->posts()->where('active', 1)->get();

Khi truy xuất các bản ghi của một Model, bạn có thể loại bỏ bớt các kết quả dựa trên một quan hệ có trước, ví dụ bạn muốn lấy tất cả các bài viết có ít nhất một bình luận:

$posts = App\Post::has('comments')->get();

Bạn cũng có thể đưa vào các toán tử trong truy vấn

$posts = Post::has('comments', '>=', 3)->get();

Ngoài ra, Eloquent cung cấp các phương thức whereHas() và orWhereHas() cho phép thêm các điều kiện where vào trong truy vấn. Các phương thức này cũng cho phép bạn thêm các rằng buộc, ví dụ kiểm tra nội dung các bình luận:

$posts = Post::whereHas('comments', function ($query) {
    $query->where('content', 'like', 'foo%');
})->get();

Trái ngược với phương thức has(), whereHas(), orWhereHas() Eloquent cung cấp phương thức doesntHave(), whereDoesntHave()

$posts = App\Post::doesntHave('comments')->get();
$posts = Post::whereDoesntHave('comments', function ($query) {
    $query->where('content', 'like', 'foo%');
})->get();

Eloquent cung cấp thêm phương thức withCount() giúp đếm kết quả trả về và đặt vào một cột có tên mặc định là {relation}_count. Ví dụ:

$posts = App\Post::withCount('comments')->get();

foreach ($posts as $post) {
    echo $post->comments_count;
}

Thậm chí, có thể thực hiện đếm kết quả cho nhiều các quan hệ khác nhau:

$posts = Post::withCount(['votes', 'comments' => function ($query) {
    $query->where('content', 'like', 'foo%');
}])->get();

echo $posts[0]->votes_count;
echo $posts[0]->comments_count;

và có thể đặt tên cho các cột đếm kết quả trả về:

$posts = Post::withCount([
    'comments',
    'comments AS pending_comments' => function ($query) {
        $query->where('approved', false);
    }
])->get();

echo $posts[0]->comments_count;

echo $posts[0]->pending_comments_count;

3. Chèn và cập nhật các Model có quan hệ

Phương thức save()

Ví dụ bạn muốn thêm một comment cho một bài viết, thay vì thiết lập thủ công thuộc tính post_id trên Comment, bạn có thể thêm Comment trực tiếp từ phương thức save dựa trên quan hệ:

$comment = new App\Comment(['message' => 'A new comment.']);

$post = App\Post::find(1);

$post->comments()->save($comment);

saveMany() giúp thêm nhiều model một lúc:

$post = App\Post::find(1);

$post->comments()->saveMany([
    new App\Comment(['message' => 'A new comment.']),
    new App\Comment(['message' => 'Another comment.']),
]);

Phương thức create()

Giống như chức năng của save() nhưng đầu vào của create() là một mảng thay vì một instance của Model.

$post = App\Post::find(1);

$comment = $post->comments()->create([
    'message' => 'A new comment.',
]);

4. Xử lý tải dữ liệu từ cơ sở dữ liệu lên Model

4.1 Lazy load – phương thức mặc định tải dữ liệu lên Model

Khi chúng ta truy xuất vào mối quan hệ trong Eloquent Model như là một thuộc tính, dữ liệu cho mối quan hệ này chỉ được tải vào khi lần đầu tiên truy nhập vào thuộc tính, nó gọi là “lazy load”. Chúng ta cùng xem ví dụ sau để hiểu rõ hơn, trong ví dụ này Model Book có quan hệ với Model Author:

<?php
namespace App;
use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    /**
     * Lấy thông tin tác giả, người viết cuốn sách này
     */
    public function author()
    {
        return $this->belongsTo('App\Author');
    }
}

Để lấy thông tin tất cả các cuốn sách và các tác giả của chúng, sử dụng đoạn code sau:

$books = App\Book::all();

foreach ($books as $book) {
    echo $book->author->name;
}

Đoạn code trên hoạt động như sau, đầu tiên nó sẽ thực hiện một truy vấn lấy ra tất cả các cuốn sách, tiếp theo nó thực hiện một vòng lặp với mỗi cuốn sách nó sẽ truy xuất đến tác giả của cuốn sách. Với lazy load, nếu có 10 cuốn sách, đoạn code trên sẽ thực hiện 11 truy vấn bao gồm 1 truy vấn lấy tất cả thông tin về sách và 10 truy vấn lấy thông tin về tác giả.

4.2 Eager load, tải dữ liệu một lần

May thay, Laravel có một cách khác để tải dữ liệu lên Model gọi là eager load, với đoạn code trên khi viết lại sử dụng phương thức with(), nó chỉ thực hiện 2 truy vấn:

$books = App\Book::with('author')->get();

foreach ($books as $book) {
    echo $book->author->name;
}

nó thực hiện như sau ở database:

select * from books

select * from authors where id in (1, 2, 3, 4, 5, ...)

Mỗi phương thức tải có những ưu nhược điểm riêng tùy thuộc từng tình huống để sử dụng, với các truy vấn dữ liệu để thực hiện tính toán tổng hợp, dữ liệu cần được tải ngay một lần chúng ta nên sử dụng eager load, còn với các truy vấn dữ liệu thực hiện từng phần một thì lazy load là một lựa chọn không tồi.

4.3 Thêm ràng buộc truy vấn trong kiểu tải dữ liệu eager load

Đôi khi bạn muốn giới hạn các kết quả với một ràng buộc điều kiện nào đó khi sử dụng eager load, việc này hoàn toàn thực hiện được thông qua arrow function:

$books = App\Book::with(['author' => function ($query) {
    $query->where('age', '>', 40);
}])->get();

khi đó nó sẽ thực hiện các truy vấn sau dưới database:

4.4 Lazy eager load

Đây là kiểu tải dữ liệu lai tạp, ví dụ như bạn muốn thực hiện eager load sau khi đã thực hiện truy vấn Model cha:

$books = App\Book::all();

if ($someCondition) {
    $books->load('author', 'publisher');
}

5. Lời kết

Xử lý mối quan hệ trong cơ sở dữ liệu là kiến thức tương đối rối rắm, nếu như bạn chưa được học qua các kiến thức về chuẩn hóa cơ sở dữ liệu thì quả thật sẽ khó để tiếp cận. Laravel Eloquent Model xử lý không những rất tốt các dạng quan hệ có sẵn mà còn đưa vào rất nhiều các cải tiến giúp chúng ta có thể lựa chọn phù hợp với đặc thù xử lý dữ liệu riêng. Qua hai phần đầu của các bài viết về Laravel Eloquent ORM chúng ta đã phần nào mapping được giữa database và các đối tượng trong lập trình. Các phần tiếp theo, bạn sẽ được giới thiệu các cách thức xử lý thao tác với dữ liệu sau khi tải về từ database vào Model.

3 thoughts on “Laravel Eloquent ORM phần 2: Xử lý database relationship

  1. giữa with() và has() có cái gì đó giống nhau cùng liên quan đến quan hệ giữa các model, bạn nào có ví dụ giải thích cụ thể hơn vấn đề này không, mình thấy khó phân biệt quá?

  2. Amazing Laravel! Đây là phần mà những lập trình viên mới vào nghề hay vấp phải. Thay vì đưa các quan hệ vào trong Model để tiện dụng sau này thì mình toàn sử dụng query builder để viết các lệnh join, rất dài dòng mà không sử dụng lại được. Bài viết này thật tuyệt, nó giúp mình có cái nhìn khác về cách lập trình với Laravel.

Add Comment