6 เทคนิค Eloquent ที่ช่วยให้ code ของคุณดูง่ายขึ้น

31 มกราคม 2564
6 เทคนิค Eloquent ที่ช่วยให้ code ของคุณดูง่ายขึ้น

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 ของเราทำงานอ่านได้ง่ายขึ้นและทำงานได้มีประสิทธิภาพมากขึ้นด้วยเช่นกัน

Phattarachai Chaimongkol

เกี่ยวกับ phattarachai.dev

มองหาคนช่วยทำ Web App ใช้ภายในธุรกิจอยู่มั้ยครับ
มีความชำนาญในการพัฒนา Web Application ด้วย Laravel รับพัฒนาโปรเจคให้ผู้ประกอบการ ธุรกิจ SME ทั้งขนาดเล็กและขนาดใหญ่ พัฒนาระบบใช้ในองค์กรทั้งภาครัฐและเอกชน เป็นพาร์ทเนอร์กับบริษัททางด้าน Digital Agency เพื่อพัฒนาโปรเจคให้แก่ลูกค้า ทักเข้ามาพูดคุยกันก่อนได้เลยครับ

เรื่องล่าสุด

Backup ฐานข้อมูลด้วย Laravel อย่างง่าย ๆ ด้วย Spatie DB Snapshots
31 มกราคม 2564
Backup ฐานข้อมูลด้วย Laravel อย่างง่าย ๆ ด้วย Spatie DB Snapshots
วิธีการให้ git จดจำ password โดยไม่ต้องระบุใหม่ทุกครั้ง
31 มกราคม 2564
วิธีการให้ git จดจำ password โดยไม่ต้องระบุใหม่ทุกครั้ง
วิธีการเช็ค Detect Browser ผู้ใช้จาก Laravel
31 มกราคม 2564
วิธีการเช็ค Detect Browser ผู้ใช้จาก Laravel