6 เทคนิค Eloquent ที่ช่วยให้ code ของคุณดูง่ายขึ้น
31 มกราคม 2564
Eloquent เป็น ORM ที่มาพร้อมกับ Laravel มีการใช้งานแบบ Active Record และทำให้เราสามารถติดต่อกับ database ได้ง่าย แต่ละ Model ใช้แทนตารางในฐานข้อมูลที่เราทำงานด้วย ในบทความนี้เราจะพาไปดูเคล็ดลับ method และ property ที่นักพัฒนาหลาย ๆ คนอาจจะไม่รู้ว่าจะช่วยปรับปรุง code ของเราให้ทำงานดีขึ้นได้
Snake Attributes
ลองดูตัวอย่าง code ต่อไปนี้เพื่อใช้งาน Snake Attribute
/**
* Indicates whether attributes are snake cased on arrays.
*
* @var bool
*/
public static $snakeAttributes = true;
ส่วนใหญ่แล้วคนมักจะสับสนว่าใช้ property นี้แล้วจะเปลี่ยนวิธีที่เราเรียกใช้ model attributes หลายคนคิดว่าถ้าเปลี่ยนค่านี้แล้วจะทำให้สามารถเรียกใช้ attribute แบบ camel-case จริง ๆ แล้วไม่ใช่แบบนั้นเลยและเป็นวิธีที่ไม่แนะนำให้ใช้ การเลี่ยน property นี้มีผลเพียงแค่กำหนดว่าเมื่อ model แสดงออกมาเป็น array แล้วจะให้ attribute เป็นแบบ camel หรือว่า snake-case กันแน่
Pagination
การทำ Pagination โดยใช้ Laravel Eloquent ORM นั้นง่ายมาก เราอาจจะคุ้นเคยกับการใช้ method paginate()
ลักษณะนี้
$comments = Comment::paginate(20);
ด้วย method นี้เราสามารถ paginate โมเดล comment โดยแสดงหน้าละ 20 รายการ การเปลี่ยนค่านี้ทำให้เราสามารถกำหนดได้ว่าเราต้องการให้แสดงจำนวนกี่รายการในหนึ่งหน้า ถ้าเราไม่ได้ระบุค่าอะไร ค่าเริ่มต้นจะถูกใช้งาน ซึ่งก็คือ 15 รายการ
สมมุติว่าเราต้องการกำหนดให้ comment ที่แสดงในหลาย ๆ ที่บนเว็บไซต์ของเรา แสดง 30 รายการต่อหน้าเสมอ เราไม่จำเป็นต้องเรียก paginate(30) ทุกครั้ง แล้วตามไปเปลี่ยนในทุก ๆ ที่ถ้าเราเปลี่ยนใจอยากแสดงจำนวนอื่นที่ไม่ใช่ 30 รายการต่อหน้า ดังนั้นเราสามารถกำหนด ค่า 30 ไว้ที่คลาส Model ได้เลยโดยตรง ดังนี้
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Author extends Model
{
protected $perPage = 30;
// ...
}
เพิ่ม custom values ไปยัง Model
Eloquent มีฟีเจอร์ที่เรียกว่า "Accessor" ฟีเจอร์นี้ทำให้เราเพิ่ม custom field ไปยัง Model ที่ไม่ได้มีค่านั้นจริงอยู่ในตารางได้ ไม่จำเป็นว่าเราจะใช้ค่าที่มีอยู่แล้วหรือสร้างขึ้นมาใหม่เลย เราสามารถ return ค่าอะไรออกมาก็ได้ อันนี้เป็นตัวอย่างในการใช้งาน Accessor สมมุติว่าเรามี Model ชื่อ User โดยระบุ code ดังนี้
function getFullNameAttribute() {
return sprintf('%s %s', $this->first_name, $this->last_name);
}
เราจะสามารถใช้ attribute full_name ได้บน Model User แบบนี้
User::latest()->first()->full_name;
ปัญหาคือถ้าเรา return เป็น object อย่างเวลาเรียกใน collection ค่า attribute นี้จะไม่ถูกดึงมาด้วย
App\Models\User {#4287
id: 1,
first_name: "Tre",
last_name: "Murazik",
email: "stanton.nayeli@example.net",
email_verified_at: "2021-01-31 10:16:19",
created_at: "2021-01-31 10:16:19",
updated_at: "2021-01-31 10:16:19",
},
เพิ่ม attribute $appends
ไปยัง model โดยระบุ array ของชื่อ custom field ที่เราต้องการให้รวมใน collection ด้วย
protected $appends = ['full_name'];
เมื่อเราเรียกผ่าน collection จะมี full_name ปรากฏขึ้นมาด้วย
App\Models\User {#4287
id: 1,
first_name: "Tre",
last_name: "Murazik",
full_name: "Tre Murazik",
email: "stanton.nayeli@example.net",
email_verified_at: "2021-01-31 10:16:19",
created_at: "2021-01-31 10:16:19",
updated_at: "2021-01-31 10:16:19",
},
ใช้ Mutator กับ column ที่ไม่มีอยู่จริง
Mutoator เป็นฟีเจอร์ที่ตรงกันข้ามกับ Accessor เราสามารถใช้ทำอะไรได้หลายอย่าง เช่น การแปลงค่าเพื่อ save ลง column จาก input หลาย ๆ แบบ ลองดูจากตัวนี้ สมมติว่าเราต้องการบันทึกค่าระยะเวลาอะไรสักอย่างหนึ่ง ปกติแล้วเราจะเก็บหน่วยย่อยที่สุด อย่างกรณีนี้เราจะเก็บเวลาเป็นจำนวนวินาที ทีนี้โดยเหตุผลทาง UX แล้วเราไม่อยากให้ User คีย์เป็นวินาที เราจะให้ User คีย์ข้อมูลเข้ามเป็นนาทีหรือชั่วโมงเลยก้ได้ เราสามารถเก็บข้อมูลเป็นวินาทีจาก input ที่เข้ามาได้ง่าย ๆ แบบนี้
class Video extends Model
{
public function setDurationInMinutesAttribute($value)
{
$this->attributes['duration_in_seconds'] = $value * 60;
}
public function setDurationInHoursAttribute($value)
{
$this->attributes['duration_in_seconds'] = $value * 60 * 60;
}
}
จากตัวอย่างนี้แปลว่าเราสามารถใช้ column ที่ไม่ได้มีจริงใน table ในฐานข้อมูล duration_in_minutes
หรือ duration_in_hours
แต่เรามี column จริงที่ชื่อว่า duration_in_seconds โดยค่าจะถูก update ตามการคำนวณที่เราระบุไว้ใน Mutator โดยมีตัวอย่างการใช้งานดังนี้
class VideoController
{
public function store()
{
$video->update([
'title' => request('title'),
'duration_in_minutes' => request('duration_in_minutes'),
]);
}
}
วิธีนี้ช่วยให้เราลด logic และการคำนวณใน Controller ลงได้ ทำให้ code เราดูเรียบง่ายมากขึ้น โดยอาศัยการซ่อนการคำนวณไว้ใน mutator ภายใน Model นั่นเอง
Eager loading โดยใช้ $with
โดยปกติแล้ว Laravel จะทำการ "Lazy Loading" หมายความว่าเวลาเราอ่านข้อมูลจาก Model จะไม่มีการดึงข้อมูล relation ที่เกี่ยวข้องมาด้วยถ้าเราไม่ต้องการใช้งาน วิธีการนี้มีข้อดีคือช่วยลดการใช้ memory และ performance ของการอ่านข้อมูล เพราะไม่จำเป็นต้องดึงข้อมูลมาเยอะเกินกว่าที่ใช้งาน ลองดูจาก code ตัวอย่างนี้
foreach (Post::all() as $post) {
echo $post->author->name;
}
จากตัวอย่างด้านบนเราจะได้ข้อมูล Post มาทั้งหมด แล้ว loop ทีละ post เพื่อแสดงชื่อผู้เขียนของแต่ละ post ทีนี้ด้วยวิธีการแบบ lazy load query ที่เรียกชื่อผู้เขียน จะถูกทำงานตอนที่เราเรียกใช้เท่านั้น วิธีการเขียนแบนี้ทำให้เกิดปัญหาที่เรียกว่า N+1
ที่เรียกว่าเป็นปัญหาแบบ N+1 เพราะว่า N จะเป็นจำนวนของ Post และ 1 เป็นจำนวน query ที่อ่าน post ทั้งหมด ตัวอย่างเช่น ถ้าเรามี 500 Post แล้วเราเรียก query 1 ครั้งเพื่อดึง Post ทั้งหมดึข้นมา และอีก 1 query ของแต่ละ Post เพื่อดึง author ขึ้นมาด้วย ดังนั้นจึงเป็น 500 + 1 query นั่นหมายความว่าจำนวน query จะเพิ่มขึ้นด้วยถ้าเราดึง Post ขึ้นมามากขึ้น
เราสามารถป้องกันการ query จำนวนมากแบบนี้ได้ด้วยการใช้ Eager Loading
$posts = Post::with('author')->get();
foreach ($posts as $post) {
echo $post->user->name;
}
ด้วยวิธีการนี้จะทำให้เหลือเพียง 2 query เท่านั้น query แรกเพื่ออ่าน Post ทั้งหมด และอีก query เพื่อดึง Author ที่เกี่ยวกับ Post ขึ้นมา โดยเบื้องหลังแล้วจะทำให้เกิด SQL Query ลักษณะนี้
SELECT id, author_id, body FROM posts;
SELECT name FROM authors WHERE id IN (1,2,3,4,5...);
ไม่ว่าจะมี Post 50, 100, 10,000 หรือจำนวนเท่าไหร่ก็ตามจำนวน query จะมีเพียง 2 ครั้งเท่านั้น
เราได้เห็นวิธีการใช้งาน Eager Loading แล้วแต่เป็นเพียงการใช้งานแบบ Manual ถ้าเราต้องการให้ทุกครั้งที่เราเรียกใช้ Model แล้วมีการดึง relation บางตัวที่เกี่ยวข้องมาให้โดยอัตโนมัติแบบ Eager Loading เราสามารถกำหนด Property ที่ Model ได้เลยแบบนี้
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $with =['author'];
}
ทีนี้ทุกครึ่งที่เรามีการ query Post เราจะได้ author มาด้วยทุกครั้งโดยไม่จำเป็นต้องเรียก method with()
อีกครั้ง
นอกจากตัวอย่างนี้แล้วเรายังสามารถทำ Eager Loading กับ nested relation ได้สามารถดูวิธีการและคำอธิบายเชิงลึกจาก Laravel Eloquent Eager Loading
Model keys
บ่อย ๆ ครั้งเราต้องการดึง ID ทั้งหมดจาก query ไม่สำคคัญว่า query จะมีความซับซ้อนแค่ไหนหรือไม่ ส่วนใหญ่แล้วเราสามารถเราจะใช้วิธีประมาณนี้
User::all()->pluck('id');
วิธีการนี้ใช้งานได้แต่เราจะได้เป็น collection กลับมา ถ้าเราต้องการเป็น array เราต้องเรียก method toArray()
อีกครั้ง
User::all()->pluck('id')->toArray();
โดยทั่ว ๆ ไปแล้วเราสามารถเขียนให้สั่นลงได้โดยใช้ method modelKeys()
แบบนี้
User::all()->modelKeys();
method นี้จะ return ออกมาเป็น array โดย default แล้วจะเป็น ID แต่จริง ๆ แล้ว method นี้ไม่จำเป็นต้อง return ID ออกมาเสมอไป โดย method นี้จะใช้ field ที่ระบุไว้ใน $primaryKey
ซึ่งเราสามารถเปลี่ยนเป็น field อื่นได้ให้ตรงกับการใช้งานของเรา
protected $primaryKey = 'id';
สรุป
จากบทความนี้ได้แนะนำเคล็ดลับเกี่ยวกับ Eloquent ทั้ง method และ property ที่มือใหม่หรือแม้แต่นักพัฒนา Laravel ที่ช่ำชองแล้วอาจจะมองข้ามและไม่รู้ว่ามีให้ใช้งานที่สามารถช่วยให้ code ของเราทำงานอ่านได้ง่ายขึ้นและทำงานได้มีประสิทธิภาพมากขึ้นด้วยเช่นกัน