Devfavor

Back

การใช้งาน Eloquent Relationships - เพื่อดึงข้อมูลใน Laravel อย่างมีประสิทธิภาพBlur image

🤔 ทำไม Relationships ถึงสำคัญ#

ในการเขียนโปรแกรมด้วย Laravel เราสามารถดึงข้อมูลจากฐานข้อมูลได้หลากหลายวิธีมาก ไม่ว่าจะเป็นการเรียกผ่าน Facade DB ตรง ๆ หรือการใช้ Model ช่วย Query ก็ตาม แต่อีก 1 วิธีที่เราสามารถใช้เรียกข้อมูลได้เหมือนกันก็คือการดึงผ่าน Eloquent Relationships นั่นเอง

การดึงข้อมูลด้วย Eloquent Relationships นั้นจะเป็นการดึงข้อมูลที่มีความสัมพันธ์ซึ่งกันและกัน โดยมีข้อดีคือ เราไม่ต้องคอยเขียนโค้ดเพื่อบริหารจัดการความสัมพันธ์ทุกครั้งที่เรียกใช้ ขอเพียงเราสร้างและประกาศความสัมพันธ์ไว้ก่อน เวลาเรียกใช้ก็เพียงแค่ดึงผ่านตัว Model เท่านั้น ทำให้การเขียนโค้ดของเรามีประสิทธิภาพมากยิ่งขึ้น นอกจากนี้ การดึงข้อมูลในรูปแบบของ Relationships ยังมีส่วนช่วยในการมองภาพของระบบและทำความเข้าใจกับ Business Logic ของ Model นั้น ๆ ได้ดียิ่งขึ้นอีกด้วย

💻 พื้นฐาน Eloquent Relationships#

การจะดีไซน์ Eloquent Relationships นอกจากจะต้องรู้พื้นฐานของภาษา PHP และ Laravel แล้ว ยังต้องเข้าใจความสัมพันธ์ในเชิงของฐานข้อมูลควบคู่กันไปด้วย เพื่อให้การดีไซน์ Relationships เกิดประสิทธิภาพสูงสุดและมีความถูกต้องในเชิง Logic ก่อนลงรายละเอียด ขอปูพื้นฐานคำศัพท์บางคำที่จำเป็นต่อการทำความเข้าใจไว้ก่อน

  • Pivot Table - คือตารางกลาง ที่ใช้สำหรับเชื่อมความสัมพันธ์แบบ Many-to-Many โดยจะเก็บ foreign key ของทั้ง 2 Model ไว้ใช้สำหรับดึงข้อมูล
  • Eager Loading - คือการดึงข้อมูลจากความสัมพันธ์พร้อมกันล่วงหน้า เพื่อป้องกันการ Query ที่เยอะเกินจำเป็น นอกจากนี้ยังช่วยป้องกันปัญหา N+1 อีกด้วย (ไว้ในโอกาสหน้าจะมาเล่าเกี่ยวกับปัญหา N+1 อีกที) ตัวอย่างเช่น User::with('posts')->get(); คือการ eager load ตั้งแต่ตอน Query ส่วน $user->load('roles'); คือ lazy eager load ที่โหลดทีหลังจากดึง Model มาแล้ว ถ้าไม่ใช้ with แล้ววน loop เรียก $user->posts ทีละตัว จะเกิดปัญหา N+1 ทันที
  • Polymorphic - หรือ Polymorphism หมายถึง การมีหลายรูปแบบ ซึ่งในกรณีของ Eloquent Relationships นี้คำว่า Polymorphic มีความหมายเฉพาะตัวคือ การที่ Model หนึ่งสามารถมีความสัมพันธ์แบบ One-to-Many กับหลาย Model ได้ (หลายรูปแบบ) ผ่านคู่คอลัมน์ *_id + *_type ชุดเดียว โดยไม่ต้องสร้าง foreign key แยกสำหรับแต่ละ Model ปลายทาง

ในความเป็นจริงแล้วคำศัพท์เหล่านี้มีรายละเอียดเฉพาะตัวอีกมาก แต่ในที่นี้ขอให้เข้าใจพื้นฐานโดยคร่าวก่อน ก็เพียงพอที่จะดีไซน์ความสัมพันธ์ได้อย่างมีประสิทธิภาพแล้ว

📝 ประเภทของ Eloquent Relationships ใน Laravel#

