Eloquent คือ ORM ที่มากับ Laravel ใช้แนวคิด Active Record แต่ละ model แทนหนึ่งตารางในฐานข้อมูล ทำให้เราคุยกับ database ได้ง่าย บทความนี้จะพาไปดู property และ method ของ Eloquent ที่เราอาจมองข้ามไป แต่ช่วยให้ code ของเราสะอาดขึ้นได้
$snakeAttributes เปลี่ยนอะไรบ้าง
model ทุกตัวมี static property ตัวนี้อยู่
/**
* Indicates whether attributes are snake cased on arrays.
*
* @var bool
*/
public static $snakeAttributes = true;
ตอนแรกเราอาจเข้าใจว่าถ้าตั้งค่านี้เป็น false แล้วจะเรียก attribute แบบ camelCase ได้ เช่น $user->firstName จริง ๆ ไม่ใช่แบบนั้น และไม่ควรเอามาใช้แบบนั้นด้วย ค่านี้มีผลแค่ตอนแปลง model เป็น array ว่าชื่อ key จะออกมาเป็น snake_case หรือ camelCase
กำหนดจำนวนรายการต่อหน้าไว้ที่ model ด้วย $perPage
แบ่งหน้าด้วย Eloquent ทำได้ง่ายมาก เราน่าจะคุ้นกับ paginate() แบบนี้อยู่แล้ว
$comments = Comment::paginate(20);
บรรทัดนี้แบ่ง comment ออกเป็นหน้าละ 20 รายการ ตัวเลขที่ส่งเข้าไปคือจำนวนรายการต่อหน้า ถ้าไม่ส่งอะไรเลย Laravel จะใช้ค่าเริ่มต้นคือ 15
สมมติว่า comment แสดงอยู่หลายที่บนเว็บ และเราอยากให้ทุกที่แสดงหน้าละ 30 รายการเสมอ เราไม่ต้องเขียน paginate(30) ทุกที่ แล้วต้องตามไปแก้ทุกจุดตอนเปลี่ยนใจ กำหนดค่า 30 ไว้ที่ model ได้เลย
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
protected $perPage = 30;
// ...
}
ทีนี้ Comment::paginate() ที่ไม่ส่งตัวเลขจะได้หน้าละ 30 รายการทุกที่
เพิ่มค่าที่ไม่มีในตารางให้ model ด้วย accessor
Eloquent มีฟีเจอร์ชื่อ accessor ใช้เพิ่ม field ที่ไม่มีอยู่จริงในตารางให้ model จะคำนวณจากค่าที่มีอยู่แล้วหรือสร้างขึ้นมาใหม่เลยก็ได้ return อะไรออกมาก็ได้ ลองดูตัวอย่าง สมมติว่าใน model User เราเขียนแบบนี้
use Illuminate\Database\Eloquent\Casts\Attribute;
protected function fullName(): Attribute
{
return Attribute::make(
get: fn () => sprintf('%s %s', $this->first_name, $this->last_name),
);
}
เราจะเรียก full_name บน model User ได้แบบนี้
User::latest()->first()->full_name;
ปัญหาคือตอนแปลง model เป็น array หรือ JSON เช่น return model ออกจาก controller หรือส่งเป็น props ให้ Inertia ค่า full_name จะไม่ติดไปด้วย
> User::latest()->first()->toArray()
= [
"id" => 1,
"first_name" => "Tre",
"last_name" => "Murazik",
"email" => "[email protected]",
"email_verified_at" => "2021-01-31T10:16:19.000000Z",
"created_at" => "2021-01-31T10:16:19.000000Z",
"updated_at" => "2021-01-31T10:16:19.000000Z",
]
ให้เพิ่ม $appends ใน model ใส่ชื่อ accessor ที่อยากให้ติดไปด้วยเป็น array ใน Laravel 13 จะเขียนเป็น attribute #[Appends] บนคลาสแทนก็ได้
protected $appends = ['full_name'];
use Illuminate\Database\Eloquent\Attributes\Appends;
#[Appends(['full_name'])]
class User extends Authenticatable
{
// ...
}
ทีนี้ตอนแปลงเป็น array จะมี full_name ติดมาด้วย
> User::latest()->first()->toArray()
= [
"id" => 1,
"first_name" => "Tre",
"last_name" => "Murazik",
"email" => "[email protected]",
"email_verified_at" => "2021-01-31T10:16:19.000000Z",
"created_at" => "2021-01-31T10:16:19.000000Z",
"updated_at" => "2021-01-31T10:16:19.000000Z",
"full_name" => "Tre Murazik",
]
รับค่าเข้า column ที่ไม่มีอยู่จริงด้วย mutator
Mutator ทำงานกลับกันกับ accessor คือจัดการค่าตอนเรา set เข้า model ใช้ได้หลายแบบ เช่น แปลง input หลายรูปแบบให้ลง column เดียวกัน
ลองดูตัวอย่างนี้ เราเก็บความยาวของวิดีโอ ปกติเราจะเก็บเป็นหน่วยที่เล็กที่สุด ในที่นี้คือวินาที แต่ในแง่ UX เราไม่อยากให้ผู้ใช้กรอกเป็นวินาที อยากให้กรอกเป็นนาทีหรือชั่วโมงก็ได้ เราแปลงค่าเป็นวินาทีก่อนเก็บได้ง่าย ๆ แบบนี้
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
class Video extends Model
{
protected $fillable = ['title', 'duration_in_minutes', 'duration_in_hours'];
protected function durationInMinutes(): Attribute
{
return Attribute::make(
set: fn (int $value) => ['duration_in_seconds' => $value * 60],
);
}
protected function durationInHours(): Attribute
{
return Attribute::make(
set: fn (int $value) => ['duration_in_seconds' => $value * 60 * 60],
);
}
}
ถ้า set คืนค่าเป็น array Eloquent จะเอา key ใน array ไปเป็น column ที่จะ set แทนชื่อ attribute เดิม เราจึงใช้ duration_in_minutes หรือ duration_in_hours ได้ ทั้งที่ในตารางไม่มี column นี้ ตารางมีแค่ duration_in_seconds แล้ว Eloquent คำนวณค่าให้ตามที่เราเขียนไว้ใน mutator ใช้งานแบบนี้
public function update(Request $request, Video $video): RedirectResponse
{
$video->update($request->validate([
'title' => ['required', 'string', 'max:255'],
'duration_in_minutes' => ['required', 'integer', 'min:1'],
]));
return back();
}
controller ไม่ต้องมี logic คำนวณเอง code ก็สั้นและอ่านง่ายขึ้น เพราะการคำนวณไปอยู่ใน mutator ของ model แล้ว
ให้ model โหลด relation มาทุกครั้งด้วย $with
ปกติ Laravel โหลด relation แบบ lazy loading คือตอนอ่าน model มา Eloquent ยังไม่ดึง relation มาให้จนกว่าเราจะเรียกใช้ ข้อดีคือประหยัด memory และไม่ดึงข้อมูลเกินที่ใช้ ลองดู code นี้
foreach (Post::all() as $post) {
echo $post->author->name;
}
เราดึง post ทั้งหมดมา แล้ว loop แสดงชื่อผู้เขียนของแต่ละ post ทีนี้เพราะเป็น lazy loading Eloquent จะ query หา author ตอนที่เราเรียก $post->author ในแต่ละรอบ เขียนแบบนี้จะเจอปัญหาที่เรียกว่า N+1
ที่เรียกว่า N+1 เพราะ N คือจำนวน post และ 1 คือ query แรกที่ดึง post ทั้งหมด เช่น มี 500 post เราจะมี 1 query ดึง post ทั้งหมด และอีก 1 query ต่อ post เพื่อดึง author รวมเป็น 500 + 1 query ยิ่งมี post เยอะ query ก็ยิ่งเยอะตาม
เราแก้ได้ด้วย eager loading
$posts = Post::with('author')->get();
foreach ($posts as $post) {
echo $post->author->name;
}
แบบนี้จะเหลือแค่ 2 query คือ query แรกดึง post ทั้งหมด อีก query ดึง author ของ post เหล่านั้น เบื้องหลังจะได้ SQL ประมาณนี้
SELECT id, author_id, body FROM posts;
SELECT name FROM authors WHERE id IN (1, 2, 3, 4, 5, ...);
ไม่ว่าจะมี post 50, 100 หรือ 10,000 ก็ยังเป็น 2 query
ตัวอย่างนี้เราสั่ง eager loading เองทุกครั้ง ถ้าอยากให้ model โหลด relation บางตัวมาให้ทุกครั้งที่ query กำหนด property $with ไว้ที่ model ได้เลย
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $with = ['author'];
}
ทีนี้ทุกครั้งที่ query Post เราจะได้ author มาด้วยโดยไม่ต้องเรียก with() เอง
ถ้าอยากดู eager loading แบบ nested relation และรายละเอียดเพิ่ม อ่านต่อได้ที่ เพิ่มประสิทธิภาพ Laravel Eloquent Queries ด้วย Eager Loading
ถ้าเราใช้ $with เพื่อกัน N+1 มีอีกทางที่ปลอดภัยกว่า คือเปิด preventLazyLoading ไว้ตอน dev ทุกครั้งที่เกิด lazy loading Laravel จะโยน exception ให้เห็นทันที แล้วเราค่อยใส่ with() เฉพาะจุดที่ใช้
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}
Laravel 13 ยังมี Model::automaticallyEagerLoadRelationships() ด้วย ตัวนี้ให้ Eloquent eager load relation ของทั้ง collection ให้เองตอนที่เราเรียกใช้ relation ครั้งแรก ดูเพิ่มได้ที่ docs เรื่อง eager loading
ดึง ID ทั้งหมดจาก query ด้วย modelKeys()
บ่อยครั้งเราอยากได้ ID ทั้งหมดจาก query ไม่ว่า query จะซับซ้อนแค่ไหน ส่วนใหญ่เราจะเขียนประมาณนี้
User::all()->pluck('id');
ได้ผลก็จริง แต่ได้กลับมาเป็น collection ถ้าอยากได้ array ต้องเรียก toArray() ต่ออีกที
User::all()->pluck('id')->toArray();
เขียนให้สั้นลงได้ด้วย modelKeys()
User::modelKeys();
User::where('active', true)->modelKeys();
modelKeys() คืนค่าเป็น array ปกติจะเป็น ID แต่ไม่จำเป็นต้องเป็น ID เสมอไป Eloquent ใช้ column ที่กำหนดไว้ใน $primaryKey ซึ่งเราเปลี่ยนเป็น column อื่นให้ตรงกับตารางของเราได้
protected $primaryKey = 'id';





