Pivot tables and many-to-many relationships trong Laravel

Hôm nay chúng ta cùng tìm hiểu về Eloquent trong Laravel với mối quan hệ nhiều - nhiều (many to many relationship). Thoạt đầu, có thể sẽ hơi lạ và khó định nghĩa cụm từ pivot tables nhưng tính năng này rất hữu dụng trong việc build many-to-many relationship với Laravel framework. Pivot tables về cơ bản là một bảng trung gian giữa 2 bảng chính (main tables). Chúng ta sẽ cùng tìm hiểu cụ thể ở ví dụ thực tế dưới đây.

Example of pivot tables:

Ở trong documentation của Laravel, họ lấy ví dụ về mối quan hệ giữa UserRole, một user về cơ bản có nhiều role và ngược lại. Để dễ hiểu, mình sẽ lấy một ví dụ thực tế khác đó là , một phần yêu cầu của bài cuối kỳ môn học về webservices của mình, mối quan hệ giữa bài viết và các tags (article - tag relationship). Ví dụ như các bài viết trên viblo chẳng hạn, hay như bài viết này của mình sẽ có các tag như: Laravel, PHP, Eloquent mà trong tag Laravel thì có nhiều bài viết khác như về Trait, gulp,…

Đầu tiên, chúng ta sẽ tạo 2 bảng chính. Ở ví dụ này, chúng ta sẽ tạo 2 bảng articletag trước:

public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->increments('id');
            $table->string('title');
            $table->string('short_intro');
            $table->text('content');
            $table->string('slug')->nullable();
            $table->integer('category_id')->unsigned();
            $table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade');
            $table->integer('author_id')->unsigned();
            $table->foreign('author_id')->references('id')->on('authors')->onDelete('cascade');
            $table->timestamps();
        });
    }

tiếp theo là bảng tag:

public function up()
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('slug')->nullable();
            $table->timestamps();
        });
    }

Bước tiếp theo, chúng ta sẽ tạo pivot table dựa trên 2 bảng chính là article và tag. Ở đây chúng ta cần chú ý một chút:

  • Tên của pivot table bao gồm tên của 2 bảng chính (ở dạng số ít), ngăn cách nhau bởi dấu gạch dưới (underscore) và sắp xếp theo thứ tự bảng chữ cái (alphabetical).
  • Để tạo pivot table đơn gianr chúng ta có thể dùng
    php artisan make:migration

hoặc dùng Jeffrey Way’s package Laravel 5 Generators Extended Mặc định trong pivot table sẽ chỉ có 2 trường article_id và tag_id. Chúng ta sẽ nói thêm về việc customize sao nếu như muốn add thêm trường vào pivot table. Do vậy ở ví dụ này pivot table của chúng ta sẽ có tên là article_tag

Model

Ở phần trên, chúng ta mới chỉ tạo ra migration. Gio chúng ta sẽ tạo model và mối quan hệ many to many cho 2 bảng qua pivot table. Việc này có thể làm ở một trong hai bảng chính. Tất nhiên là bạn có thể viết ở cả 2 bảng (nhưng thường thì k ai làm như vậy cả). Ở ví dụ này, mình sẽ viết ở tag model như sau:

class Tag extends Model
{
    //
  protected $table = 'tags';
  protected $fillable = ['name'];

  public $timestamps = true;

  public function articles() {
    return $this->belongsToMany('App\Article', 'article_tag', 'article_id', 'tag_id');
  }
}

Như mọi người thấy, ở đây mình chỉ ra rõ 2 trường của pivot table là article_id và tag_id. Lợi ích: Chúng ta sẽ chẳng cần tạo thêm một model là article_tag làm gì cả. Chúng ta sẽ quản lý nó dựa trên commands.

Managing Many-to-Many Relationships: attach-detach-sync

Đã có bảng và model rồi. Chúng ta chỉ việc xử lý việc lưu dữ liệu dựa trên pivot table.

Ví dụ chúng ta muốn add thêm 1 tag cho 1 bài viết cụ thể, chúng ta sử dụng method attach():

$article = Article::find($article_id);
$article->tags()->attach($tag_id);

Và kết qủa thu được là gía trị của article_id và tag_id được thêm vào bảng article_tag.

Cũng tương tự vậy, nếu như chúng ta muốn gỡ bỏ 1 tag từ article thì chúng ta sử dụng method detach():

$article->tags()->detach($tag_id);

hoặc nếu như bạn muốn gỡ bỏ toàn bộ tag trong article đó- easy :

$article->tags()->detach();

Chúng ta cũng có thể attach() và detach() rows bằng việc truyền vào một tham biến (params) như một array:

$article->tags()->attach([123, 456, 789]);
$article->tags()->detach([123, 456, 789]);

Một function khác cũng hữu ích đó là update toàn bộ pivot table: sync()

Method này chỉ accept một mảng ID truyền vào trong pivot table. Do vậy, tất cả ID không nằm trong mảng ID đó sẽ bị loại bỏ khỏi pivot table và trong pivot table lúc này sẽ chỉ còn lại ID nằm trong mảng truyền vào:

$article->tags()->sync([1, 2, 3]);

Nếu bạn không muốn detach những ID tồn tại trong đó, bạn nên dùng method syncWithoutDetaching :

$article->tags()->syncWithoutDetaching([1, 2, 3]);

Additional Columns in Pivot Tables

Như đã đề cập ở trên, chúng ta sẽ làm gì nếu như muốn thêm 1 trường và pivot table? ĐƠn giản là chúng ta sẽ dùng hàm withPivot() để thêm vào pivot table (mình sẽ lấy 1 ví dụ khác):

public function skills() {
    return $this->belongsToMany('App\Skill',  'skill_user', 'user_id', 'skill_id')->withPivot('level');
}

ở ví dụ này, mình giả sử mình có 2 bảng chính đó là userskill với 1 pivot table là skill_user. Mối quan hệ là many to many, một user có thể có nhiều skill như HTML, CSS, PHP,... và tương tự skill cũng vậy. Trong pivot table skill_user mình muốn thêm một trường đó là level. Thuộc tính này dùng để define level của user về skill nào đó. Ví dụ như user1 có skill PHP ở mức độ là intermediate và Java level là talented. Bạn cũng có thể thêm timestamps vào bằng việc gọi đến method withTimestamps().

Bây giờ, chúng ta có thể gọi value level với thuộc tính đó là pivot:

@foreach ($member->skills as $skill)
    <div class="col-md-3">
        <div class="circular-bar">
            <div class="circular-bar-chart" data-percent="{{$skill->pivot->level}}" data-plugin-options='{"barColor": "{{rand_color()}}", "delay":300}'>
                <strong>{{$skill->name}}</strong>
                <label><span class="percent">{{$skill->pivot->level}}</span>%</label>
            </div>
        </div>
    </div>
 @endforeach

Updating A Record On A Pivot Table

Nếu như bạn muốn update 1 row đã có trong pivot table, bạn có thể sử dụng updateExistingPivot method.

$article->tags()->updateExistingPivot($tag_id, $attributes);

Conclusion

Pivot table là một technique rất tiện trong việc xây dựng eloquent và mối quan hệ many to many, chúng ta không cần thiết phải tạo một model riêng cho pivot table. Cảm ơn mọi người đã quan tâm !

References

Pivot tables and many-to-many relationships

Eloquent: Relationships