ใน Laravel เราจะพบกับความสัมพันธ์ และ Eloquent Relationships ของ Model ดังต่อไปนี้

One-to-One (hasOne / belongsTo)#

เป็นความสัมพันธ์แบบที่ Model หนึ่งในตาราง A เชื่อมโยงกับ Model เดียวในตาราง B

One-to-Many (hasMany / belongsTo)#

เป็นความสัมพันธ์แบบที่ Model หนึ่งในตาราง A เชื่อมโยงกับหลาย Model (หนึ่ง Model ขึ้นไป) ในตาราง B

Many-to-Many (belongsToMany)#

เป็นความสัมพันธ์แบบที่ Model ในตาราง A สามารถเชื่อมโยงกับ Model ในตาราง B ได้มากกว่า 1 Model โดยผ่านตารางกลางที่เป็น Pivot Table

Has One Through (hasOneThrough)#

เป็นความสัมพันธ์แบบที่ Model ในตาราง A สามารถเชื่อมโยงกับ Model ในตาราง C ได้ 1 Model ผ่านตัวกลางที่เป็นตาราง B

Has Many Through (hasManyThrough)#

เป็นความสัมพันธ์แบบที่ Model ในตาราง A สามารถเชื่อมโยงกับ Model ในตาราง C ได้มากกว่า 1 Model ผ่านตัวกลางที่เป็นตาราง B

Polymorphic One-to-One (morphOne)#

เป็นความสัมพันธ์แบบที่ Model หนึ่งในตาราง A เชื่อมโยงกับ Model เดียวในตาราง B แบบ Polymorphic

Polymorphic One-to-Many (morphMany)#

เป็นความสัมพันธ์แบบที่ Model หนึ่งในตาราง A เชื่อมโยงกับ Model ในตาราง B มากกว่า 1 Model แบบ Polymorphic

Polymorphic Many-to-Many (morphToMany)#

เป็นความสัมพันธ์แบบที่ Model ในตาราง A สามารถเชื่อมโยงกับ Model ในตาราง B ได้มากกว่า 1 Model แบบ Polymorphic

จะสังเกตว่าจริง ๆ แล้วรูปแบบความสัมพันธ์ของ Eloquent Relationships จะเหมือนกับความสัมพันธ์ของฐานข้อมูลเลย เพียงแค่ตัว Laravel นั้นจะเพิ่มฟังก์ชันการใช้งานให้เรา เพื่อให้เราเรียกใช้งานได้สะดวกขึ้นเท่านั้นเอง

👩‍💻 ตัวอย่างฐานข้อมูล เตรียม Migration และ Model ให้พร้อม#

พอเห็นภาพแล้วว่า Eloquent Relationships มีประโยชน์อย่างไร ก็ได้เวลาลองดูโค้ดจริง ๆ กันเลย

ก่อนอื่นให้เตรียมฐานข้อมูลสำหรับใช้ในการ Query ให้พร้อมโดยการพิมพ์คำสั่งดังนี้

php artisan make:migration create_countries_table
php artisan make:migration create_users_table
php artisan make:migration create_profiles_table
php artisan make:migration create_roles_table
php artisan make:migration create_role_user_table
php artisan make:migration create_posts_table
php artisan make:migration create_comments_table
php artisan make:migration create_videos_table
php artisan make:migration create_photos_table
php artisan make:migration create_tags_table
php artisan make:migration create_taggables_table
bash

หลังจากสร้างไฟล์ Migration แล้วให้จัดการ fields ของแต่ละตารางดังนี้

create_countries_table

public function up(): void {
    Schema::create('countries', function (Blueprint $table) {
        $table->id();
        $table->string('name')->unique();
        $table->string('iso2', 2)->unique();
        $table->timestamps();
    });
}
php

create_users_table

public function up(): void {
    Schema::create('users', function (Blueprint $table) {
        $table->id();
        $table->foreignId('country_id')->nullable()->constrained()->nullOnDelete();
        $table->string('name');
        $table->string('email')->unique();
        $table->timestamp('email_verified_at')->nullable();
        $table->string('password');
        $table->rememberToken();
        $table->timestamps();

        $table->index('country_id');
    });
}
php

create_profiles_table

public function up(): void {
    Schema::create('profiles', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete();
        $table->text('bio')->nullable();
        $table->string('phone')->nullable();
        $table->timestamps();
    });
}
php

