

🤔 ทำไม 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 อีกที)
- Polymorphic - หรือ Polymorphism หมายถึง การมีหลายรูปแบบ ซึ่งในกรณีของ Eloquent Relationships นี้คำว่า Polymorphic มีความหมายเฉพาะตัวคือ การที่ Model หนึ่งสามารถมีความสัมพันธ์แบบ One-to-Many กับหลาย Model ได้ (หลายรูปแบบ) ผ่าน field เดียวกัน โดยไม่ต้องเพิ่ม foreign key
ในความเป็นจริงแล้วคำศัพท์เหล่านี้มีรายละเอียดเฉพาะตัวอีกมาก แต่ในที่นี้ขอให้เข้าใจพื้นฐานโดยคร่าวก่อน ก็เพียงพอที่จะดีไซน์ความสัมพันธ์ได้อย่างมีประสิทธิภาพแล้ว
📝 ประเภทของ 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_tablebashหลังจากสร้างไฟล์ 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();
});
}phpcreate_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');
});
}phpcreate_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();
});
}phpcreate_roles_table
public function up(): void {
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('label')->nullable();
$table->timestamps();
});
}phpcreate_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']);
});
}phpcreate_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');
});
}phpcreate_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']);
});
}phpcreate_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');
});
}phpcreate_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();
});
}phpcreate_tags_table
public function up(): void {
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->timestamps();
});
}phpcreate_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 migratebashเป็นอันเรียบร้อยสำหรับการเตรียมฐานข้อมูล จากนั้นให้เตรียม 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 Tagbashจากนั้นแก้ไขไฟล์ของ 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->hasOneThrough(Post::class, User::class)->latestOfMany();
}
}phpUser.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'); }
}phpProfile.php
class Profile extends Model
{
protected $fillable = ['user_id', 'bio', 'phone'];
public function user() {
return $this->belongsTo(User::class);
}
}phpRole.php
class Role extends Model
{
protected $fillable = ['name','label'];
public function users() {
return $this->belongsToMany(User::class)->withTimestamps();
}
}phpPost.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']); }
}phpComment.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); }
}phpVideo.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']); }
}phpPhoto.php
class Photo extends Model
{
protected $fillable = ['path','alt'];
public function imageable() {
return $this->morphTo();
}
}phpTag.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()->latestOfMany()->first();
// 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📚 สรุปและแนวทางต่อยอด#
จากตัวอย่างจะเห็นว่า หากเราสามารถใช้ Eloquent Relationships ได้อย่างถูกต้อง จะทำให้โค้ดสั้นขึ้นและอ่านง่ายขึ้นมาก ทำให้เราบริหารจัดการความสัมพันธ์ของ Model ได้ดียิ่งขึ้น แต่ทั้งนี้ในการใช้งานโปรเจกต์จริง อาจมีเงื่อนไขที่ซับซ้อนมากกว่านี้อีกมาก ซึ่งใน Document ของ Laravel ก็ได้มีการอัปเดตอยู่อย่างสม่ำเสมอ ดังนั้นหากต้องการใช้งาน Eloquent Relationships ให้ชำนาญจะต้องศึกษาเอกสารควบคู่ไปด้วย เพียงเท่านี้เราก็สามารถบริหารจัดการโค้ดของเราให้เป็นระเบียบและพร้อมเรียกใช้ข้อมูลได้แล้ว 🥰