NTM Solutions

Bài đăng nổi bật

🚀 Laravel 12 (2026) — Từ PHP Core đến Full Stack📘

Nếu cập nhật theo Laravel 12 LTS (2026) thì mình sẽ chia thành các phần như sau để người học đi từ PHP Core → Laravel → Triển khai dự án th...

Thứ Ba, 21 tháng 7, 2026

🚀 Laravel 12 (2026) — Bài 15 — Relationship Many To Many

Ở bài trước, chúng ta đã tìm hiểu quan hệ One To Many, trong đó một bản ghi có thể liên kết với nhiều bản ghi khác.

Tuy nhiên, có những trường hợp cả hai phía đều có thể liên kết với nhiều bản ghi của nhau. Ví dụ:

  • Một bài viết có nhiều thẻ (Tag).

  • Một thẻ có thể xuất hiện trong nhiều bài viết.

  • Một sinh viên học nhiều môn học.

  • Một môn học có nhiều sinh viên.

Đó chính là quan hệ Many To Many.

Laravel hỗ trợ kiểu quan hệ này thông qua Pivot Table (bảng trung gian).


Many To Many là gì?

Many To Many là quan hệ nhiều - nhiều.

Ví dụ:

Post
 │
 ├── Laravel
 ├── PHP
 └── MySQL

Tag
 │
 ├── Web
 ├── Backend
 ├── Database
 └── Tutorial

Một bài viết có thể mang nhiều Tag.

Một Tag cũng có thể thuộc nhiều bài viết.

Bước 1. Tạo Model + Migration

php artisan make:model Tag -m

Lệnh này tạo:

app/Models/Tag.php
database/migrations/xxxx_create_tags_table.php

abc


Bước 2. Migration bảng tags

Schema::create('tags', function (Blueprint $table) {
    $table->id();
    $table->string('name')->unique();
    $table->timestamps();
});

Vì sao cần Pivot Table?

Nếu thêm trực tiếp tag_id vào bảng posts thì mỗi bài viết chỉ có một Tag.

Nếu thêm post_id vào bảng tags thì mỗi Tag cũng chỉ thuộc một bài viết.

Điều này không đáp ứng được quan hệ nhiều - nhiều.

Giải pháp là tạo một bảng thứ ba.

posts

id
title

tags

id
name

post_tag

post_id
tag_id

Bảng post_tag chỉ lưu mối liên kết giữa hai bảng.

Đây được gọi là Pivot Table.


Migration

Tạo bảng trung gian:

php artisan make:migration create_post_tag_table

Migration:

Schema::create('post_tag', function (Blueprint $table) {

    $table->foreignId('post_id')
          ->constrained()
          ->cascadeOnDelete();

    $table->foreignId('tag_id')
          ->constrained()
          ->cascadeOnDelete();

});

Thông thường bảng Pivot không cần cột id.


belongsToMany()

Trong model Post:

class Post extends Model
{
    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }
}

Ý nghĩa:

Một Post có nhiều Tag.


Trong model Tag:

class Tag extends Model
{
    protected $fillable = [
        'name',
    ];

    public function posts() { return $this->belongsToMany(Post::class); } }

Ý nghĩa:

Một Tag thuộc nhiều Post.

Laravel sẽ tự tìm bảng post_tag.

Tạo TagSeeder

database/seeders/TagSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\Tag;

class TagSeeder extends Seeder
{
    public function run(): void
    {
        Tag::create([
            'name' => 'Laravel'
        ]);

        Tag::create([
            'name' => 'PHP'
        ]);

        Tag::create([
            'name' => 'MySQL'
        ]);
    }
}

Tạo PostTagSeeder

database/seeders/PostTagSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\Post;

class PostTagSeeder extends Seeder
{
    public function run(): void
    {
        $post = Post::find(1);

        if ($post) {

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

        }
    }
}

👉 Dùng sync() thay vì attach() để chạy seed nhiều lần cũng không bị trùng dữ liệu.


DatabaseSeeder

public function run(): void
{
    //phải chạy sau post và tag để tránh lỗi empty data
    $this->call([ TagSeeder::class, PostTagSeeder::class, ]); }

Chạy

php artisan db:seed

Sau đó:


Lấy danh sách Tag

/* PHẦN THÊM VÀO routes/web.php*/
use App\Models\Post;
use App\Models\Tag;

Route::get('/test', function () {

    $post = Post::first();

    if (!$post) {
        return 'Chưa có dữ liệu Post.';
    }

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

        echo $tag->name . "<br>";

    }

});

Laravel sẽ tự JOIN bảng post_tag.


Lấy danh sách Post

Ngược lại:

$tag = Tag::first();

if (!$tag) {
    return 'Chưa có dữ liệu Tag.';
}

foreach ($tag->posts as $post) {

    echo $post->title . "<br>";

}

attach()

Thêm liên kết:

$post = Post::first();

if (!$post) {
    return 'Chưa có dữ liệu Post.';
}

$post->tags()->attach(3);

Laravel sẽ thêm:

post_id = 1
tag_id = 3

vào bảng post_tag.


Có thể thêm nhiều Tag:

$post->tags()->attach([
    1,
    2,
    3
]);

detach()

Xóa liên kết:

$post->tags()->detach(3);

Hoặc xóa tất cả:

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

Chỉ dữ liệu trong bảng Pivot bị xóa.

Bảng poststags vẫn giữ nguyên.


sync()

Đây là phương thức được sử dụng nhiều nhất.

$post->tags()->sync([
    1,
    2,
    5
]);

Laravel sẽ:

  • thêm Tag mới,

  • xóa Tag cũ không còn,

  • giữ lại Tag đã tồn tại.

Kết quả là bài viết chỉ còn đúng ba Tag trên.


eager loading

Có thể tải trước:

$posts = Post::with('tags')->get();

Sau đó:

foreach ($posts as $post) {

    echo $post->title;

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

        echo $tag->name;

    }

}

Điều này giúp tránh hiện tượng N+1 Query.


Pivot Table có thêm dữ liệu

Đôi khi bảng trung gian không chỉ lưu khóa ngoại.

Ví dụ:

post_tag

post_id
tag_id
created_at
created_by

Hoặc:

course_student

student_id
course_id
score
joined_at

Laravel vẫn hỗ trợ rất tốt kiểu dữ liệu này.

Chúng ta sẽ tìm hiểu chi tiết ở các bài nâng cao.


Tổng kết

Trong bài này chúng ta đã học:

  • Quan hệ Many To Many.

  • Pivot Table.

  • belongsToMany().

  • attach().

  • detach().

  • sync().

  • with() để tối ưu truy vấn.

Many To Many được sử dụng rất nhiều trong các hệ thống Blog, CMS, E-commerce và các ứng dụng quản lý.


Bài tập

  1. Tạo model Tag.

  2. Tạo bảng post_tag.

  3. Khai báo belongsToMany() trong model Post.

  4. Khai báo belongsToMany() trong model Tag.

  5. Thêm ba Tag cho một Post bằng attach().

  6. Thay đổi danh sách Tag của Post bằng sync().

  7. Hiển thị tất cả Tag của từng Post bằng with('tags').


Bài tiếp theo

Ở bài tiếp theo, chúng ta sẽ tìm hiểu Has Many Through, một kiểu Relationship giúp truy cập dữ liệu gián tiếp thông qua một Model trung gian mà không cần tự viết câu lệnh JOIN.

x0

quay về MỤC LỤC

Không có nhận xét nào:

Đăng nhận xét

Facebook Youtube RSS