create_roles_table

public function up(): void {
    Schema::create('roles', function (Blueprint $table) {
        $table->id();
        $table->string('name')->unique();
        $table->string('label')->nullable();
        $table->timestamps();
    });
}
php

create_role_user_table

public function up(): void {
    Schema::create('role_user', function (Blueprint $table) {
        $table->foreignId('role_id')->constrained()->cascadeOnDelete();
        $table->foreignId('user_id')->constrained()->cascadeOnDelete();
        $table->foreignId('assigned_by')->nullable()->constrained('users')->nullOnDelete();
        $table->timestamp('expires_at')->nullable();
        $table->timestamps();

        $table->primary(['role_id', 'user_id']);
    });
}
php

create_posts_table

public function up(): void {
    Schema::create('posts', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->cascadeOnDelete();
        $table->string('title');
        $table->text('body')->nullable();
        $table->enum('status', ['draft','published'])->default('draft');
        $table->timestamps();

        $table->index('user_id');
        $table->index('status');
    });
}
php

create_comments_table

public function up(): void {
    Schema::create('comments', function (Blueprint $table) {
        $table->id();
        $table->foreignId('post_id')->constrained()->cascadeOnDelete();
        $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
        $table->text('content');
        $table->timestamps();

        $table->index(['post_id', 'user_id']);
    });
}
php

create_videos_table

public function up(): void {
    Schema::create('videos', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
        $table->string('title');
        $table->string('url');
        $table->timestamps();

        $table->index('user_id');
    });
}
php

create_photos_table

public function up(): void {
    Schema::create('photos', function (Blueprint $table) {
        $table->id();
        $table->morphs('imageable'); // imageable_type, imageable_id (indexed)
        $table->string('path');
        $table->string('alt')->nullable();
        $table->timestamps();
    });
}
php

create_tags_table

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

create_taggables_table

public function up(): void {
    Schema::create('taggables', function (Blueprint $table) {
        $table->foreignId('tag_id')->constrained()->cascadeOnDelete();
        $table->morphs('taggable'); // taggable_type, taggable_id
        $table->foreignId('added_by')->nullable()->constrained('users')->nullOnDelete();
        $table->timestamps();

        $table->index(['tag_id', 'taggable_type', 'taggable_id'], 'taggables_full_index');
    });
}
php

จากนั้นสั่ง

php artisan migrate
bash

เป็นอันเรียบร้อยสำหรับการเตรียมฐานข้อมูล จากนั้นให้เตรียม Model ต่อ โดยการพิมพ์คำสั่งดังนี้

php artisan make:model Country
php artisan make:model User
php artisan make:model Profile
php artisan make:model Role
php artisan make:model Post
php artisan make:model Comment
php artisan make:model Video
php artisan make:model Photo
php artisan make:model Tag
bash

จากนั้นแก้ไขไฟล์ของ Model แต่ละไฟล์ดังนี้

Country.php

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

    public function users() {
        return $this->hasMany(User::class);
    }

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

    public function latestPost() {
        return $this->hasManyThrough(Post::class, User::class)->one()->latestOfMany();
    }
}
php

User.php

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    public function country() { return $this->belongsTo(Country::class); }

    public function profile() { return $this->hasOne(Profile::class); }

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

    public function videos() { return $this->hasMany(Video::class); }

    public function roles() { return $this->belongsToMany(Role::class)->withTimestamps()->withPivot(['assigned_by', 'expires_at']); }

    public function photo() { return $this->morphOne(Photo::class, 'imageable'); }
}
php

Profile.php

class Profile extends Model
{
    protected $fillable = ['user_id', 'bio', 'phone'];

    public function user() {
        return $this->belongsTo(User::class);
    }
}
php

Role.php

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

    public function users() {
        return $this->belongsToMany(User::class)->withTimestamps();
    }
}
php

Post.php

class Post extends Model
{
    protected $fillable = ['user_id','title','body','status'];

    public function user() { return $this->belongsTo(User::class); }

    public function comments() { return $this->hasMany(Comment::class); }

    public function photos() { return $this->morphMany(Photo::class, 'imageable'); }

    public function tags() { return $this->morphToMany(Tag::class, 'taggable')->withPivot(['added_by']); }
}
php

Comment.php

class Comment extends Model
{
    protected $fillable = ['post_id','user_id','content'];

    public function post() { return $this->belongsTo(Post::class); }
    public function user() { return $this->belongsTo(User::class); }
}
php

Video.php

class Video extends Model
{
    protected $fillable = ['user_id','title','url'];

    public function user() { return $this->belongsTo(User::class); }

    public function tags() { return $this->morphToMany(Tag::class, 'taggable')->withPivot(['added_by']); }
}
php

Photo.php

class Photo extends Model
{
    protected $fillable = ['path','alt'];

    public function imageable() {
        return $this->morphTo();
    }
}
php

Tag.php

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

    public function posts() { return $this->morphedByMany(Post::class, 'taggable'); }

    public function videos() { return $this->morphedByMany(Video::class, 'taggable'); }
}
php

หลังจากเพิ่ม Model เสร็จเรียบร้อย มาลองดูตัวอย่างการดึงข้อมูลแบบต่าง ๆ ได้เลย

🛠️ ตัวอย่างการดึงและอัปเดตข้อมูลแบบต่าง ๆ ผ่าน Eloquent Relationships#

หลังจากที่เตรียมไฟล์ Model เสร็จเรียบร้อย ก็ได้เวลาลองใช้งาน Eloquent Relationships โดยจะแบ่งเป็นประเภทดังนี้

ตัวอย่าง One-to-One (hasOne / belongsTo)#

ตัวอย่างการดึงข้อมูลแบบ One-to-One ผ่านฟังก์ชัน hasOne จะเป็นการเรียกข้อมูล Profile ของ User คนนั้น ๆ

โดยที่หากเราต้องการแก้ไขข้อมูล Profile ของ User เราสามารถทำได้ง่าย ๆ ดังนี้

// Read
$profile = $user->profile;
$owner   = $profile->user;

// Create
$user->profile()->create([
  'bio' => 'I love Laravel',
  'phone' => '0812345678',
]);

// Update
$user->profile->update(['phone' => '0888888888']);
php

ตัวอย่าง One-to-Many (hasMany / belongsTo)#

สำหรับในตัวอย่างนี้ เราจะมาดูที่ความสัมพันธ์ของ User และ Post โดยที่ User 1 คน สามารถเขียนได้หลาย Post

สามารถใช้ Eloquent Relationships เรียกข้อมูลได้ดังนี้

// Read
$posts = $user->posts;
$owner = $post->user;

// Create
$user->posts()->createMany([
  ['title' => 'A', 'status' => 'draft'],
  ['title' => 'B', 'status' => 'published'],
]);

// Update
$user->posts()->where('status', 'draft')->update(['status' => 'published']);
php

ตัวอย่าง Many-to-Many (belongsToMany)#

ตัวอย่าง Many-to-Many เราจะใช้ความสัมพันธ์ระหว่าง User และ Role โดยการดึงข้อมูลผ่านตาราง Pivot ชื่อ role_user โดยสามารถดึงข้อมูลได้โดยใช้ฟังก์ชันดังนี้

// Read
$roles = $user->roles;

// Create
$user->roles()->attach($roleId, [
  'assigned_by' => auth()->id(),
  'expires_at'  => now()->addMonth(),
]);

// Update
$user->roles()->updateExistingPivot($roleId, [
  'expires_at' => now()->addMonths(3),
]);
php

ตัวอย่าง Has One/Many Through (hasOneThrough / hasManyThrough)#

ในตัวอย่างนี้ เราจะลองดึงข้อมูลของ Post ภายใต้ Country ผ่านทาง Model ตัวกลางคือ User ดังตัวอย่างต่อไปนี้

// Read
$posts = $country->posts;
$latest = $country->latestPost;

// Create
$user = $country->users()->firstOrFail();
$user->posts()->create(['title' => 'From Country', 'status' => 'published']);

// Update
$country->posts()->where('status', 'draft')->update(['status' => 'published']);
php

ตัวอย่าง Polymorphic One-to-One / One-to-Many (morphOne / morphMany)#

การดึงข้อมูลแบบ Polymorphic ในตัวอย่างนี้ จะใช้ Model Photo โดยที่ ทั้ง User และ Post สามารถมี Photo ได้ทั้งคู่

แต่เราจะใช้คุณสมบัติของ Polymorphic ในการจัดการ Photo แยกออกจากกัน โดยที่ User จะมี 1 Photo และ Post มีมากกว่า 1 Photo

// Read
$photo = $user->photo;
$photos = $post->photos;

// Create
$user->photo()->create([
  'path' => 'avatars/u1.jpg',
  'alt'  => 'User avatar',
]);

$post->photos()->createMany([
  ['path' => 'posts/1/cover.jpg', 'alt' => 'Cover'],
  ['path' => 'posts/1/detail.jpg', 'alt' => 'Detail'],
]);

// Update
$photo->update(['alt' => 'Main Cover']);

$post->photos()->whereNull('alt')->update(['alt' => 'N/A']);
php

ตัวอย่าง Polymorphic Many-to-Many (morphToMany / morphedByMany)#

สำหรับตัวอย่างสุดท้ายจะเป็นการใช้ Polymorphic แบบ Many-to-Many กับตาราง Tag โดยที่ทั้ง Post และ Video สามารถมีได้หลาย Tag สามารถเขียนโค้ดได้ดังนี้

// Read
$postTags = $post->tags;
$videoTags = $video->tags;

$postsOfTag  = $tag->posts;
$videosOfTag = $tag->videos;

// Create
$post->tags()->attach($tagId);
$post->tags()->attach([1 => ['added_by' => auth()->id()], 3 => []]);

// Update
$post->tags()->updateExistingPivot($tagId, ['added_by' => auth()->id()]);
php

🔍 ตรวจสอบการมีอยู่ของความสัมพันธ์ (Querying Relationship Existence)#

นอกจากการดึงข้อมูลความสัมพันธ์ออกมาตรง ๆ แล้ว ในงานจริงเรามักจะเจอโจทย์ประเภท “ดึงเฉพาะ Model ที่มี (หรือไม่มี) ความสัมพันธ์ตามเงื่อนไข” อยู่บ่อยมาก ซึ่ง Eloquent เตรียมฟังก์ชันสำหรับกรณีนี้ไว้ให้แล้ว โดยไม่ต้องเขียน subquery หรือ join เองให้ปวดหัว

has() คือฟังก์ชันที่ใช้กรองเฉพาะ Model ที่มีความสัมพันธ์อยู่อย่างน้อย 1 รายการ และยังกำหนดจำนวนขั้นต่ำได้ด้วย

// ดึงเฉพาะ user ที่มี post อย่างน้อย 1 รายการ
$users = User::has('posts')->get();

// ดึงเฉพาะ user ที่มี post ตั้งแต่ 3 รายการขึ้นไป
$users = User::has('posts', '>=', 3)->get();
php

ส่วน whereHas() จะคล้ายกับ has() แต่เพิ่มความสามารถในการใส่เงื่อนไขเข้าไปในความสัมพันธ์ผ่าน closure ได้ เหมาะกับโจทย์ที่ซับซ้อนขึ้น

use Illuminate\Database\Eloquent\Builder;

// ดึงเฉพาะ user ที่มี post ซึ่งเผยแพร่แล้ว (status = published)
$users = User::whereHas('posts', function (Builder $query) {
    $query->where('status', 'published');
})->get();

// ใส่เงื่อนไขจำนวนได้ด้วย เช่น user ที่มี post เผยแพร่แล้วตั้งแต่ 5 รายการขึ้นไป
$users = User::whereHas('posts', function (Builder $query) {
    $query->where('status', 'published');
}, '>=', 5)->get();
php

ในทางกลับกัน หากต้องการดึง Model ที่ “ไม่มี” ความสัมพันธ์ ก็มี doesntHave() และ whereDoesntHave() ให้ใช้คู่กัน

use Illuminate\Database\Eloquent\Builder;

// ดึงเฉพาะ user ที่ยังไม่มี post เลย
$users = User::doesntHave('posts')->get();

// ดึงเฉพาะ user ที่ไม่มี post ซึ่งเผยแพร่แล้ว (อาจมีแต่ draft ก็ได้)
$users = User::whereDoesntHave('posts', function (Builder $query) {
    $query->where('status', 'published');
})->get();
php

🔢 นับความสัมพันธ์ด้วย withCount() และจัดการปัญหา N+1#

ในหัวข้อพื้นฐานเราได้เกริ่นเรื่อง Eager Loading และปัญหา N+1 ไว้แล้ว ในส่วนนี้จะมาขยายให้เห็นภาพแบบรูปธรรมพร้อมตัวอย่างที่จับต้องได้

สมมติว่าเราต้องการแสดงรายชื่อ user พร้อมกับ post ของแต่ละคน หากเขียนแบบ lazy load โดยไม่ใช้ with จะเกิดอะไรขึ้น

// ❌ เกิดปัญหา N+1
$users = User::all();           // 1 query สำหรับดึง user ทั้งหมด

foreach ($users as $user) {
    echo $user->posts->count(); // ยิงเพิ่มอีก 1 query ต่อ user 1 คน
}
php

หากมี user 100 คน โค้ดข้างบนจะยิง query รวม 101 ครั้ง (1 ครั้งสำหรับดึง user + อีก 100 ครั้งสำหรับ post ของแต่ละคน) นี่คือที่มาของชื่อ N+1 นั่นเอง

ทางแก้คือใช้ Eager Loading ด้วย with() เพื่อให้ Eloquent ดึง post ของทุก user มาในครั้งเดียว

// ✅ แก้ปัญหา N+1 ด้วย Eager Loading
$users = User::with('posts')->get(); // เหลือเพียง 2 query เท่านั้น

foreach ($users as $user) {
    echo $user->posts->count();
}
php

แบบนี้จะเหลือเพียง 2 query (1 ครั้งสำหรับ user + อีก 1 ครั้งสำหรับ post ของทุกคนรวมกัน) ไม่ว่าจะมี user กี่คนก็ตาม

แต่ถ้าจุดประสงค์ของเราคือต้องการแค่ “จำนวน” ของความสัมพันธ์ ไม่ได้ต้องการตัวข้อมูล post จริง ๆ การดึง post ทั้งหมดมานับถือว่าสิ้นเปลือง กรณีนี้ควรใช้ withCount() แทน ซึ่งจะให้ค่ามาเป็น attribute ในรูปแบบ {relation}_count

// ดึง user พร้อมจำนวน post ของแต่ละคน
$users = User::withCount('posts')->get();

foreach ($users as $user) {
    echo $user->posts_count; // เข้าถึงผ่าน attribute ชื่อ posts_count
}
php

นอกจากนี้ยังใส่เงื่อนไขและตั้งชื่อ alias ให้ count แต่ละชุดได้ รวมถึงมีฟังก์ชัน aggregate ตัวอื่น เช่น withSum(), withMax(), withMin(), withAvg() ให้เลือกใช้

use Illuminate\Database\Eloquent\Builder;

// นับ post ทั้งหมด และนับเฉพาะ post ที่เผยแพร่แล้วแยกอีกชุด พร้อมตั้งชื่อ alias
$users = User::withCount([
    'posts',
    'posts as published_posts_count' => function (Builder $query) {
        $query->where('status', 'published');
    },
])->get();

echo $users[0]->posts_count;
echo $users[0]->published_posts_count;
php

🔗 จัดการ Pivot ของ Many-to-Many ให้ครบ (sync / detach + อ่าน pivot data)#

ในตัวอย่าง Many-to-Many ก่อนหน้านี้เราใช้ attach() กับ updateExistingPivot() ไปแล้ว แต่ในการใช้งานจริงยังมีอีกหลายฟังก์ชันที่ช่วยจัดการความสัมพันธ์ผ่าน Pivot Table ได้สะดวกขึ้น

detach() ใช้สำหรับถอดความสัมพันธ์ออก ซึ่งจะลบเฉพาะ record ในตาราง Pivot ไม่ได้ไปลบตัว Role จริง

// ถอด role เดียวออกจาก user
$user->roles()->detach($roleId);

// ถอดหลาย role พร้อมกัน
$user->roles()->detach([1, 2, 3]);

// ถอด role ทั้งหมดออก (ไม่ใส่ argument)
$user->roles()->detach();
php

sync() ใช้สำหรับกำหนดชุดความสัมพันธ์ใหม่ทั้งหมด โดย id ที่ไม่ได้อยู่ใน array จะถูก detach ออกให้อัตโนมัติ เหมาะกับฟอร์มแก้ไขที่ติ๊กเลือก role ได้หลายตัว

// หลังจากนี้ user จะเหลือเฉพาะ role 1, 2, 3 เท่านั้น (ตัวอื่นถูกถอดออก)
$user->roles()->sync([1, 2, 3]);

// แนบ pivot data ไปพร้อมกันได้
$user->roles()->sync([
    1 => ['expires_at' => now()->addMonth()],
    2 => ['expires_at' => now()->addMonths(3)],
]);
php

หากต้องการเพิ่มความสัมพันธ์ใหม่โดย “ไม่ถอด” ตัวเดิมที่มีอยู่ออก ให้ใช้ syncWithoutDetaching() แทน

// เพิ่ม role 4, 5 เข้าไป โดย role เดิมยังอยู่ครบ
$user->roles()->syncWithoutDetaching([4, 5]);
php

และเนื่องจากเราประกาศ withPivot(['assigned_by', 'expires_at']) ไว้ที่ Model ตั้งแต่ต้น จึงสามารถอ่านข้อมูลใน Pivot Table ผ่าน property pivot ได้โดยตรง

foreach ($user->roles as $role) {
    echo $role->pivot->expires_at;   // อ่านค่า expires_at จากตาราง role_user
    echo $role->pivot->assigned_by;
}
php

🎯 ใส่เงื่อนไขให้ Eager Loading และโหลดแบบซ้อนชั้น#

โดยปกติ with() จะดึงความสัมพันธ์มาทั้งหมด แต่หลายครั้งเราอยากดึงมาเฉพาะที่ตรงเงื่อนไขเท่านั้น เพื่อลดข้อมูลที่ไม่จำเป็น ทำได้โดยส่ง closure เข้าไปกำกับความสัมพันธ์นั้น

// ดึง user มาพร้อม post แต่เอาเฉพาะ post ที่เผยแพร่แล้ว
$users = User::with(['posts' => fn ($query) => $query->where('status', 'published')])->get();
php

และหากความสัมพันธ์มีหลายชั้นซ้อนกัน เราสามารถ eager load แบบ nested ได้ด้วยการใช้จุด (.) คั่นชื่อความสัมพันธ์ เช่น ต้องการดึง post พร้อมเจ้าของ post และ profile ของเจ้าของในคราวเดียว

// โหลด post -> user (เจ้าของ) -> profile แบบซ้อนชั้นในครั้งเดียว
$posts = Post::with('user.profile')->get();

// ใส่หลายความสัมพันธ์พร้อมกันได้
$posts = Post::with(['user.profile', 'comments'])->get();
php

🔑 กำหนด Foreign Key เองให้ belongsTo#

โดยค่าเริ่มต้น Eloquent จะเดาชื่อ foreign key จากชื่อความสัมพันธ์ให้อัตโนมัติ (เช่น ความสัมพันธ์ country จะมองหาคอลัมน์ country_id) แต่ถ้าโครงสร้างตารางของเราใช้ชื่อคอลัมน์ที่ต่างออกไป ก็สามารถระบุเองได้ผ่าน argument ตัวที่ 2 (foreign key) และตัวที่ 3 (owner key หรือ primary key ของตารางปลายทาง)

// belongsTo(RelatedModel::class, foreign_key, owner_key)
public function country() {
    return $this->belongsTo(Country::class, 'country_id', 'id');
}
php

📚 สรุปและแนวทางต่อยอด#

จากตัวอย่างจะเห็นว่า หากเราสามารถใช้ Eloquent Relationships ได้อย่างถูกต้อง จะทำให้โค้ดสั้นขึ้นและอ่านง่ายขึ้นมาก ทำให้เราบริหารจัดการความสัมพันธ์ของ Model ได้ดียิ่งขึ้น แต่ทั้งนี้ในการใช้งานโปรเจกต์จริง อาจมีเงื่อนไขที่ซับซ้อนมากกว่านี้อีกมาก ซึ่งใน Document ของ Laravel ก็ได้มีการอัปเดตอยู่อย่างสม่ำเสมอ ดังนั้นหากต้องการใช้งาน Eloquent Relationships ให้ชำนาญจะต้องศึกษาเอกสารควบคู่ไปด้วย เพียงเท่านี้เราก็สามารถบริหารจัดการโค้ดของเราให้เป็นระเบียบและพร้อมเรียกใช้ข้อมูลได้แล้ว

🥰

Code, coffee, and calm — the holy trinity of a good day. ☕

การใช้งาน Eloquent Relationships - เพื่อดึงข้อมูลใน Laravel อย่างมีประสิทธิภาพ
Author Coffee Stack
Published at 11 สิงหาคม 2025